github.com/InjectiveLabs/sdk-go@v1.53.0/client/chain/markets_assistant.go (about)

     1  package chain
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"path"
     7  	"runtime"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/InjectiveLabs/sdk-go/client/core"
    12  	"github.com/InjectiveLabs/sdk-go/client/exchange"
    13  	derivativeExchangePB "github.com/InjectiveLabs/sdk-go/exchange/derivative_exchange_rpc/pb"
    14  	spotExchangePB "github.com/InjectiveLabs/sdk-go/exchange/spot_exchange_rpc/pb"
    15  	"github.com/cosmos/cosmos-sdk/types/query"
    16  	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
    17  	"github.com/shopspring/decimal"
    18  	"gopkg.in/ini.v1"
    19  )
    20  
    21  var legacyMarketAssistantLazyInitialization sync.Once
    22  var legacyMarketAssistant MarketsAssistant
    23  
    24  type TokenMetadata interface {
    25  	GetName() string
    26  	GetAddress() string
    27  	GetSymbol() string
    28  	GetLogo() string
    29  	GetDecimals() int32
    30  	GetUpdatedAt() int64
    31  }
    32  
    33  type MarketsAssistant struct {
    34  	tokensBySymbol    map[string]core.Token
    35  	tokensByDenom     map[string]core.Token
    36  	spotMarkets       map[string]core.SpotMarket
    37  	derivativeMarkets map[string]core.DerivativeMarket
    38  }
    39  
    40  func newMarketsAssistant() MarketsAssistant {
    41  	return MarketsAssistant{
    42  		tokensBySymbol:    make(map[string]core.Token),
    43  		tokensByDenom:     make(map[string]core.Token),
    44  		spotMarkets:       make(map[string]core.SpotMarket),
    45  		derivativeMarkets: make(map[string]core.DerivativeMarket),
    46  	}
    47  }
    48  
    49  // Deprecated: use NewMarketsAssistantInitializedFromChain instead
    50  func NewMarketsAssistant(networkName string) (MarketsAssistant, error) {
    51  
    52  	legacyMarketAssistantLazyInitialization.Do(func() {
    53  		assistant := newMarketsAssistant()
    54  		fileName := getFileAbsPath(fmt.Sprintf("../metadata/assets/%s.ini", networkName))
    55  		metadataFile, err := ini.Load(fileName)
    56  
    57  		if err == nil {
    58  			for _, section := range metadataFile.Sections() {
    59  				sectionName := section.Name()
    60  				if strings.HasPrefix(sectionName, "0x") {
    61  					description := section.Key("description").Value()
    62  
    63  					decimals, _ := section.Key("quote").Int()
    64  					quoteToken := core.Token{
    65  						Name:     "",
    66  						Symbol:   "",
    67  						Denom:    "",
    68  						Address:  "",
    69  						Decimals: int32(decimals),
    70  						Logo:     "",
    71  						Updated:  -1,
    72  					}
    73  
    74  					minPriceTickSize := decimal.RequireFromString(section.Key("min_price_tick_size").String())
    75  					minQuantityTickSize := decimal.RequireFromString(section.Key("min_quantity_tick_size").String())
    76  					minNotional := decimal.Zero
    77  					if section.HasKey("min_notional") {
    78  						minNotional = decimal.RequireFromString(section.Key("min_notional").String())
    79  					}
    80  
    81  					if strings.Contains(description, "Spot") {
    82  						baseDecimals, _ := section.Key("quote").Int()
    83  						baseToken := core.Token{
    84  							Name:     "",
    85  							Symbol:   "",
    86  							Denom:    "",
    87  							Address:  "",
    88  							Decimals: int32(baseDecimals),
    89  							Logo:     "",
    90  							Updated:  -1,
    91  						}
    92  
    93  						market := core.SpotMarket{
    94  							Id:                  sectionName,
    95  							Status:              "",
    96  							Ticker:              description,
    97  							BaseToken:           baseToken,
    98  							QuoteToken:          quoteToken,
    99  							MakerFeeRate:        decimal.NewFromInt32(0),
   100  							TakerFeeRate:        decimal.NewFromInt32(0),
   101  							ServiceProviderFee:  decimal.NewFromInt32(0),
   102  							MinPriceTickSize:    minPriceTickSize,
   103  							MinQuantityTickSize: minQuantityTickSize,
   104  							MinNotional:         minNotional,
   105  						}
   106  
   107  						assistant.spotMarkets[market.Id] = market
   108  					} else {
   109  						market := core.DerivativeMarket{
   110  							Id:                     sectionName,
   111  							Status:                 "",
   112  							Ticker:                 description,
   113  							OracleBase:             "",
   114  							OracleQuote:            "",
   115  							OracleType:             "",
   116  							OracleScaleFactor:      1,
   117  							InitialMarginRatio:     decimal.NewFromInt32(0),
   118  							MaintenanceMarginRatio: decimal.NewFromInt32(0),
   119  							QuoteToken:             quoteToken,
   120  							MakerFeeRate:           decimal.NewFromInt32(0),
   121  							TakerFeeRate:           decimal.NewFromInt32(0),
   122  							ServiceProviderFee:     decimal.NewFromInt32(0),
   123  							MinPriceTickSize:       minPriceTickSize,
   124  							MinQuantityTickSize:    minQuantityTickSize,
   125  							MinNotional:            minNotional,
   126  						}
   127  
   128  						assistant.derivativeMarkets[market.Id] = market
   129  					}
   130  				} else if sectionName != "DEFAULT" {
   131  					tokenDecimals, _ := section.Key("decimals").Int()
   132  					newToken := core.Token{
   133  						Name:     sectionName,
   134  						Symbol:   sectionName,
   135  						Denom:    section.Key("peggy_denom").String(),
   136  						Address:  "",
   137  						Decimals: int32(tokenDecimals),
   138  						Logo:     "",
   139  						Updated:  -1,
   140  					}
   141  
   142  					assistant.tokensByDenom[newToken.Denom] = newToken
   143  					assistant.tokensBySymbol[newToken.Symbol] = newToken
   144  				}
   145  			}
   146  		}
   147  
   148  		legacyMarketAssistant = assistant
   149  	})
   150  
   151  	return legacyMarketAssistant, nil
   152  }
   153  
   154  func NewMarketsAssistantInitializedFromChain(ctx context.Context, exchangeClient exchange.ExchangeClient) (MarketsAssistant, error) {
   155  	assistant := newMarketsAssistant()
   156  
   157  	officialTokens, err := core.LoadTokens(exchangeClient.GetNetwork().OfficialTokensListURL)
   158  	if err == nil {
   159  		for i := range officialTokens {
   160  			tokenMetadata := officialTokens[i]
   161  			if tokenMetadata.Denom != "" {
   162  				// add tokens to the assistant ensuring all of them get assigned a unique symbol
   163  				tokenRepresentation(tokenMetadata.GetSymbol(), tokenMetadata, tokenMetadata.Denom, &assistant)
   164  			}
   165  		}
   166  	}
   167  
   168  	spotMarketsRequest := spotExchangePB.MarketsRequest{
   169  		MarketStatus: "active",
   170  	}
   171  	spotMarkets, err := exchangeClient.GetSpotMarkets(ctx, &spotMarketsRequest)
   172  
   173  	if err != nil {
   174  		return assistant, err
   175  	}
   176  
   177  	for _, marketInfo := range spotMarkets.GetMarkets() {
   178  		if marketInfo.GetBaseTokenMeta().GetSymbol() == "" || marketInfo.GetQuoteTokenMeta().GetSymbol() == "" {
   179  			continue
   180  		}
   181  
   182  		var baseTokenSymbol, quoteTokenSymbol string
   183  		if strings.Contains(marketInfo.GetTicker(), "/") {
   184  			baseAndQuote := strings.Split(marketInfo.GetTicker(), "/")
   185  			baseTokenSymbol, quoteTokenSymbol = baseAndQuote[0], baseAndQuote[1]
   186  		} else {
   187  			baseTokenSymbol = marketInfo.GetBaseTokenMeta().GetSymbol()
   188  			quoteTokenSymbol = marketInfo.GetQuoteTokenMeta().GetSymbol()
   189  		}
   190  
   191  		baseToken := tokenRepresentation(baseTokenSymbol, marketInfo.GetBaseTokenMeta(), marketInfo.GetBaseDenom(), &assistant)
   192  		quoteToken := tokenRepresentation(quoteTokenSymbol, marketInfo.GetQuoteTokenMeta(), marketInfo.GetQuoteDenom(), &assistant)
   193  
   194  		makerFeeRate := decimal.RequireFromString(marketInfo.GetMakerFeeRate())
   195  		takerFeeRate := decimal.RequireFromString(marketInfo.GetTakerFeeRate())
   196  		serviceProviderFee := decimal.RequireFromString(marketInfo.GetServiceProviderFee())
   197  		minPriceTickSize := decimal.RequireFromString(marketInfo.GetMinPriceTickSize())
   198  		minQuantityTickSize := decimal.RequireFromString(marketInfo.GetMinQuantityTickSize())
   199  		minNotional := decimal.RequireFromString(marketInfo.GetMinNotional())
   200  
   201  		market := core.SpotMarket{
   202  			Id:                  marketInfo.GetMarketId(),
   203  			Status:              marketInfo.GetMarketStatus(),
   204  			Ticker:              marketInfo.GetTicker(),
   205  			BaseToken:           baseToken,
   206  			QuoteToken:          quoteToken,
   207  			MakerFeeRate:        makerFeeRate,
   208  			TakerFeeRate:        takerFeeRate,
   209  			ServiceProviderFee:  serviceProviderFee,
   210  			MinPriceTickSize:    minPriceTickSize,
   211  			MinQuantityTickSize: minQuantityTickSize,
   212  			MinNotional:         minNotional,
   213  		}
   214  
   215  		assistant.spotMarkets[market.Id] = market
   216  	}
   217  
   218  	derivativeMarketsRequest := derivativeExchangePB.MarketsRequest{
   219  		MarketStatus: "active",
   220  	}
   221  	derivativeMarkets, err := exchangeClient.GetDerivativeMarkets(ctx, &derivativeMarketsRequest)
   222  
   223  	if err != nil {
   224  		return assistant, err
   225  	}
   226  
   227  	for _, marketInfo := range derivativeMarkets.GetMarkets() {
   228  		if marketInfo.GetQuoteTokenMeta().GetSymbol() == "" {
   229  			continue
   230  		}
   231  
   232  		quoteTokenSymbol := marketInfo.GetQuoteTokenMeta().GetSymbol()
   233  
   234  		quoteToken := tokenRepresentation(quoteTokenSymbol, marketInfo.GetQuoteTokenMeta(), marketInfo.GetQuoteDenom(), &assistant)
   235  
   236  		initialMarginRatio := decimal.RequireFromString(marketInfo.GetInitialMarginRatio())
   237  		maintenanceMarginRatio := decimal.RequireFromString(marketInfo.GetMaintenanceMarginRatio())
   238  		makerFeeRate := decimal.RequireFromString(marketInfo.GetMakerFeeRate())
   239  		takerFeeRate := decimal.RequireFromString(marketInfo.GetTakerFeeRate())
   240  		serviceProviderFee := decimal.RequireFromString(marketInfo.GetServiceProviderFee())
   241  		minPriceTickSize := decimal.RequireFromString(marketInfo.GetMinPriceTickSize())
   242  		minQuantityTickSize := decimal.RequireFromString(marketInfo.GetMinQuantityTickSize())
   243  		minNotional := decimal.RequireFromString(marketInfo.GetMinNotional())
   244  
   245  		market := core.DerivativeMarket{
   246  			Id:                     marketInfo.GetMarketId(),
   247  			Status:                 marketInfo.GetMarketStatus(),
   248  			Ticker:                 marketInfo.GetTicker(),
   249  			OracleBase:             marketInfo.GetOracleBase(),
   250  			OracleQuote:            marketInfo.GetOracleQuote(),
   251  			OracleType:             marketInfo.GetOracleType(),
   252  			OracleScaleFactor:      marketInfo.GetOracleScaleFactor(),
   253  			InitialMarginRatio:     initialMarginRatio,
   254  			MaintenanceMarginRatio: maintenanceMarginRatio,
   255  			QuoteToken:             quoteToken,
   256  			MakerFeeRate:           makerFeeRate,
   257  			TakerFeeRate:           takerFeeRate,
   258  			ServiceProviderFee:     serviceProviderFee,
   259  			MinPriceTickSize:       minPriceTickSize,
   260  			MinQuantityTickSize:    minQuantityTickSize,
   261  			MinNotional:            minNotional,
   262  		}
   263  
   264  		assistant.derivativeMarkets[market.Id] = market
   265  	}
   266  
   267  	return assistant, nil
   268  }
   269  
   270  func NewMarketsAssistantWithAllTokens(ctx context.Context, exchangeClient exchange.ExchangeClient, chainClient ChainClient) (MarketsAssistant, error) {
   271  	assistant, err := NewMarketsAssistantInitializedFromChain(ctx, exchangeClient)
   272  	if err != nil {
   273  		return assistant, err
   274  	}
   275  
   276  	assistant.initializeTokensFromChainDenoms(ctx, chainClient)
   277  
   278  	return assistant, nil
   279  }
   280  
   281  func uniqueSymbol(symbol, denom, tokenMetaSymbol, tokenMetaName string, assistant MarketsAssistant) string {
   282  	uniqueSymbol := denom
   283  	_, isSymbolPresent := assistant.tokensBySymbol[symbol]
   284  	if isSymbolPresent {
   285  		_, isSymbolPresent = assistant.tokensBySymbol[tokenMetaSymbol]
   286  		if isSymbolPresent {
   287  			_, isSymbolPresent = assistant.tokensBySymbol[tokenMetaName]
   288  			if !isSymbolPresent {
   289  				uniqueSymbol = tokenMetaName
   290  			}
   291  		} else {
   292  			uniqueSymbol = tokenMetaSymbol
   293  		}
   294  	} else {
   295  		uniqueSymbol = symbol
   296  	}
   297  
   298  	return uniqueSymbol
   299  }
   300  
   301  func tokenRepresentation(symbol string, tokenMeta TokenMetadata, denom string, assistant *MarketsAssistant) core.Token {
   302  	_, isPresent := assistant.tokensByDenom[denom]
   303  
   304  	if !isPresent {
   305  		uniqueSymbol := uniqueSymbol(symbol, denom, tokenMeta.GetSymbol(), tokenMeta.GetName(), *assistant)
   306  
   307  		newToken := core.Token{
   308  			Name:     tokenMeta.GetName(),
   309  			Symbol:   symbol,
   310  			Denom:    denom,
   311  			Address:  tokenMeta.GetAddress(),
   312  			Decimals: tokenMeta.GetDecimals(),
   313  			Logo:     tokenMeta.GetLogo(),
   314  			Updated:  tokenMeta.GetUpdatedAt(),
   315  		}
   316  
   317  		assistant.tokensByDenom[denom] = newToken
   318  		assistant.tokensBySymbol[uniqueSymbol] = newToken
   319  	}
   320  
   321  	return assistant.tokensByDenom[denom]
   322  }
   323  
   324  func getFileAbsPath(relativePath string) string {
   325  	_, filename, _, _ := runtime.Caller(0)
   326  	return path.Join(path.Dir(filename), relativePath)
   327  }
   328  
   329  func (assistant MarketsAssistant) AllTokens() map[string]core.Token {
   330  	return assistant.tokensBySymbol
   331  }
   332  
   333  func (assistant MarketsAssistant) AllSpotMarkets() map[string]core.SpotMarket {
   334  	return assistant.spotMarkets
   335  }
   336  
   337  func (assistant MarketsAssistant) AllDerivativeMarkets() map[string]core.DerivativeMarket {
   338  	return assistant.derivativeMarkets
   339  }
   340  
   341  func (assistant MarketsAssistant) initializeTokensFromChainDenoms(ctx context.Context, chainClient ChainClient) {
   342  	var denomsMetadata []banktypes.Metadata
   343  	var nextKey []byte
   344  
   345  	for readNextPage := true; readNextPage; readNextPage = len(nextKey) > 0 {
   346  		pagination := query.PageRequest{Key: nextKey}
   347  		result, err := chainClient.GetDenomsMetadata(ctx, &pagination)
   348  
   349  		if err != nil {
   350  			panic(err)
   351  		}
   352  
   353  		denomsMetadata = append(denomsMetadata, result.GetMetadatas()...)
   354  	}
   355  
   356  	for i := range denomsMetadata {
   357  		denomMetadata := denomsMetadata[i]
   358  		symbol := denomMetadata.GetSymbol()
   359  		denom := denomMetadata.GetBase()
   360  
   361  		_, isDenomPresent := assistant.tokensByDenom[denom]
   362  
   363  		if symbol != "" && denom != "" && !isDenomPresent {
   364  			name := denomMetadata.GetName()
   365  			if name == "" {
   366  				name = symbol
   367  			}
   368  
   369  			var decimals int32 = -1
   370  			for _, denomUnit := range denomMetadata.GetDenomUnits() {
   371  				exponent := int32(denomUnit.GetExponent())
   372  				if exponent > decimals {
   373  					decimals = exponent
   374  				}
   375  			}
   376  
   377  			uniqueSymbol := uniqueSymbol(symbol, denom, symbol, name, assistant)
   378  
   379  			newToken := core.Token{
   380  				Name:     name,
   381  				Symbol:   symbol,
   382  				Denom:    denom,
   383  				Address:  "",
   384  				Decimals: decimals,
   385  				Logo:     denomMetadata.GetURI(),
   386  				Updated:  -1,
   387  			}
   388  
   389  			assistant.tokensByDenom[denom] = newToken
   390  			assistant.tokensBySymbol[uniqueSymbol] = newToken
   391  		}
   392  	}
   393  }