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  }