github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/mobile/mysterium/proposals_manager.go (about)

     1  /*
     2   * Copyright (C) 2019 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 mysterium
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"math/big"
    24  	"time"
    25  
    26  	"github.com/mysteriumnetwork/node/core/discovery/proposal"
    27  	"github.com/mysteriumnetwork/node/core/quality"
    28  	"github.com/mysteriumnetwork/node/market"
    29  	"github.com/mysteriumnetwork/node/money"
    30  	"github.com/mysteriumnetwork/node/nat"
    31  	"github.com/mysteriumnetwork/node/services/datatransfer"
    32  	"github.com/mysteriumnetwork/node/services/dvpn"
    33  	"github.com/mysteriumnetwork/node/services/openvpn"
    34  	"github.com/mysteriumnetwork/node/services/scraping"
    35  	"github.com/mysteriumnetwork/node/services/wireguard"
    36  )
    37  
    38  const (
    39  	qualityLevelMedium = 1
    40  	qualityLevelHigh   = 2
    41  )
    42  
    43  // AutoNATType passed as NATCompatibility parameter in proposal request
    44  // indicates NAT type should be probed automatically immediately within given
    45  // request
    46  const AutoNATType = "auto"
    47  
    48  type proposalQualityLevel int
    49  
    50  const (
    51  	proposalQualityLevelUnknown proposalQualityLevel = 0
    52  	proposalQualityLevelLow     proposalQualityLevel = 1
    53  	proposalQualityLevelMedium  proposalQualityLevel = 2
    54  	proposalQualityLevelHigh    proposalQualityLevel = 3
    55  )
    56  
    57  // GetProposalsRequest represents proposals request.
    58  type GetProposalsRequest struct {
    59  	ServiceType      string
    60  	LocationCountry  string
    61  	IPType           string
    62  	Refresh          bool
    63  	PriceHourMax     float64
    64  	PriceGiBMax      float64
    65  	QualityMin       float32
    66  	PresetID         int
    67  	NATCompatibility string
    68  }
    69  
    70  func (r GetProposalsRequest) toFilter() *proposal.Filter {
    71  	return &proposal.Filter{
    72  		PresetID:           r.PresetID,
    73  		ServiceType:        r.ServiceType,
    74  		LocationCountry:    r.LocationCountry,
    75  		IPType:             r.IPType,
    76  		QualityMin:         r.QualityMin,
    77  		ExcludeUnsupported: true,
    78  		NATCompatibility:   nat.NATType(r.NATCompatibility),
    79  	}
    80  }
    81  
    82  // GetProposalRequest represents proposal request.
    83  type GetProposalRequest struct {
    84  	ProviderID  string
    85  	ServiceType string
    86  }
    87  
    88  type proposalDTO struct {
    89  	ProviderID   string               `json:"provider_id"`
    90  	ServiceType  string               `json:"service_type"`
    91  	Country      string               `json:"country"`
    92  	IPType       string               `json:"ip_type"`
    93  	QualityLevel proposalQualityLevel `json:"quality_level"`
    94  	Price        proposalPrice        `json:"price"`
    95  }
    96  
    97  type proposalPrice struct {
    98  	Currency string  `json:"currency"`
    99  	PerGiB   float64 `json:"per_gib"`
   100  	PerHour  float64 `json:"per_hour"`
   101  }
   102  
   103  type getProposalsResponse struct {
   104  	Proposals []*proposalDTO `json:"proposals"`
   105  }
   106  
   107  type getCountriesResponse map[string]int
   108  
   109  type getProposalResponse struct {
   110  	Proposal *proposalDTO `json:"proposal"`
   111  }
   112  
   113  type qualityFinder interface {
   114  	ProposalsQuality() []quality.ProposalQuality
   115  }
   116  
   117  type proposalRepository interface {
   118  	Proposals(filter *proposal.Filter) ([]proposal.PricedServiceProposal, error)
   119  	Countries(filter *proposal.Filter) (map[string]int, error)
   120  	Proposal(market.ProposalID) (*proposal.PricedServiceProposal, error)
   121  }
   122  
   123  type natProber interface {
   124  	Probe(context.Context) (nat.NATType, error)
   125  }
   126  
   127  func newProposalsManager(
   128  	repository proposalRepository,
   129  	filterPresetStorage *proposal.FilterPresetStorage,
   130  	natProber natProber,
   131  	cacheTTL time.Duration,
   132  ) *proposalsManager {
   133  	return &proposalsManager{
   134  		repository:          repository,
   135  		filterPresetStorage: filterPresetStorage,
   136  		cacheTTL:            cacheTTL,
   137  		natProber:           natProber,
   138  	}
   139  }
   140  
   141  type proposalsManager struct {
   142  	repository          proposalRepository
   143  	cache               []proposal.PricedServiceProposal
   144  	cachedAt            time.Time
   145  	cacheTTL            time.Duration
   146  	filterPresetStorage *proposal.FilterPresetStorage
   147  	natProber           natProber
   148  }
   149  
   150  func (m *proposalsManager) isCacheStale() bool {
   151  	return time.Now().After(m.cachedAt.Add(m.cacheTTL))
   152  }
   153  
   154  func (m *proposalsManager) getCountries(req *GetProposalsRequest) (getCountriesResponse, error) {
   155  	return m.getCountriesFromRepository(req)
   156  }
   157  
   158  func (m *proposalsManager) getProposals(req *GetProposalsRequest) (*getProposalsResponse, error) {
   159  	// Get proposals from cache if exists.
   160  	if req.Refresh || m.isCacheStale() {
   161  		apiProposals, err := m.getFromRepository(req)
   162  		if err != nil {
   163  			return nil, err
   164  		}
   165  		m.addToCache(apiProposals)
   166  	}
   167  
   168  	filteredProposals, err := m.applyFilter(req.PresetID, m.getFromCache())
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	return m.map2Response(filteredProposals)
   173  }
   174  
   175  func (m *proposalsManager) applyFilter(presetID int, proposals []proposal.PricedServiceProposal) ([]proposal.PricedServiceProposal, error) {
   176  	if presetID != 0 {
   177  		preset, err := m.filterPresetStorage.Get(presetID)
   178  		if err != nil {
   179  			return nil, err
   180  		}
   181  		return preset.Filter(proposals), nil
   182  	}
   183  
   184  	return proposals, nil
   185  }
   186  
   187  func (m *proposalsManager) getFromCache() []proposal.PricedServiceProposal {
   188  	return m.cache
   189  }
   190  
   191  func (m *proposalsManager) addToCache(proposals []proposal.PricedServiceProposal) {
   192  	m.cache = proposals
   193  	m.cachedAt = time.Now()
   194  }
   195  
   196  func (m *proposalsManager) getFromRepository(req *GetProposalsRequest) ([]proposal.PricedServiceProposal, error) {
   197  	filter := req.toFilter()
   198  	if filter.NATCompatibility == AutoNATType {
   199  		natType, err := m.natProber.Probe(context.TODO())
   200  		if err != nil {
   201  			filter.NATCompatibility = ""
   202  		} else {
   203  			filter.NATCompatibility = natType
   204  		}
   205  	}
   206  	filter.CompatibilityMin = 2
   207  	allProposals, err := m.repository.Proposals(filter)
   208  	if err != nil {
   209  		return nil, fmt.Errorf("could not get proposals from repository: %w", err)
   210  	}
   211  
   212  	// Ideally api should allow to pass multiple service types to skip noop
   213  	// proposals, but for now just filter in memory.
   214  	serviceTypes := map[string]bool{
   215  		openvpn.ServiceType:      true,
   216  		wireguard.ServiceType:    true,
   217  		datatransfer.ServiceType: true,
   218  		scraping.ServiceType:     true,
   219  		dvpn.ServiceType:         true,
   220  	}
   221  	var res []proposal.PricedServiceProposal
   222  	for _, p := range allProposals {
   223  		if serviceTypes[p.ServiceType] {
   224  			res = append(res, p)
   225  		}
   226  	}
   227  	return res, nil
   228  }
   229  
   230  func (m *proposalsManager) getCountriesFromRepository(req *GetProposalsRequest) (getCountriesResponse, error) {
   231  	filter := req.toFilter()
   232  	if filter.NATCompatibility == AutoNATType {
   233  		natType, err := m.natProber.Probe(context.TODO())
   234  		if err != nil {
   235  			filter.NATCompatibility = ""
   236  		} else {
   237  			filter.NATCompatibility = natType
   238  		}
   239  	}
   240  	filter.CompatibilityMin = 2
   241  	countries, err := m.repository.Countries(filter)
   242  	if err != nil {
   243  		return nil, fmt.Errorf("could not get proposals from repository: %w", err)
   244  	}
   245  
   246  	return countries, nil
   247  }
   248  
   249  func (m *proposalsManager) map2Response(serviceProposals []proposal.PricedServiceProposal) (*getProposalsResponse, error) {
   250  	var proposals []*proposalDTO
   251  	for _, p := range serviceProposals {
   252  		proposals = append(proposals, m.mapProposal(&p))
   253  	}
   254  	return &getProposalsResponse{Proposals: proposals}, nil
   255  }
   256  
   257  func (m *proposalsManager) mapProposal(p *proposal.PricedServiceProposal) *proposalDTO {
   258  	perGib, _ := big.NewFloat(0).SetInt(p.Price.PricePerGiB).Float64()
   259  	perHour, _ := big.NewFloat(0).SetInt(p.Price.PricePerHour).Float64()
   260  	prop := &proposalDTO{
   261  		ProviderID:   p.ProviderID,
   262  		ServiceType:  p.ServiceType,
   263  		QualityLevel: proposalQualityLevelUnknown,
   264  		Price: proposalPrice{
   265  			Currency: money.CurrencyMyst.String(),
   266  			PerGiB:   perGib,
   267  			PerHour:  perHour,
   268  		},
   269  	}
   270  
   271  	prop.Country = p.Location.Country
   272  	prop.IPType = p.Location.IPType
   273  	prop.QualityLevel = m.calculateMetricQualityLevel(p.Quality.Quality)
   274  
   275  	return prop
   276  }
   277  
   278  func (m *proposalsManager) calculateMetricQualityLevel(quality float64) proposalQualityLevel {
   279  	if quality == 0 {
   280  		return proposalQualityLevelUnknown
   281  	}
   282  
   283  	if quality >= qualityLevelHigh {
   284  		return proposalQualityLevelHigh
   285  	}
   286  
   287  	if quality >= qualityLevelMedium {
   288  		return proposalQualityLevelMedium
   289  	}
   290  
   291  	return proposalQualityLevelLow
   292  }