github.com/trezor/blockbook@v0.4.1-0.20240328132726-e9a08582ee2c/fourbyte/fourbyte.go (about)

     1  package fourbyte
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/golang/glog"
    13  	"github.com/linxGnu/grocksdb"
    14  	"github.com/trezor/blockbook/bchain"
    15  	"github.com/trezor/blockbook/db"
    16  )
    17  
    18  // Coingecko is a structure that implements RatesDownloaderInterface
    19  type FourByteSignaturesDownloader struct {
    20  	url                string
    21  	httpTimeoutSeconds time.Duration
    22  	db                 *db.RocksDB
    23  }
    24  
    25  // NewFourByteSignaturesDownloader initializes the downloader for FourByteSignatures API.
    26  func NewFourByteSignaturesDownloader(db *db.RocksDB, url string) (*FourByteSignaturesDownloader, error) {
    27  	return &FourByteSignaturesDownloader{
    28  		url:                url,
    29  		httpTimeoutSeconds: 15 * time.Second,
    30  		db:                 db,
    31  	}, nil
    32  }
    33  
    34  // Run starts the FourByteSignatures downloader
    35  func (fd *FourByteSignaturesDownloader) Run() {
    36  	period := time.Hour * 24
    37  	timer := time.NewTimer(period)
    38  	for {
    39  		fd.downloadSignatures()
    40  		<-timer.C
    41  		timer.Reset(period)
    42  	}
    43  }
    44  
    45  type signatureData struct {
    46  	Id            int    `json:"id"`
    47  	TextSignature string `json:"text_signature"`
    48  	HexSignature  string `json:"hex_signature"`
    49  }
    50  
    51  type signaturesPage struct {
    52  	Count   int             `json:"count"`
    53  	Next    string          `json:"next"`
    54  	Results []signatureData `json:"results"`
    55  }
    56  
    57  func (fd *FourByteSignaturesDownloader) getPage(url string) (*signaturesPage, error) {
    58  	req, err := http.NewRequest("GET", url, nil)
    59  	if err != nil {
    60  		glog.Errorf("Error creating a new request for %v: %v", url, err)
    61  		return nil, err
    62  	}
    63  	req.Close = true
    64  	req.Header.Set("Content-Type", "application/json")
    65  	client := &http.Client{
    66  		Timeout: fd.httpTimeoutSeconds,
    67  	}
    68  	resp, err := client.Do(req)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	defer resp.Body.Close()
    73  	if resp.StatusCode != http.StatusOK {
    74  		return nil, errors.New("Invalid response status: " + string(resp.Status))
    75  	}
    76  	bodyBytes, err := ioutil.ReadAll(resp.Body)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	var data signaturesPage
    81  	err = json.Unmarshal(bodyBytes, &data)
    82  	if err != nil {
    83  		glog.Errorf("Error parsing 4byte signatures response from %s: %v", url, err)
    84  		return nil, err
    85  	}
    86  	return &data, nil
    87  }
    88  
    89  func (fd *FourByteSignaturesDownloader) getPageWithRetry(url string) (*signaturesPage, error) {
    90  	for retry := 1; retry <= 16; retry++ {
    91  		page, err := fd.getPage(url)
    92  		if err == nil && page != nil {
    93  			return page, err
    94  		}
    95  		glog.Errorf("Error getting 4byte signatures from %s: %v, retry count %d", url, err, retry)
    96  		timer := time.NewTimer(time.Second * time.Duration(retry))
    97  		<-timer.C
    98  	}
    99  	return nil, errors.New("Too many retries to 4byte signatures")
   100  }
   101  
   102  func parseSignatureFromText(t string) *bchain.FourByteSignature {
   103  	s := strings.Index(t, "(")
   104  	e := strings.LastIndex(t, ")")
   105  	if s < 0 || e < 0 {
   106  		return nil
   107  	}
   108  	var signature bchain.FourByteSignature
   109  	signature.Name = t[:s]
   110  	params := t[s+1 : e]
   111  	if len(params) > 0 {
   112  		s = 0
   113  		tupleDepth := 0
   114  		// parse params as comma separated list
   115  		// tuple is regarded as one parameter and not parsed further
   116  		for i, c := range params {
   117  			if c == ',' && tupleDepth == 0 {
   118  				signature.Parameters = append(signature.Parameters, params[s:i])
   119  				s = i + 1
   120  			} else if c == '(' {
   121  				tupleDepth++
   122  			} else if c == ')' {
   123  				tupleDepth--
   124  			}
   125  		}
   126  		signature.Parameters = append(signature.Parameters, params[s:])
   127  	}
   128  	return &signature
   129  }
   130  
   131  func (fd *FourByteSignaturesDownloader) downloadSignatures() {
   132  	period := time.Millisecond * 100
   133  	timer := time.NewTimer(period)
   134  	url := fd.url
   135  	results := make([]signatureData, 0)
   136  	glog.Info("FourByteSignaturesDownloader starting download")
   137  	for {
   138  		page, err := fd.getPageWithRetry(url)
   139  		if err != nil {
   140  			glog.Errorf("Error getting 4byte signatures from %s: %v", url, err)
   141  			return
   142  		}
   143  		if page == nil {
   144  			glog.Errorf("Empty page from 4byte signatures from %s: %v", url, err)
   145  			return
   146  		}
   147  		glog.Infof("FourByteSignaturesDownloader downloaded %s with %d results", url, len(page.Results))
   148  		if len(page.Results) > 0 {
   149  			fourBytes, err := strconv.ParseUint(page.Results[0].HexSignature, 0, 0)
   150  			if err != nil {
   151  				glog.Errorf("Invalid 4byte signature %+v on page %s: %v", page.Results[0], url, err)
   152  				return
   153  			}
   154  			sig, err := fd.db.GetFourByteSignature(uint32(fourBytes), uint32(page.Results[0].Id))
   155  			if err != nil {
   156  				glog.Errorf("db.GetFourByteSignature error %+v on page %s: %v", page.Results[0], url, err)
   157  				return
   158  			}
   159  			// signature is already stored in db, break
   160  			if sig != nil {
   161  				break
   162  			}
   163  			results = append(results, page.Results...)
   164  		}
   165  		if page.Next == "" {
   166  			// at the end
   167  			break
   168  		}
   169  		url = page.Next
   170  		// wait a bit to not to flood the server
   171  		<-timer.C
   172  		timer.Reset(period)
   173  	}
   174  	if len(results) > 0 {
   175  		glog.Infof("FourByteSignaturesDownloader storing %d new signatures", len(results))
   176  		wb := grocksdb.NewWriteBatch()
   177  		defer wb.Destroy()
   178  
   179  		for i := range results {
   180  			r := &results[i]
   181  			fourBytes, err := strconv.ParseUint(r.HexSignature, 0, 0)
   182  			if err != nil {
   183  				glog.Errorf("Invalid 4byte signature %+v: %v", r, err)
   184  				return
   185  			}
   186  			fbs := parseSignatureFromText(r.TextSignature)
   187  			if fbs != nil {
   188  				fd.db.StoreFourByteSignature(wb, uint32(fourBytes), uint32(r.Id), fbs)
   189  			} else {
   190  				glog.Errorf("FourByteSignaturesDownloader invalid signature %s", r.TextSignature)
   191  			}
   192  		}
   193  
   194  		if err := fd.db.WriteBatch(wb); err != nil {
   195  			glog.Errorf("FourByteSignaturesDownloader failed to store signatures, %v", err)
   196  		}
   197  
   198  	}
   199  	glog.Infof("FourByteSignaturesDownloader finished")
   200  }