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 }