github.com/diadata-org/diadata@v1.4.593/pkg/dia/helpers/alephium-helper/client.go (about) 1 package alephiumhelper 2 3 import ( 4 "bytes" 5 "crypto/tls" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "log" 11 "net/http" 12 "net/http/httputil" 13 "strconv" 14 "time" 15 16 "github.com/diadata-org/diadata/pkg/dia" 17 "github.com/sirupsen/logrus" 18 ) 19 20 const ( 21 BackendURL = "https://backend.mainnet.alephium.org" 22 NodeURL = "https://node.mainnet.alephium.org" 23 AYINPairContractAddress = "vyrkJHG49TXss6pGAz2dVxq5o7mBXNNXAV18nAeqVT1R" 24 ) 25 26 const ( 27 SymbolMethod = iota 28 NameMethod 29 DecimalsMethod 30 TokenPairMethod = 7 31 ) 32 33 const ( 34 SwapEventIndex = 2 35 ) 36 37 const ( 38 DefaultRefreshDelay = 400 // millisec 39 DefaultSleepBetweenContractCalls = 300 // millisec 40 DefaultEventsLimit = 100 41 DefaultSwapContractsLimit = 100 42 ) 43 44 // ALPHNativeToken: native alephium token - it has no related contract 45 // details -> https://github.com/alephium/token-list/blob/master/tokens/mainnet.json#L4-L11 46 var ALPHNativeToken = dia.Asset{ 47 Address: "tgx7VNFoP9DJiFMFgXXtafQZkUvyEdDHT9ryamHJYrjq", 48 Symbol: "ALPH", 49 Decimals: 18, 50 Name: "Alephium", 51 } 52 53 // AlephiumClient: interaction with alephium REST API with urls from @BackendURL, @NodeURL contants 54 type AlephiumClient struct { 55 Debug bool 56 HTTPClient *http.Client 57 logger *logrus.Entry 58 sleepBetweenCalls time.Duration 59 } 60 61 // NewAlephiumClient returns AlephiumClient 62 func NewAlephiumClient(logger *logrus.Entry, sleepBetweenCalls time.Duration, debug bool) *AlephiumClient { 63 tr := &http.Transport{ 64 TLSClientConfig: &tls.Config{ 65 MinVersion: tls.VersionTLS12, 66 MaxVersion: 0, 67 }, 68 } 69 httpClient := &http.Client{ 70 Transport: tr, 71 Timeout: 10 * time.Second, 72 } 73 74 result := &AlephiumClient{ 75 HTTPClient: httpClient, 76 Debug: debug, 77 logger: logger, 78 sleepBetweenCalls: sleepBetweenCalls, 79 } 80 81 return result 82 } 83 84 func (c *AlephiumClient) callAPI(request *http.Request, target interface{}) error { 85 if c.Debug { 86 dump, err := httputil.DumpRequestOut(request, true) 87 if err != nil { 88 return err 89 } 90 log.Printf("DumpRequestOut: \n%s\n", string(dump)) 91 } 92 93 resp, err := c.HTTPClient.Do(request) 94 if err != nil { 95 return err 96 } 97 98 if c.Debug && resp != nil { 99 dump, err := httputil.DumpResponse(resp, true) 100 if err != nil { 101 return err 102 } 103 c.logger.Printf("\n%s\n", string(dump)) 104 } 105 data, _ := io.ReadAll(resp.Body) 106 107 if resp.StatusCode != http.StatusOK { 108 err = errors.New("not 200 http response code from api") 109 c.logger. 110 WithError(err). 111 WithField("resp.StatusCode", resp.StatusCode). 112 WithField("body", string(data)). 113 WithField("url", request.URL). 114 Error("failed to call api") 115 return err 116 } 117 118 err = json.Unmarshal(data, &target) 119 if err != nil { 120 return err 121 } 122 123 c.waiting() 124 125 return resp.Body.Close() 126 } 127 128 // GetSwapPairsContractAddresses returns swap contract addresses for alephium 129 func (c *AlephiumClient) GetSwapPairsContractAddresses(swapContractsLimit int) (SubContractResponse, error) { 130 var contractResponsePage1, contractResponsePage2 SubContractResponse 131 132 // Page 1 133 url := fmt.Sprintf("%s/contracts/%s/sub-contracts?limit=%d&page=1", BackendURL, AYINPairContractAddress, swapContractsLimit) 134 request, _ := http.NewRequest("GET", url, http.NoBody) 135 err := c.callAPI(request, &contractResponsePage1) 136 if err != nil { 137 return contractResponsePage1, err 138 } 139 140 // Page 2 141 url = fmt.Sprintf("%s/contracts/%s/sub-contracts?limit=%d&page=2", BackendURL, AYINPairContractAddress, swapContractsLimit) 142 request, _ = http.NewRequest("GET", url, http.NoBody) 143 err = c.callAPI(request, &contractResponsePage2) 144 if err != nil { 145 return contractResponsePage1, err 146 } 147 148 for _, contract := range contractResponsePage2.SubContracts { 149 contractResponsePage1.SubContracts = append(contractResponsePage1.SubContracts, contract) 150 } 151 return contractResponsePage1, nil 152 } 153 154 // GetTokenPairAddresses returns token address pair for swap contract address 155 func (c *AlephiumClient) GetTokenPairAddresses(contractAddress string) ([]string, error) { 156 group, err := groupOfAddress(contractAddress) 157 if err != nil { 158 return nil, err 159 } 160 inputData := CallContractRequest{ 161 Group: int(group), 162 Address: contractAddress, 163 MethodIndex: TokenPairMethod, 164 } 165 logger := c.logger. 166 WithField("function", "GetTokenPairAddresses"). 167 WithField("contractAddress", contractAddress) 168 169 jsonData, err := json.Marshal(inputData) 170 171 if err != nil { 172 logger.Fatalf("failed to marshal input data: %v", err) 173 return nil, err 174 } 175 url := fmt.Sprintf("%s/contracts/call-contract", NodeURL) 176 req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) 177 if err != nil { 178 logger.Fatalf("failed to create request: %v", err) 179 return nil, err 180 } 181 var response CallContractResult 182 err = c.callAPI(req, &response) 183 184 if err != nil { 185 logger.WithError(err).Error("failed to callApi") 186 return nil, err 187 } 188 if response.Error != nil { 189 err = errors.New(*response.Error) 190 logger. 191 WithError(err). 192 WithField("jsonData", string(jsonData)). 193 WithField("contractAddress", contractAddress). 194 Error("failed to get token pair") 195 return nil, err 196 } 197 198 address1, err := AddressFromTokenId(response.Returns[0].Value) 199 if err != nil { 200 logger.WithError(err).Error("failed to calculate address1") 201 return nil, err 202 } 203 address2, err := AddressFromTokenId(response.Returns[1].Value) 204 if err != nil { 205 logger.WithError(err).Error("failed to calculate address2") 206 return nil, err 207 } 208 209 output := []string{address1, address2} 210 return output, nil 211 } 212 213 // GetTokenInfoForContractDecoded returns alephium token metainfo, decoded to dia.Asset struct 214 func (c *AlephiumClient) GetTokenInfoForContractDecoded(contractAddress, blockchain string) (*dia.Asset, error) { 215 inputData := make([]CallContractRequest, 0) 216 logger := c.logger.WithField("function", "GetTokenInfoForContract") 217 218 if contractAddress == ALPHNativeToken.Address { 219 return &ALPHNativeToken, nil 220 } 221 for i := 0; i < 3; i++ { 222 group, err := groupOfAddress(contractAddress) 223 if err != nil { 224 return nil, err 225 } 226 row := CallContractRequest{ 227 Group: int(group), 228 Address: contractAddress, 229 MethodIndex: i, 230 } 231 inputData = append(inputData, row) 232 } 233 234 calls := Calls{Calls: inputData} 235 jsonData, err := json.Marshal(calls) 236 237 if err != nil { 238 logger.Fatalf("failed to marshal input data: %v", err) 239 return nil, err 240 } 241 url := fmt.Sprintf("%s/contracts/multicall-contract", NodeURL) 242 req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) 243 244 if err != nil { 245 logger.Fatalf("failed to create request: %v", err) 246 return nil, err 247 } 248 249 var response MulticallContractResponse 250 err = c.callAPI(req, &response) 251 252 if err != nil { 253 logger.WithError(err).Error("failed to callApi") 254 return nil, err 255 } 256 output := OutputResult{ 257 Address: contractAddress, 258 Results: []OutputField{}, 259 } 260 for _, row := range response.Results { 261 if row.Error != nil { 262 err = errors.New(*row.Error) 263 logger. 264 WithError(err). 265 WithField("jsonData", string(jsonData)). 266 WithField("contractAddress", contractAddress). 267 Error("failed to get token info") 268 return nil, err 269 } 270 result := OutputField{ 271 ResponseResult: row.Type, 272 Field: row.Returns[0], 273 } 274 output.Results = append(output.Results, result) 275 } 276 asset, err := c.decodeMulticallRequestToAssets(contractAddress, blockchain, &output) 277 278 return &asset, err 279 } 280 281 // GetCurrentHeight returns the current height (block number) in Alephium network 282 func (c *AlephiumClient) GetCurrentHeight() (int, error) { 283 logger := c.logger.WithField("function", "GetLatestBlockHash") 284 285 url := fmt.Sprintf("%s/blockflow/chain-info?fromGroup=0&toGroup=0", NodeURL) 286 request, _ := http.NewRequest("GET", url, http.NoBody) 287 288 var response ChainInfoResponse 289 err := c.callAPI(request, &response) 290 291 if err != nil { 292 logger.WithError(err).Error("failed to callApi") 293 return 0, err 294 } 295 296 return response.CurrentHeight, nil 297 } 298 299 // GetBlockHashes returns all block hashes at a given height from REST API 300 func (c *AlephiumClient) GetBlockHashes(height int) ([]string, error) { 301 logger := c.logger.WithField("function", "GetBlockHashes") 302 303 url := fmt.Sprintf("%s/blockflow/hashes?fromGroup=0&toGroup=0&height=%d", NodeURL, height) 304 request, _ := http.NewRequest("GET", url, http.NoBody) 305 306 var response BlockHashesResponse 307 err := c.callAPI(request, &response) 308 309 if err != nil { 310 logger.WithError(err).Error("failed to callApi") 311 return nil, err 312 } 313 314 return response.Headers, nil 315 } 316 317 // GetContractEvents returns events included in a specific block from REST API 318 func (c *AlephiumClient) GetBlockEvents(blockHash string) ([]ContractEvent, error) { 319 logger := c.logger.WithField("function", "GetEvents") 320 321 url := fmt.Sprintf("%s/events/block-hash/%s?group=0", NodeURL, blockHash) 322 request, _ := http.NewRequest("GET", url, http.NoBody) 323 324 var response BlockEventsResponse 325 err := c.callAPI(request, &response) 326 327 if err != nil { 328 logger.WithError(err).Error("failed to callApi") 329 return nil, err 330 } 331 332 return response.Events, nil 333 } 334 335 // GetSwapContractEvents returns swap event transaction details by transaction hash 336 func (c *AlephiumClient) GetTransactionDetails(txnHash string) (TransactionDetailsResponse, error) { 337 logger := c.logger.WithField("function", "GetTransactionDetails") 338 339 // 'https://backend.mainnet.alephium.org/transactions/b9744b60b94a342c488dbf827747e5ac8ff8adabce48a72167f0ce3dfbe8291a 340 url := fmt.Sprintf("%s/transactions/%s", BackendURL, txnHash) 341 request, _ := http.NewRequest("GET", url, http.NoBody) 342 343 var transactionDetailsResponse TransactionDetailsResponse 344 err := c.callAPI(request, &transactionDetailsResponse) 345 346 if err != nil { 347 logger.WithError(err).Error("failed to callApi") 348 return transactionDetailsResponse, err 349 } 350 return transactionDetailsResponse, nil 351 } 352 353 func (s *AlephiumClient) FilterEvents(allEvents []ContractEvent, filter int) []ContractEvent { 354 events := make([]ContractEvent, 0, len(allEvents)) 355 for _, event := range allEvents { 356 if event.EventIndex == filter { 357 events = append(events, event) 358 } 359 } 360 return events 361 } 362 363 func (c *AlephiumClient) GetContractState(address string) (ContractStateResponse, error) { 364 logger := c.logger.WithField("function", "GetContractState") 365 // https://node.mainnet.alephium.org/contracts/22po9GJCMoLcYgXL3Znv2cSXcMnKmfm36MrBdqB4rSoKV/state 366 url := fmt.Sprintf("%s/contracts/%s/state", NodeURL, address) 367 request, _ := http.NewRequest("GET", url, http.NoBody) 368 369 var contractStateResponse ContractStateResponse 370 err := c.callAPI(request, &contractStateResponse) 371 if err != nil { 372 logger.WithError(err).Error("failed to callApi") 373 return contractStateResponse, err 374 } 375 return contractStateResponse, nil 376 } 377 378 func (s *AlephiumClient) decodeMulticallRequestToAssets(contractAddress, blockchain string, resp *OutputResult) (dia.Asset, error) { 379 asset := dia.Asset{} 380 381 symbol, err := DecodeHex(resp.Results[SymbolMethod].Value) 382 if err != nil { 383 s.logger. 384 WithField("row", resp). 385 WithError(err). 386 Error("failed to decode symbol") 387 return asset, err 388 } 389 asset.Symbol = symbol 390 391 name, err := DecodeHex(resp.Results[NameMethod].Value) 392 if err != nil { 393 s.logger. 394 WithField("row", resp). 395 WithError(err). 396 Error("failed to decode name") 397 return asset, err 398 } 399 asset.Name = name 400 401 decimals, err := strconv.ParseUint(resp.Results[DecimalsMethod].Value, 10, 32) 402 if err != nil { 403 s.logger. 404 WithField("row", resp). 405 WithError(err). 406 Error("failed to decode decimals") 407 return asset, err 408 } 409 asset.Decimals = uint8(decimals) 410 asset.Address = contractAddress 411 asset.Blockchain = blockchain 412 413 return asset, nil 414 } 415 416 func (c *AlephiumClient) waiting() { 417 time.Sleep(c.sleepBetweenCalls) 418 }