github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/tequilapi/endpoints/proposals.go (about)

     1  /*
     2   * Copyright (C) 2017 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 endpoints
    19  
    20  import (
    21  	"strconv"
    22  
    23  	"github.com/gin-gonic/gin"
    24  	"github.com/mysteriumnetwork/go-rest/apierror"
    25  
    26  	"github.com/mysteriumnetwork/node/core/discovery/proposal"
    27  	"github.com/mysteriumnetwork/node/core/location"
    28  	"github.com/mysteriumnetwork/node/core/quality"
    29  	"github.com/mysteriumnetwork/node/market"
    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/scraping"
    34  	"github.com/mysteriumnetwork/node/services/wireguard"
    35  	"github.com/mysteriumnetwork/node/tequilapi/contract"
    36  	"github.com/mysteriumnetwork/node/tequilapi/utils"
    37  )
    38  
    39  // QualityFinder allows to fetch proposal quality data
    40  type QualityFinder interface {
    41  	ProposalsQuality() []quality.ProposalQuality
    42  }
    43  
    44  type priceAPI interface {
    45  	GetCurrentPrice(nodeType string, country string, serviceType string) (market.Price, error)
    46  }
    47  
    48  type proposalsEndpoint struct {
    49  	proposalRepository proposalRepository
    50  	pricer             priceAPI
    51  	locationResolver   location.Resolver
    52  	filterPresets      proposal.FilterPresetRepository
    53  	natProber          natProber
    54  }
    55  
    56  // NewProposalsEndpoint creates and returns proposal creation endpoint
    57  func NewProposalsEndpoint(proposalRepository proposalRepository, pricer priceAPI, locationResolver location.Resolver, filterPresetRepository proposal.FilterPresetRepository, natProber natProber) *proposalsEndpoint {
    58  	return &proposalsEndpoint{
    59  		proposalRepository: proposalRepository,
    60  		pricer:             pricer,
    61  		locationResolver:   locationResolver,
    62  		filterPresets:      filterPresetRepository,
    63  		natProber:          natProber,
    64  	}
    65  }
    66  
    67  // swagger:operation GET /proposals Proposal listProposals
    68  //
    69  //	---
    70  //	summary: Returns proposals
    71  //	description: Returns list of proposals filtered by provider id
    72  //	parameters:
    73  //	  - in: query
    74  //	    name: provider_id
    75  //	    description: id of provider proposals
    76  //	    type: string
    77  //	  - in: query
    78  //	    name: service_type
    79  //	    description: the service type of the proposal. Possible values are "openvpn", "wireguard" and "noop"
    80  //	    type: string
    81  //	  - in: query
    82  //	    name: access_policy
    83  //	    description: the access policy id to filter the proposals by
    84  //	    type: string
    85  //	  - in: query
    86  //	    name: access_policy_source
    87  //	    description: the access policy source to filter the proposals by
    88  //	    type: string
    89  //	  - in: query
    90  //	    name: country
    91  //	    description: If given will filter proposals by node location country.
    92  //	    type: string
    93  //	  - in: query
    94  //	    name: ip_type
    95  //	    description: IP Type (residential, datacenter, etc.).
    96  //	    type: string
    97  //	  - in: query
    98  //	    name: compatibility_min
    99  //	    description: Minimum compatibility level of the proposal.
   100  //	    type: integer
   101  //	  - in: query
   102  //	    name: compatibility_max
   103  //	    description: Maximum compatibility level of the proposal.
   104  //	    type: integer
   105  //	  - in: query
   106  //	    name: quality_min
   107  //	    description: Minimum quality of the provider.
   108  //	    type: number
   109  //	  - in: query
   110  //	    name: nat_compatibility
   111  //	    description: Pick nodes compatible with NAT of specified type. Specify "auto" to probe NAT.
   112  //	    type: string
   113  //	responses:
   114  //	  200:
   115  //	    description: List of proposals
   116  //	    schema:
   117  //	      "$ref": "#/definitions/ListProposalsResponse"
   118  //	  500:
   119  //	    description: Internal server error
   120  //	    schema:
   121  //	      "$ref": "#/definitions/APIError"
   122  func (pe *proposalsEndpoint) List(c *gin.Context) {
   123  	req := c.Request
   124  	presetID, _ := strconv.Atoi(req.URL.Query().Get("preset_id"))
   125  	compatibilityMinQuery := req.URL.Query().Get("compatibility_min")
   126  	compatibilityMin := 2
   127  	if compatibilityMinQuery != "" {
   128  		compatibilityMin, _ = strconv.Atoi(compatibilityMinQuery)
   129  	}
   130  	compatibilityMax, _ := strconv.Atoi(req.URL.Query().Get("compatibility_max"))
   131  	qualityMin := func() float32 {
   132  		f, err := strconv.ParseFloat(req.URL.Query().Get("quality_min"), 32)
   133  		if err != nil {
   134  			return 0
   135  		}
   136  		return float32(f)
   137  	}()
   138  
   139  	natCompatibility := nat.NATType(req.URL.Query().Get("nat_compatibility"))
   140  	if natCompatibility == contract.AutoNATType {
   141  		natType, err := pe.natProber.Probe(req.Context())
   142  		if err != nil {
   143  			natCompatibility = ""
   144  		} else {
   145  			natCompatibility = natType
   146  		}
   147  	}
   148  
   149  	includeMonitoringFailed, _ := strconv.ParseBool(req.URL.Query().Get("include_monitoring_failed"))
   150  	proposals, err := pe.proposalRepository.Proposals(&proposal.Filter{
   151  		PresetID:                presetID,
   152  		ProviderID:              req.URL.Query().Get("provider_id"),
   153  		ServiceType:             req.URL.Query().Get("service_type"),
   154  		AccessPolicy:            req.URL.Query().Get("access_policy"),
   155  		AccessPolicySource:      req.URL.Query().Get("access_policy_source"),
   156  		LocationCountry:         req.URL.Query().Get("location_country"),
   157  		IPType:                  req.URL.Query().Get("ip_type"),
   158  		NATCompatibility:        natCompatibility,
   159  		CompatibilityMin:        compatibilityMin,
   160  		CompatibilityMax:        compatibilityMax,
   161  		QualityMin:              qualityMin,
   162  		ExcludeUnsupported:      true,
   163  		IncludeMonitoringFailed: includeMonitoringFailed,
   164  	})
   165  	if err != nil {
   166  		c.Error(apierror.Internal("Proposal query failed: "+err.Error(), contract.ErrCodeProposalsQuery))
   167  		return
   168  	}
   169  
   170  	proposalsRes := contract.ListProposalsResponse{Proposals: []contract.ProposalDTO{}}
   171  	for _, p := range proposals {
   172  		proposalsRes.Proposals = append(proposalsRes.Proposals, contract.NewProposalDTO(p))
   173  	}
   174  
   175  	utils.WriteAsJSON(proposalsRes, c.Writer)
   176  }
   177  
   178  // swagger:operation GET /proposals/countries Countries listCountries
   179  //
   180  //	---
   181  //	summary: Returns number of proposals per country
   182  //	description: Returns a list of countries with a number of proposals
   183  //	parameters:
   184  //	  - in: query
   185  //	    name: provider_id
   186  //	    description: id of provider proposals
   187  //	    type: string
   188  //	  - in: query
   189  //	    name: service_type
   190  //	    description: the service type of the proposal. Possible values are "openvpn", "wireguard" and "noop"
   191  //	    type: string
   192  //	  - in: query
   193  //	    name: access_policy
   194  //	    description: the access policy id to filter the proposals by
   195  //	    type: string
   196  //	  - in: query
   197  //	    name: access_policy_source
   198  //	    description: the access policy source to filter the proposals by
   199  //	    type: string
   200  //	  - in: query
   201  //	    name: country
   202  //	    description: If given will filter proposals by node location country.
   203  //	    type: string
   204  //	  - in: query
   205  //	    name: ip_type
   206  //	    description: IP Type (residential, datacenter, etc.).
   207  //	    type: string
   208  //	  - in: query
   209  //	    name: compatibility_min
   210  //	    description: Minimum compatibility level of the proposal.
   211  //	    type: integer
   212  //	  - in: query
   213  //	    name: compatibility_max
   214  //	    description: Maximum compatibility level of the proposal.
   215  //	    type: integer
   216  //	  - in: query
   217  //	    name: quality_min
   218  //	    description: Minimum quality of the provider.
   219  //	    type: number
   220  //	  - in: query
   221  //	    name: nat_compatibility
   222  //	    description: Pick nodes compatible with NAT of specified type. Specify "auto" to probe NAT.
   223  //	    type: string
   224  //	responses:
   225  //	  200:
   226  //	    description: List of countries
   227  //	    schema:
   228  //	      "$ref": "#/definitions/ListProposalsCountiesResponse"
   229  //	  500:
   230  //	    description: Internal server error
   231  //	    schema:
   232  //	      "$ref": "#/definitions/APIError"
   233  func (pe *proposalsEndpoint) Countries(c *gin.Context) {
   234  	req := c.Request
   235  
   236  	presetID, _ := strconv.Atoi(req.URL.Query().Get("preset_id"))
   237  	compatibilityMinQuery := req.URL.Query().Get("compatibility_min")
   238  	compatibilityMin := 2
   239  	if compatibilityMinQuery != "" {
   240  		compatibilityMin, _ = strconv.Atoi(compatibilityMinQuery)
   241  	}
   242  	compatibilityMax, _ := strconv.Atoi(req.URL.Query().Get("compatibility_max"))
   243  	qualityMin := func() float32 {
   244  		f, err := strconv.ParseFloat(req.URL.Query().Get("quality_min"), 32)
   245  		if err != nil {
   246  			return 0
   247  		}
   248  		return float32(f)
   249  	}()
   250  
   251  	natCompatibility := nat.NATType(req.URL.Query().Get("nat_compatibility"))
   252  	if natCompatibility == contract.AutoNATType {
   253  		natType, err := pe.natProber.Probe(req.Context())
   254  		if err != nil {
   255  			natCompatibility = ""
   256  		} else {
   257  			natCompatibility = natType
   258  		}
   259  	}
   260  
   261  	includeMonitoringFailed, _ := strconv.ParseBool(req.URL.Query().Get("include_monitoring_failed"))
   262  	countries, err := pe.proposalRepository.Countries(&proposal.Filter{
   263  		PresetID:                presetID,
   264  		ProviderID:              req.URL.Query().Get("provider_id"),
   265  		ServiceType:             req.URL.Query().Get("service_type"),
   266  		AccessPolicy:            req.URL.Query().Get("access_policy"),
   267  		AccessPolicySource:      req.URL.Query().Get("access_policy_source"),
   268  		LocationCountry:         req.URL.Query().Get("location_country"),
   269  		IPType:                  req.URL.Query().Get("ip_type"),
   270  		NATCompatibility:        natCompatibility,
   271  		CompatibilityMin:        compatibilityMin,
   272  		CompatibilityMax:        compatibilityMax,
   273  		QualityMin:              qualityMin,
   274  		ExcludeUnsupported:      true,
   275  		IncludeMonitoringFailed: includeMonitoringFailed,
   276  	})
   277  	if err != nil {
   278  		c.Error(apierror.Internal("Proposal country query failed: "+err.Error(), contract.ErrCodeProposalsCountryQuery))
   279  		return
   280  	}
   281  
   282  	utils.WriteAsJSON(countries, c.Writer)
   283  }
   284  
   285  // swagger:operation GET /prices/current
   286  //
   287  //	---
   288  //	summary: Returns proposals
   289  //	description: Returns list of proposals filtered by provider id
   290  //	responses:
   291  //	  200:
   292  //	    description: Current proposal price
   293  //	    schema:
   294  //	      "$ref": "#/definitions/CurrentPriceResponse"
   295  //	  500:
   296  //	    description: Internal server error
   297  //	    schema:
   298  //	      "$ref": "#/definitions/APIError"
   299  func (pe *proposalsEndpoint) CurrentPrice(c *gin.Context) {
   300  	allowedServiceTypes := map[string]struct{}{
   301  		wireguard.ServiceType:    {},
   302  		scraping.ServiceType:     {},
   303  		datatransfer.ServiceType: {},
   304  		dvpn.ServiceType:         {},
   305  	}
   306  
   307  	serviceType := c.Request.URL.Query().Get("service_type")
   308  	if len(serviceType) == 0 {
   309  		serviceType = wireguard.ServiceType
   310  	} else {
   311  		if _, ok := allowedServiceTypes[serviceType]; !ok {
   312  			c.Error(apierror.BadRequest("Invalid service type", contract.ErrCodeProposalsServiceType))
   313  			return
   314  		}
   315  	}
   316  
   317  	loc, err := pe.locationResolver.DetectLocation()
   318  	if err != nil {
   319  		c.Error(apierror.Internal("Cannot detect location", contract.ErrCodeProposalsDetectLocation))
   320  		return
   321  	}
   322  
   323  	price, err := pe.pricer.GetCurrentPrice(loc.IPType, loc.Country, serviceType)
   324  	if err != nil {
   325  		c.Error(apierror.Internal("Cannot retrieve current prices: "+err.Error(), contract.ErrCodeProposalsPrices))
   326  		return
   327  	}
   328  
   329  	utils.WriteAsJSON(contract.CurrentPriceResponse{
   330  		ServiceType: serviceType,
   331  
   332  		PricePerHour: price.PricePerHour,
   333  		PricePerGiB:  price.PricePerGiB,
   334  
   335  		PricePerHourTokens: contract.NewTokens(price.PricePerHour),
   336  		PricePerGiBTokens:  contract.NewTokens(price.PricePerGiB),
   337  	}, c.Writer)
   338  }
   339  
   340  // swagger:operation GET /v2/prices/current
   341  //
   342  //	---
   343  //	summary: Returns prices
   344  //	description: Returns prices for all service types
   345  //	responses:
   346  //	  200:
   347  //	    description: Current price for service type
   348  //	    schema:
   349  //	      "$ref": "#/definitions/CurrentPriceResponse"
   350  //	  500:
   351  //	    description: Internal server error
   352  //	    schema:
   353  //	      "$ref": "#/definitions/APIError"
   354  func (pe *proposalsEndpoint) CurrentPrices(c *gin.Context) {
   355  	loc, err := pe.locationResolver.DetectLocation()
   356  	if err != nil {
   357  		c.Error(apierror.Internal("Cannot detect location", contract.ErrCodeProposalsDetectLocation))
   358  		return
   359  	}
   360  
   361  	serviceTypes := []string{wireguard.ServiceType, scraping.ServiceType, datatransfer.ServiceType, dvpn.ServiceType}
   362  	result := make([]contract.CurrentPriceResponse, len(serviceTypes))
   363  
   364  	for i, serviceType := range serviceTypes {
   365  		price, err := pe.pricer.GetCurrentPrice(loc.IPType, loc.Country, serviceType)
   366  		if err != nil {
   367  			c.Error(apierror.Internal("Cannot retrieve current prices: "+err.Error(), contract.ErrCodeProposalsPrices))
   368  			return
   369  		}
   370  
   371  		result[i] = contract.CurrentPriceResponse{
   372  			ServiceType:  serviceType,
   373  			PricePerHour: price.PricePerHour,
   374  			PricePerGiB:  price.PricePerGiB,
   375  
   376  			PricePerHourTokens: contract.NewTokens(price.PricePerHour),
   377  			PricePerGiBTokens:  contract.NewTokens(price.PricePerGiB),
   378  		}
   379  	}
   380  
   381  	utils.WriteAsJSON(result, c.Writer)
   382  }
   383  
   384  // swagger:operation GET /proposals/filter-presets Proposal proposalFilterPresets
   385  //
   386  //	---
   387  //	summary: Returns proposal filter presets
   388  //	description: Returns proposal filter presets
   389  //	responses:
   390  //	  200:
   391  //	    description: List of proposal filter presets
   392  //	    schema:
   393  //	      "$ref": "#/definitions/ListProposalFilterPresetsResponse"
   394  //	  500:
   395  //	    description: Internal server error
   396  //	    schema:
   397  //	      "$ref": "#/definitions/APIError"
   398  func (pe *proposalsEndpoint) FilterPresets(c *gin.Context) {
   399  	presets, err := pe.filterPresets.List()
   400  	if err != nil {
   401  		c.Error(apierror.Internal("Cannot list presets", contract.ErrCodeProposalsPresets))
   402  		return
   403  	}
   404  	presetsRes := contract.ListProposalFilterPresetsResponse{Items: []contract.FilterPreset{}}
   405  	for _, p := range presets.Entries {
   406  		presetsRes.Items = append(presetsRes.Items, contract.NewFilterPreset(p))
   407  	}
   408  	utils.WriteAsJSON(presetsRes, c.Writer)
   409  }
   410  
   411  // AddRoutesForProposals attaches proposals endpoints to router
   412  func AddRoutesForProposals(
   413  	proposalRepository proposalRepository,
   414  	pricer priceAPI,
   415  	locationResolver location.Resolver,
   416  	filterPresetRepository proposal.FilterPresetRepository,
   417  	natProber natProber,
   418  ) func(*gin.Engine) error {
   419  	pe := NewProposalsEndpoint(proposalRepository, pricer, locationResolver, filterPresetRepository, natProber)
   420  	return func(e *gin.Engine) error {
   421  		proposalGroup := e.Group("/proposals")
   422  		{
   423  			proposalGroup.GET("", pe.List)
   424  			proposalGroup.GET("/filter-presets", pe.FilterPresets)
   425  			proposalGroup.GET("/countries", pe.Countries)
   426  		}
   427  
   428  		e.GET("/prices/current", pe.CurrentPrice)
   429  		e.GET("/v2/prices/current", pe.CurrentPrices)
   430  		return nil
   431  	}
   432  }