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 }