github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/session/pingpong/pricing.go (about) 1 /* 2 * Copyright (C) 2021 The "MysteriumNetwork/node" Authors. 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 package pingpong 19 20 import ( 21 "errors" 22 "strings" 23 "sync" 24 "time" 25 26 "github.com/mysteriumnetwork/node/config" 27 nodevent "github.com/mysteriumnetwork/node/core/node/event" 28 "github.com/mysteriumnetwork/node/eventbus" 29 "github.com/mysteriumnetwork/node/market" 30 "github.com/mysteriumnetwork/payments/crypto" 31 "github.com/rs/zerolog/log" 32 ) 33 34 var defaultPrice = market.Price{ 35 PricePerHour: crypto.FloatToBigMyst(0.00006), 36 PricePerGiB: crypto.FloatToBigMyst(0.1), 37 } 38 39 type discoAPI interface { 40 GetPricing() (market.LatestPrices, error) 41 } 42 43 // Pricer fetches and caches prices from discovery api. 44 type Pricer struct { 45 discoAPI discoAPI 46 lastLoad market.LatestPrices 47 mut sync.Mutex 48 } 49 50 // NewPricer creates a new instance of pricer. 51 func NewPricer(discoAPI discoAPI) *Pricer { 52 return &Pricer{ 53 lastLoad: market.LatestPrices{ 54 PerCountry: make(map[string]*market.PriceHistory), 55 Defaults: &market.PriceHistory{ 56 Current: &market.PriceByType{ 57 Residential: &market.PriceByServiceType{ 58 Wireguard: &market.Price{ 59 PricePerHour: defaultPrice.PricePerHour, 60 PricePerGiB: defaultPrice.PricePerGiB, 61 }, 62 Scraping: &market.Price{ 63 PricePerHour: defaultPrice.PricePerHour, 64 PricePerGiB: defaultPrice.PricePerGiB, 65 }, 66 DataTransfer: &market.Price{ 67 PricePerHour: defaultPrice.PricePerHour, 68 PricePerGiB: defaultPrice.PricePerGiB, 69 }, 70 DVPN: &market.Price{ 71 PricePerHour: defaultPrice.PricePerHour, 72 PricePerGiB: defaultPrice.PricePerGiB, 73 }, 74 }, 75 Other: &market.PriceByServiceType{ 76 Wireguard: &market.Price{ 77 PricePerHour: defaultPrice.PricePerHour, 78 PricePerGiB: defaultPrice.PricePerGiB, 79 }, 80 Scraping: &market.Price{ 81 PricePerHour: defaultPrice.PricePerHour, 82 PricePerGiB: defaultPrice.PricePerGiB, 83 }, 84 DataTransfer: &market.Price{ 85 PricePerHour: defaultPrice.PricePerHour, 86 PricePerGiB: defaultPrice.PricePerGiB, 87 }, 88 DVPN: &market.Price{ 89 PricePerHour: defaultPrice.PricePerHour, 90 PricePerGiB: defaultPrice.PricePerGiB, 91 }, 92 }, 93 }, 94 }, 95 CurrentValidUntil: time.Now().Truncate(0).UTC().Add(-time.Hour * 1000), 96 }, 97 discoAPI: discoAPI, 98 } 99 } 100 101 // GetCurrentPrice gets the current price from cache if possible, fetches it otherwise. 102 func (p *Pricer) GetCurrentPrice(nodeType string, country string, serviceType string) (market.Price, error) { 103 pricing := p.getPricing() 104 105 price := p.getCurrentByType(pricing, nodeType, country, serviceType) 106 if price == nil { 107 return market.Price{}, errors.New("no data available") 108 } 109 return *price, nil 110 } 111 112 func (p *Pricer) getPriceForCountry(pricing market.LatestPrices, country string) *market.PriceHistory { 113 v, ok := pricing.PerCountry[strings.ToUpper(country)] 114 if ok { 115 return v 116 } 117 return pricing.Defaults 118 } 119 120 func (p *Pricer) getCurrentByType(pricing market.LatestPrices, nodeType string, country string, serviceType string) *market.Price { 121 base := p.getPriceForCountry(pricing, country) 122 switch strings.ToLower(nodeType) { 123 case "residential", "cellular": 124 return p.getCurrentByServiceType(base.Current.Residential, serviceType) 125 default: 126 return p.getCurrentByServiceType(base.Current.Other, serviceType) 127 } 128 } 129 130 func (p *Pricer) getCurrentByServiceType(pricingByServiceType *market.PriceByServiceType, serviceType string) *market.Price { 131 switch strings.ToLower(serviceType) { 132 case "wireguard": 133 return pricingByServiceType.Wireguard 134 case "scraping": 135 return pricingByServiceType.Scraping 136 case "dvpn": 137 return pricingByServiceType.DVPN 138 default: 139 return pricingByServiceType.DataTransfer 140 } 141 } 142 143 func (p *Pricer) getPreviousByType(pricing market.LatestPrices, nodeType string, country string, serviceType string) *market.Price { 144 base := p.getPriceForCountry(pricing, country) 145 switch strings.ToLower(nodeType) { 146 case "residential", "cellular": 147 return p.getCurrentByServiceType(base.Previous.Residential, serviceType) 148 default: 149 return p.getCurrentByServiceType(base.Previous.Other, serviceType) 150 } 151 } 152 153 // IsPriceValid checks if the given price is valid or not. 154 func (p *Pricer) IsPriceValid(in market.Price, nodeType string, country string, serviceType string) bool { 155 if config.GetBool(config.FlagPaymentsDuringSessionDebug) { 156 log.Info().Msg("Payments debug bas been enabled, will agree with any price given") 157 return true 158 } 159 160 pricing := p.getPricing() 161 if p.pricesEqual(p.getCurrentByType(pricing, nodeType, country, serviceType), in) { 162 return true 163 } 164 if p.pricesEqual(p.getPreviousByType(pricing, nodeType, country, serviceType), in) { 165 return true 166 } 167 168 // this is the fallback in case loading of prices fails. 169 return p.isCheaperThanDefault(in) 170 } 171 172 func (p *Pricer) pricesEqual(api *market.Price, local market.Price) bool { 173 if api == nil || api.PricePerGiB == nil || api.PricePerHour == nil { 174 return false 175 } 176 177 return api.PricePerGiB.Cmp(local.PricePerGiB) == 0 && api.PricePerHour.Cmp(local.PricePerHour) == 0 178 } 179 180 func (p *Pricer) isCheaperThanDefault(in market.Price) bool { 181 return in.PricePerGiB.Cmp(defaultPrice.PricePerGiB) <= 0 && in.PricePerHour.Cmp(defaultPrice.PricePerHour) <= 0 182 } 183 184 // Subscribe subscribes to node events. 185 func (p *Pricer) Subscribe(bus eventbus.Subscriber) error { 186 return bus.SubscribeAsync(nodevent.AppTopicNode, p.preloadOnNodeStart) 187 } 188 189 func (p *Pricer) getPricing() market.LatestPrices { 190 p.mut.Lock() 191 lastLoad := p.lastLoad 192 p.mut.Unlock() 193 194 if time.Now().Truncate(0).UTC().After(lastLoad.CurrentValidUntil) { 195 p.loadPricing() 196 } 197 198 return p.lastLoad 199 } 200 201 func (p *Pricer) loadPricing() { 202 p.mut.Lock() 203 defer p.mut.Unlock() 204 205 now := time.Now().Truncate(0).UTC() 206 prices, err := p.discoAPI.GetPricing() 207 if err != nil { 208 log.Err(err).Msg("could not load pricing") 209 return 210 } 211 if prices.Defaults == nil { 212 log.Info().Msg("pricing info empty") 213 return 214 } 215 216 // shift clock skew 217 delta := now.Sub(prices.CurrentServerTime) 218 prices.CurrentValidUntil = prices.CurrentValidUntil.Add(delta) 219 prices.PreviousValidUntil = prices.PreviousValidUntil.Add(delta) 220 // equalize 221 prices.CurrentServerTime = now 222 223 log.Info().Msgf("pricing info loaded. expires @ %v", prices.CurrentValidUntil) 224 p.lastLoad = prices 225 } 226 227 func (p *Pricer) preloadOnNodeStart(se nodevent.Payload) { 228 if se.Status != nodevent.StatusStarted { 229 return 230 } 231 p.loadPricing() 232 }