decred.org/dcrdex@v1.0.5/client/core/exchangeratefetcher.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package core 5 6 import ( 7 "context" 8 "fmt" 9 "sync" 10 "time" 11 12 "decred.org/dcrdex/dex" 13 "decred.org/dcrdex/dex/dexnet" 14 "decred.org/dcrdex/dex/fiatrates" 15 ) 16 17 const ( 18 // DefaultFiatCurrency is the currency for displaying assets fiat value. 19 DefaultFiatCurrency = "USD" 20 // fiatRateRequestInterval is the amount of time between calls to the exchange API. 21 fiatRateRequestInterval = 12 * time.Minute 22 // fiatRateDataExpiry : Any data older than fiatRateDataExpiry will be discarded. 23 fiatRateDataExpiry = 60 * time.Minute 24 fiatRequestTimeout = time.Second * 5 25 26 // Tokens. Used to identify fiat rate source, source name must not contain a 27 // comma. 28 messari = "Messari" 29 coinpaprika = "Coinpaprika" 30 dcrdataDotOrg = "dcrdata" 31 ) 32 33 var ( 34 dcrDataURL = "https://explorer.dcrdata.org/api/exchangerate" 35 // The best info I can find on Messari says 36 // Without an API key requests are rate limited to 20 requests per minute 37 // and 1000 requests per day. 38 // For a 39 // fiatRateRequestInterval of 12 minutes, to hit 20 requests per minute, we 40 // would need to have 20 * 12 = 480 assets. To hit 1000 requests per day, 41 // we would need 12 * 60 / (86,400 / 1000) = 8.33 assets. Very likely. So 42 // we're in a similar position to coinpaprika here too. 43 messariURL = "https://data.messari.io/api/v1/assets/%s/metrics/market-data" 44 btcBipID, _ = dex.BipSymbolID("btc") 45 dcrBipID, _ = dex.BipSymbolID("dcr") 46 ) 47 48 // fiatRateFetchers is the list of all supported fiat rate fetchers. 49 var fiatRateFetchers = map[string]rateFetcher{ 50 coinpaprika: FetchCoinpaprikaRates, 51 dcrdataDotOrg: FetchDcrdataRates, 52 messari: FetchMessariRates, 53 } 54 55 // fiatRateInfo holds the fiat rate and the last update time for an 56 // asset. 57 type fiatRateInfo struct { 58 rate float64 59 lastUpdate time.Time 60 } 61 62 // rateFetcher can fetch fiat rates for assets from an API. 63 type rateFetcher func(context context.Context, logger dex.Logger, assets map[uint32]*SupportedAsset) map[uint32]float64 64 65 type commonRateSource struct { 66 fetchRates rateFetcher 67 68 mtx sync.RWMutex 69 fiatRates map[uint32]*fiatRateInfo 70 } 71 72 // isExpired checks the last update time for all fiat rates against the 73 // provided expiryTime. This only returns true if all rates are expired. 74 func (source *commonRateSource) isExpired(expiryTime time.Duration) bool { 75 now := time.Now() 76 77 source.mtx.RLock() 78 defer source.mtx.RUnlock() 79 if len(source.fiatRates) == 0 { 80 return false 81 } 82 for _, rateInfo := range source.fiatRates { 83 if now.Sub(rateInfo.lastUpdate) < expiryTime { 84 return false // one not expired is enough 85 } 86 } 87 return true 88 } 89 90 // assetRate returns the fiat rate information for the assetID specified. The 91 // fiatRateInfo returned should not be modified by the caller. 92 func (source *commonRateSource) assetRate(assetID uint32) *fiatRateInfo { 93 source.mtx.RLock() 94 defer source.mtx.RUnlock() 95 return source.fiatRates[assetID] 96 } 97 98 // refreshRates updates the last update time and the rate information for assets. 99 func (source *commonRateSource) refreshRates(ctx context.Context, logger dex.Logger, assets map[uint32]*SupportedAsset) { 100 fiatRates := source.fetchRates(ctx, logger, assets) 101 now := time.Now() 102 source.mtx.Lock() 103 defer source.mtx.Unlock() 104 for assetID, fiatRate := range fiatRates { 105 if fiatRate <= 0 { 106 continue 107 } 108 source.fiatRates[assetID] = &fiatRateInfo{ 109 rate: fiatRate, 110 lastUpdate: now, 111 } 112 } 113 } 114 115 // Used to initialize a fiat rate source. 116 func newCommonRateSource(fetcher rateFetcher) *commonRateSource { 117 return &commonRateSource{ 118 fetchRates: fetcher, 119 fiatRates: make(map[uint32]*fiatRateInfo), 120 } 121 } 122 123 // FetchCoinpaprikaRates retrieves and parses fiat rate data from the 124 // Coinpaprika API. See https://api.coinpaprika.com/#operation/getTickersById 125 // for sample request and response information. 126 func FetchCoinpaprikaRates(ctx context.Context, log dex.Logger, assets map[uint32]*SupportedAsset) map[uint32]float64 { 127 coinpapAssets := make([]*fiatrates.CoinpaprikaAsset, 0, len(assets) /* too small cuz tokens*/) 128 for assetID, a := range assets { 129 coinpapAssets = append(coinpapAssets, &fiatrates.CoinpaprikaAsset{ 130 AssetID: assetID, 131 Name: a.Name, 132 Symbol: a.Symbol, 133 }) 134 } 135 return fiatrates.FetchCoinpaprikaRates(ctx, coinpapAssets, log) 136 } 137 138 // FetchDcrdataRates retrieves and parses fiat rate data from dcrdata 139 // exchange rate API. 140 func FetchDcrdataRates(ctx context.Context, log dex.Logger, assets map[uint32]*SupportedAsset) map[uint32]float64 { 141 assetBTC := assets[btcBipID] 142 assetDCR := assets[dcrBipID] 143 noBTCAsset := assetBTC == nil || assetBTC.Wallet == nil 144 noDCRAsset := assetDCR == nil || assetDCR.Wallet == nil 145 if noBTCAsset && noDCRAsset { 146 return nil 147 } 148 149 fiatRates := make(map[uint32]float64) 150 res := new(struct { 151 DcrPrice float64 `json:"dcrPrice"` 152 BtcPrice float64 `json:"btcPrice"` 153 }) 154 155 if err := getRates(ctx, dcrDataURL, res); err != nil { 156 log.Error(err) 157 return nil 158 } 159 160 if !noBTCAsset { 161 fiatRates[btcBipID] = res.BtcPrice 162 } 163 if !noDCRAsset { 164 fiatRates[dcrBipID] = res.DcrPrice 165 } 166 167 return fiatRates 168 } 169 170 // FetchMessariRates retrieves and parses fiat rate data from the Messari API. 171 // See https://messari.io/api/docs#operation/Get%20Asset%20Market%20Data for 172 // sample request and response information. 173 func FetchMessariRates(ctx context.Context, log dex.Logger, assets map[uint32]*SupportedAsset) map[uint32]float64 { 174 fiatRates := make(map[uint32]float64) 175 fetchRate := func(sa *SupportedAsset) { 176 assetID := sa.ID 177 if sa.Wallet == nil { 178 // we don't want to fetch rate for assets with no wallet. 179 return 180 } 181 182 res := new(struct { 183 Data struct { 184 MarketData struct { 185 Price float64 `json:"price_usd"` 186 } `json:"market_data"` 187 } `json:"data"` 188 }) 189 190 slug := dex.TokenSymbol(sa.Symbol) 191 reqStr := fmt.Sprintf(messariURL, slug) 192 193 ctx, cancel := context.WithTimeout(ctx, fiatRequestTimeout) 194 defer cancel() 195 196 if err := getRates(ctx, reqStr, res); err != nil { 197 log.Errorf("Error getting fiat exchange rates from messari: %v", err) 198 return 199 } 200 201 fiatRates[assetID] = res.Data.MarketData.Price 202 } 203 204 for _, sa := range assets { 205 fetchRate(sa) 206 } 207 return fiatRates 208 } 209 210 func getRates(ctx context.Context, uri string, thing any) error { 211 return dexnet.Get(ctx, uri, thing, dexnet.WithSizeLimit(1<<22)) 212 }