github.com/diadata-org/diadata@v1.4.593/pkg/dia/helpers/stackshelper/client.go (about)

     1  package stackshelper
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/tls"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"net/http/httputil"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/sirupsen/logrus"
    17  )
    18  
    19  const (
    20  	StacksURL                = "https://api.mainnet.hiro.so"
    21  	DefaultSleepBetweenCalls = 1000  // ms
    22  	DefaultRefreshDelay      = 10000 // ms
    23  	MaxPageLimit             = 50
    24  )
    25  
    26  type StacksClient struct {
    27  	debug             bool
    28  	httpClient        *http.Client
    29  	logger            *logrus.Entry
    30  	sleepBetweenCalls time.Duration
    31  	apiKey            string
    32  }
    33  
    34  func NewStacksClient(logger *logrus.Entry, sleepBetweenCalls time.Duration, hiroAPIKey string, isDebug bool) *StacksClient {
    35  	tr := &http.Transport{
    36  		TLSClientConfig: &tls.Config{
    37  			MinVersion: tls.VersionTLS12,
    38  			MaxVersion: 0,
    39  		},
    40  	}
    41  	httpClient := &http.Client{
    42  		Transport: tr,
    43  		Timeout:   10 * time.Second,
    44  	}
    45  
    46  	c := &StacksClient{
    47  		httpClient:        httpClient,
    48  		debug:             isDebug,
    49  		logger:            logger,
    50  		sleepBetweenCalls: sleepBetweenCalls,
    51  		apiKey:            hiroAPIKey,
    52  	}
    53  
    54  	if hiroAPIKey != "" {
    55  		logger.Info("found hiro stacks API key, decreasing client request timeout")
    56  		c.sleepBetweenCalls = 120 * time.Millisecond
    57  	}
    58  
    59  	return c
    60  }
    61  
    62  func (c *StacksClient) GetLatestBlock() (Block, error) {
    63  	var block Block
    64  
    65  	url := fmt.Sprintf("%s/extended/v2/blocks/latest", StacksURL)
    66  	req, _ := http.NewRequest(http.MethodGet, url, http.NoBody)
    67  
    68  	err := c.callStacksAPI(req, &block)
    69  	if err != nil {
    70  		return block, err
    71  	}
    72  	return block, nil
    73  }
    74  
    75  func (c *StacksClient) GetTransactionAt(txID string) (Transaction, error) {
    76  	var transaction Transaction
    77  
    78  	url := fmt.Sprintf("%s/extended/v1/tx/%s", StacksURL, txID)
    79  	req, _ := http.NewRequest(http.MethodGet, url, http.NoBody)
    80  
    81  	err := c.callStacksAPI(req, &transaction)
    82  	if err != nil {
    83  		return transaction, err
    84  	}
    85  	return transaction, nil
    86  }
    87  
    88  func (c *StacksClient) GetAllBlockTransactions(height int) ([]Transaction, error) {
    89  	var (
    90  		resp    GetBlockTransactionsResponse
    91  		txs     = make([]Transaction, 0)
    92  		total   = MaxPageLimit
    93  		baseURL = fmt.Sprintf("%s/extended/v2/blocks/%d/transactions", StacksURL, height)
    94  	)
    95  
    96  	for offset := 0; offset < total; offset += MaxPageLimit {
    97  		url := fmt.Sprintf("%s?limit=%d&offset=%d", baseURL, MaxPageLimit, offset)
    98  		req, _ := http.NewRequest(http.MethodGet, url, http.NoBody)
    99  
   100  		err := c.callStacksAPI(req, &resp)
   101  		if err != nil {
   102  			if strings.Contains(err.Error(), "404") {
   103  				break
   104  			}
   105  			return nil, err
   106  		}
   107  
   108  		total = resp.Total
   109  		txs = append(txs, resp.Results...)
   110  	}
   111  
   112  	return txs, nil
   113  }
   114  
   115  func (c *StacksClient) GetAddressTransactions(address string, limit, offset int) (GetAddressTransactionsResponse, error) {
   116  	var resp GetAddressTransactionsResponse
   117  
   118  	url := fmt.Sprintf(
   119  		"%s/extended/v2/addresses/%s/transactions?limit=%d&offset=%d",
   120  		StacksURL,
   121  		address,
   122  		limit,
   123  		offset,
   124  	)
   125  
   126  	req, _ := http.NewRequest(http.MethodGet, url, http.NoBody)
   127  	err := c.callStacksAPI(req, &resp)
   128  	if err != nil {
   129  		return resp, err
   130  	}
   131  	return resp, nil
   132  }
   133  
   134  func (c *StacksClient) GetDataMapEntry(contractID, mapName, key string) ([]byte, error) {
   135  	address := strings.Split(contractID, ".")
   136  
   137  	url := fmt.Sprintf("%s/v2/map_entry/%s/%s/%s", StacksURL, address[0], address[1], mapName)
   138  	body := []byte(fmt.Sprintf(`"%s"`, key))
   139  
   140  	req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body))
   141  	req.Header.Set("Content-Type", "application/json")
   142  
   143  	var entry ContractValue
   144  	if err := c.callStacksAPI(req, &entry); err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	entryBytes, err := hex.DecodeString(entry.Data[2:])
   149  	if err != nil {
   150  		c.logger.WithError(err).Error("failed to decode data-map entry")
   151  		return nil, err
   152  	}
   153  
   154  	result, ok := deserializeCVOption(entryBytes)
   155  	if !ok {
   156  		err = errors.New("data-map entry not found")
   157  		return nil, err
   158  	}
   159  	return result, nil
   160  }
   161  
   162  func (c *StacksClient) GetDataVar(contractAddress, contractName, dataVar string) ([]byte, error) {
   163  	url := fmt.Sprintf("%s/v2/data_var/%s/%s/%s", StacksURL, contractAddress, contractName, dataVar)
   164  	req, _ := http.NewRequest(http.MethodGet, url, http.NoBody)
   165  
   166  	var result ContractValue
   167  	if err := c.callStacksAPI(req, &result); err != nil {
   168  		return nil, err
   169  	}
   170  	return hex.DecodeString(result.Data[2:])
   171  }
   172  
   173  func (c *StacksClient) CallContractFunction(contractAddress, contractName, functionName string, args ContractCallArgs) ([]byte, error) {
   174  	url := fmt.Sprintf("%s/v2/contracts/call-read/%s/%s/%s", StacksURL, contractAddress, contractName, functionName)
   175  
   176  	if args.Arguments == nil {
   177  		args.Arguments = []string{}
   178  	}
   179  	body, err := json.Marshal(args)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body))
   185  	req.Header.Set("Content-Type", "application/json")
   186  
   187  	var resp ContractCallResult
   188  	if err := c.callStacksAPI(req, &resp); err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	if !resp.Okay {
   193  		err = errors.New(resp.Cause)
   194  		c.logger.WithError(err).Error("failed to call a read-only function")
   195  		return nil, err
   196  	}
   197  
   198  	return hex.DecodeString(resp.Result[2:])
   199  }
   200  
   201  func (c *StacksClient) callStacksAPI(request *http.Request, target interface{}) error {
   202  	if len(c.apiKey) > 0 {
   203  		request.Header.Add("X-API-Key", c.apiKey)
   204  	}
   205  
   206  	if c.debug {
   207  		dump, err := httputil.DumpRequestOut(request, true)
   208  		if err != nil {
   209  			c.logger.WithError(err).Error("failed to dump request out")
   210  			return err
   211  		}
   212  		c.logger.Infof("\n%s", string(dump))
   213  	}
   214  
   215  	resp, err := c.httpClient.Do(request)
   216  	if err != nil {
   217  		return err
   218  	}
   219  
   220  	if c.debug && resp != nil {
   221  		dump, err := httputil.DumpResponse(resp, true)
   222  		if err != nil {
   223  			c.logger.WithError(err).Error("failed to dump response")
   224  			return err
   225  		}
   226  		c.logger.Infof("\n%s", string(dump))
   227  	}
   228  
   229  	data, err := io.ReadAll(resp.Body)
   230  	if err != nil {
   231  		c.logger.WithError(err).Error("failed to read response body")
   232  		return err
   233  	}
   234  
   235  	if resp.StatusCode != http.StatusOK {
   236  		err = fmt.Errorf("failed to call Hiro API, status code: %d", resp.StatusCode)
   237  		if resp.StatusCode != http.StatusNotFound {
   238  			c.logger.
   239  				WithField("status", resp.StatusCode).
   240  				WithField("body", string(data)).
   241  				WithField("url", request.URL).
   242  				Error(err.Error())
   243  		}
   244  		return err
   245  	}
   246  
   247  	err = json.Unmarshal(data, &target)
   248  	if err != nil {
   249  		return err
   250  	}
   251  
   252  	c.waiting()
   253  
   254  	return resp.Body.Close()
   255  }
   256  
   257  func (c *StacksClient) waiting() {
   258  	time.Sleep(c.sleepBetweenCalls)
   259  }