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  }