github.com/decred/politeia@v1.4.0/politeiawww/legacy/dcrdata.go (about)

     1  // Copyright (c) 2017-2020 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 legacy
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"net/http"
    13  	"sort"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/decred/dcrd/chaincfg/v3"
    19  	"github.com/decred/dcrd/dcrutil/v3"
    20  	"github.com/decred/politeia/util"
    21  )
    22  
    23  const (
    24  	dcrdataTimeout = 3 * time.Second // Dcrdata request timeout
    25  )
    26  
    27  func (p *Politeiawww) dcrdataHostHTTP() string {
    28  	return fmt.Sprintf("https://%v/api", p.cfg.DcrdataHost)
    29  }
    30  
    31  func (p *Politeiawww) dcrdataHostWS() string {
    32  	return fmt.Sprintf("wss://%v/ps", p.cfg.DcrdataHost)
    33  }
    34  
    35  // BETransaction is an object representing a transaction; it's
    36  // part of the data returned from the URL for the block explorer
    37  // when fetching the transactions for an address.
    38  type BETransaction struct {
    39  	TxId          string              `json:"txid"`          // Transaction id
    40  	Vin           []BETransactionVin  `json:"vin"`           // Transaction inputs
    41  	Vout          []BETransactionVout `json:"vout"`          // Transaction outputs
    42  	Confirmations uint64              `json:"confirmations"` // Number of confirmations
    43  	Timestamp     int64               `json:"time"`          // Transaction timestamp
    44  }
    45  
    46  // BETransactionVin holds the transaction prevOut address information
    47  type BETransactionVin struct {
    48  	PrevOut BETransactionPrevOut `json:"prevOut"` // Previous transaction output
    49  }
    50  
    51  // BETransactionPrevOut holds the information about the inputs' previous addresses.
    52  // This will allow one to check for dev subsidy origination, etc.
    53  type BETransactionPrevOut struct {
    54  	Addresses []string `json:"addresses"` // Array of transaction input addresses
    55  }
    56  
    57  // BETransactionVout holds the transaction amount information.
    58  type BETransactionVout struct {
    59  	Amount       json.Number               `json:"value"`        // Transaction amount (in DCR)
    60  	ScriptPubkey BETransactionScriptPubkey `json:"scriptPubkey"` // Transaction script info
    61  }
    62  
    63  // BETransactionScriptPubkey holds the script info for a
    64  // transaction.
    65  type BETransactionScriptPubkey struct {
    66  	Addresses []string `json:"addresses"` // Array of transaction input addresses
    67  }
    68  
    69  // TxDetails is an object representing a transaction.
    70  // XXX This was previously being used to standardize the different responses
    71  // from the dcrdata and insight APIs. Support for the insight API was removed
    72  // but parts of politeiawww still consume this struct so it has remained.
    73  type TxDetails struct {
    74  	Address        string   // Transaction address
    75  	TxID           string   // Transacion ID
    76  	Amount         uint64   // Transaction amount (in atoms)
    77  	Timestamp      int64    // Transaction timestamp
    78  	Confirmations  uint64   // Number of confirmations
    79  	InputAddresses []string /// An array of all addresses from previous outputs
    80  }
    81  
    82  func makeRequest(ctx context.Context, url string, timeout time.Duration) ([]byte, error) {
    83  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    84  	if err != nil {
    85  		return nil, fmt.Errorf("unable to create request: %v", err)
    86  	}
    87  
    88  	client := &http.Client{
    89  		Timeout: timeout * time.Second,
    90  	}
    91  	response, err := client.Do(req)
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  	defer response.Body.Close()
    96  
    97  	if response.StatusCode != http.StatusOK {
    98  		body, err := io.ReadAll(response.Body)
    99  		if err != nil {
   100  			return nil, fmt.Errorf("dcrdata error: %v %v %v",
   101  				response.StatusCode, url, err)
   102  		}
   103  		return nil, fmt.Errorf("dcrdata error: %v %v %s",
   104  			response.StatusCode, url, body)
   105  	}
   106  
   107  	return io.ReadAll(response.Body)
   108  }
   109  
   110  func fetchTxWithBE(ctx context.Context, url string, address string, minimumAmount uint64, txnotbefore int64, minConfirmationsRequired uint64) (string, uint64, error) {
   111  	responseBody, err := makeRequest(ctx, url, dcrdataTimeout)
   112  	if err != nil {
   113  		return "", 0, err
   114  	}
   115  
   116  	transactions := make([]BETransaction, 0)
   117  	err = json.Unmarshal(responseBody, &transactions)
   118  	if err != nil {
   119  		return "", 0, err
   120  	}
   121  
   122  	for _, v := range transactions {
   123  		if v.Timestamp < txnotbefore {
   124  			continue
   125  		}
   126  		if v.Confirmations < minConfirmationsRequired {
   127  			continue
   128  		}
   129  
   130  		for _, vout := range v.Vout {
   131  			amount, err := util.DcrStringToAtoms(vout.Amount.String())
   132  			if err != nil {
   133  				return "", 0, err
   134  			}
   135  
   136  			if amount < minimumAmount {
   137  				continue
   138  			}
   139  
   140  			for _, addr := range vout.ScriptPubkey.Addresses {
   141  				if address == addr {
   142  					return v.TxId, amount, nil
   143  				}
   144  			}
   145  		}
   146  	}
   147  
   148  	return "", 0, nil
   149  }
   150  
   151  // fetchTxWithBlockExplorers uses public block explorers to look for a
   152  // transaction for the given address that equals or exceeds the given amount,
   153  // occurs after the txnotbefore time and has the minimum number of confirmations.
   154  func fetchTxWithBlockExplorers(ctx context.Context, params *chaincfg.Params, address string, amount uint64, txnotbefore int64, minConfirmations uint64, dcrdataURL string) (string, uint64, error) {
   155  	// pre-validate that the passed address, amount, and tx are at least
   156  	// somewhat valid before querying the explorers
   157  	addr, err := dcrutil.DecodeAddress(address, params)
   158  	if err != nil {
   159  		return "", 0, fmt.Errorf("invalid address %v: %v", addr, err)
   160  	}
   161  
   162  	// Construct proper dcrdata url
   163  	dcrdataURL += "/address/" + address
   164  
   165  	explorerURL := dcrdataURL + "/raw"
   166  
   167  	// Fetch transaction from dcrdata
   168  	txID, amount, err := fetchTxWithBE(ctx, explorerURL, address, amount,
   169  		txnotbefore, minConfirmations)
   170  	if err != nil {
   171  		return "", 0, fmt.Errorf("failed to fetch from dcrdata: %v", err)
   172  	}
   173  
   174  	return txID, amount, nil
   175  }
   176  
   177  func fetchTxsWithBE(ctx context.Context, url string) ([]BETransaction, error) {
   178  	responseBody, err := makeRequest(ctx, url, dcrdataTimeout)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	transactions := make([]BETransaction, 0)
   184  	err = json.Unmarshal(responseBody, &transactions)
   185  	if err != nil {
   186  		return nil, fmt.Errorf("Unmarshal []BETransaction: %v", err)
   187  	}
   188  
   189  	return transactions, nil
   190  }
   191  
   192  func convertBETransactionToTxDetails(address string, tx BETransaction) (*TxDetails, error) {
   193  	var amount uint64
   194  	for _, vout := range tx.Vout {
   195  		amt, err := util.DcrStringToAtoms(vout.Amount.String())
   196  		if err != nil {
   197  			return nil, err
   198  		}
   199  
   200  		for _, addr := range vout.ScriptPubkey.Addresses {
   201  			if address == addr {
   202  				amount += amt
   203  			}
   204  		}
   205  	}
   206  	inputAddresses := make([]string, 0, 1064)
   207  	for _, vin := range tx.Vin {
   208  		inputAddresses = append(inputAddresses, vin.PrevOut.Addresses...)
   209  	}
   210  
   211  	return &TxDetails{
   212  		Address:        address,
   213  		TxID:           tx.TxId,
   214  		Amount:         amount,
   215  		Confirmations:  tx.Confirmations,
   216  		Timestamp:      tx.Timestamp,
   217  		InputAddresses: inputAddresses,
   218  	}, nil
   219  }
   220  
   221  // fetchTxsForAddress fetches the transactions that have been sent to the
   222  // provided wallet address from the dcrdata block explorer
   223  func fetchTxsForAddress(ctx context.Context, params *chaincfg.Params, address string, dcrdataURL string) ([]TxDetails, error) {
   224  	// Get block explorer URL
   225  	addr, err := dcrutil.DecodeAddress(address, params)
   226  	if err != nil {
   227  		return nil, fmt.Errorf("invalid address %v: %v", addr, err)
   228  	}
   229  
   230  	// Construct proper dcrdata url
   231  	dcrdataURL += "/address/" + address
   232  
   233  	explorerURL := dcrdataURL + "/raw"
   234  
   235  	// Fetch using dcrdata block explorer
   236  	dcrdataTxs, err := fetchTxsWithBE(ctx, explorerURL)
   237  	if err != nil {
   238  		return nil, fmt.Errorf("failed to fetch from dcrdata: %v", err)
   239  	}
   240  	txs := make([]TxDetails, 0, len(dcrdataTxs))
   241  	for _, tx := range dcrdataTxs {
   242  		txDetail, err := convertBETransactionToTxDetails(address, tx)
   243  		if err != nil {
   244  			return nil, fmt.Errorf("convertBETransactionToTxDetails: %v",
   245  				tx.TxId)
   246  		}
   247  		txs = append(txs, *txDetail)
   248  	}
   249  	return txs, nil
   250  }
   251  
   252  // fetchTxsForAddressNotBefore fetches all transactions for a wallet address
   253  // that occurred after the passed in notBefore timestamp.
   254  func fetchTxsForAddressNotBefore(ctx context.Context, params *chaincfg.Params, address string, notBefore int64, dcrdataURL string) ([]TxDetails, error) {
   255  	// Get block explorer URL
   256  	addr, err := dcrutil.DecodeAddress(address, params)
   257  	if err != nil {
   258  		return nil, fmt.Errorf("invalid address %v: %v", addr, err)
   259  	}
   260  
   261  	// Construct proper dcrdata url
   262  	dcrdataURL += "/address/" + address
   263  
   264  	// Fetch all txs for the passed in wallet address
   265  	// that were sent after the notBefore timestamp
   266  	var (
   267  		targetTxs []TxDetails
   268  		count     = 10
   269  		skip      = 0
   270  		done      = false
   271  	)
   272  	for !done {
   273  		// Fetch a page of user payment txs
   274  		url := dcrdataURL + "/count/" + strconv.Itoa(count) +
   275  			"/skip/" + strconv.Itoa(skip) + "/raw"
   276  		dcrdataTxs, err := fetchTxsWithBE(ctx, url)
   277  		if err != nil {
   278  			return nil, fmt.Errorf("fetchDcrdataAddress: %v", err)
   279  		}
   280  		// Convert transactions to TxDetails
   281  		txs := make([]TxDetails, len(dcrdataTxs))
   282  		for _, tx := range dcrdataTxs {
   283  			txDetails, err := convertBETransactionToTxDetails(address, tx)
   284  			if err != nil {
   285  				return nil, fmt.Errorf("convertBETransactionToTxDetails: %v",
   286  					tx.TxId)
   287  			}
   288  			txs = append(txs, *txDetails)
   289  		}
   290  		if len(txs) == 0 {
   291  			done = true
   292  			continue
   293  		}
   294  		// Sanity check. Txs should already be sorted
   295  		// in reverse chronological order.
   296  		sort.SliceStable(txs, func(i, j int) bool {
   297  			return txs[i].Timestamp > txs[j].Timestamp
   298  		})
   299  
   300  		// Verify txs are within notBefore limit
   301  		for _, tx := range txs {
   302  			if tx.Timestamp > notBefore {
   303  				targetTxs = append(targetTxs, tx)
   304  			} else {
   305  				// We have reached the notBefore
   306  				// limit; stop requesting txs
   307  				done = true
   308  				break
   309  			}
   310  		}
   311  
   312  		skip += count
   313  	}
   314  
   315  	return targetTxs, nil
   316  }
   317  
   318  // fetchTx fetches a given transaction based on the provided txid.
   319  func fetchTx(ctx context.Context, params *chaincfg.Params, address, txid, dcrdataURL string) (*TxDetails, error) {
   320  	// Get block explorer URLs
   321  	addr, err := dcrutil.DecodeAddress(address, params)
   322  	if err != nil {
   323  		return nil, fmt.Errorf("invalid address %v: %v", addr, err)
   324  	}
   325  
   326  	// Construct proper dcrdata url}
   327  	dcrdataURL += "/address/" + address
   328  
   329  	primaryURL := dcrdataURL + "/raw"
   330  
   331  	log.Debugf("fetching tx %s %s from primary %s\n", address, txid, primaryURL)
   332  	// Try the primary (dcrdata)
   333  	primaryTxs, err := fetchTxsWithBE(ctx, primaryURL)
   334  	if err != nil {
   335  		return nil, fmt.Errorf("failed to fetch from dcrdata: %v", err)
   336  	}
   337  	for _, tx := range primaryTxs {
   338  		if strings.TrimSpace(tx.TxId) != strings.TrimSpace(txid) {
   339  			continue
   340  		}
   341  		txDetail, err := convertBETransactionToTxDetails(address, tx)
   342  		if err != nil {
   343  			return nil, fmt.Errorf("convertBETransactionToTxDetails: %v",
   344  				tx.TxId)
   345  		}
   346  		return txDetail, nil
   347  	}
   348  	return nil, nil
   349  }