-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ee8ecb3
commit ad9b566
Showing
8 changed files
with
875 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package price | ||
|
||
import ( | ||
"net/http" | ||
|
||
"github.com/gin-gonic/gin" | ||
"github.com/mysteriumnetwork/discovery/gorest" | ||
"github.com/mysteriumnetwork/discovery/price/pricingbyservice" | ||
"github.com/rs/zerolog/log" | ||
) | ||
|
||
type APIByService struct { | ||
pricer *pricingbyservice.PriceGetter | ||
jwtSecret string | ||
cfger pricingbyservice.ConfigProvider | ||
} | ||
|
||
func NewAPIByService(pricer *pricingbyservice.PriceGetter, cfger pricingbyservice.ConfigProvider, jwtSecret string) *APIByService { | ||
return &APIByService{ | ||
pricer: pricer, | ||
cfger: cfger, | ||
jwtSecret: jwtSecret, | ||
} | ||
} | ||
|
||
// LatestPrices returns latest prices | ||
// @Summary Latest Prices | ||
// @Description Latest Prices | ||
// @Product json | ||
// @Success 200 {array} pricing.LatestPrices | ||
// @Router /prices [get] | ||
// @Tags prices | ||
func (a *APIByService) LatestPrices(c *gin.Context) { | ||
c.JSON(200, a.pricer.GetPrices()) | ||
} | ||
|
||
// GetConfig returns the base pricing config | ||
// @Summary Price config | ||
// @Description price config | ||
// @Product json | ||
// @Success 200 {array} pricing.Config | ||
// @Router /prices/config [get] | ||
// @Tags prices | ||
func (a *APIByService) GetConfig(c *gin.Context) { | ||
cfg, err := a.cfger.Get() | ||
if err != nil { | ||
log.Err(err).Msg("Failed to get config") | ||
c.JSON(http.StatusInternalServerError, gorest.NewErrResponse(err.Error())) | ||
return | ||
} | ||
c.JSON(http.StatusOK, cfg) | ||
} | ||
|
||
// UpdateConfig updates the pricing config | ||
// @Summary update price config | ||
// @Description update price config | ||
// @Product json | ||
// @Success 202 | ||
// @Param config body pricing.Config true "config object" | ||
// @Router /prices/config [post] | ||
// @Tags prices | ||
func (a *APIByService) UpdateConfig(c *gin.Context) { | ||
var cfg pricingbyservice.Config | ||
if err := c.BindJSON(&cfg); err != nil { | ||
c.JSON(http.StatusBadRequest, gorest.NewErrResponse(err.Error())) | ||
return | ||
} | ||
|
||
err := a.cfger.Update(cfg) | ||
if err != nil { | ||
log.Err(err).Msg("Failed to update config") | ||
c.JSON(http.StatusBadRequest, gorest.NewErrResponse(err.Error())) | ||
return | ||
} | ||
|
||
c.Data(http.StatusAccepted, gin.MIMEJSON, nil) | ||
} | ||
|
||
func (a *APIByService) RegisterRoutes(r gin.IRoutes) { | ||
r.GET("/prices/config", JWTAuthorized(a.jwtSecret), a.GetConfig) | ||
r.POST("/prices/config", JWTAuthorized(a.jwtSecret), a.UpdateConfig) | ||
r.GET("/prices", a.LatestPrices) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
package pricingbyservice | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"sync" | ||
"time" | ||
|
||
"github.com/go-redis/redis/v8" | ||
"github.com/mysteriumnetwork/discovery/price/pricing" | ||
"github.com/rs/zerolog/log" | ||
) | ||
|
||
const PricingConfigRedisKey = "DISCOVERY_PRICE_BASE_CONFIG_BY_SERVICE" | ||
|
||
type ConfigProvider interface { | ||
Get() (Config, error) | ||
Update(Config) error | ||
} | ||
|
||
type ConfigProviderDB struct { | ||
db *redis.Client | ||
lock sync.Mutex | ||
} | ||
|
||
func NewConfigProviderDB(redis *redis.Client) *ConfigProviderDB { | ||
return &ConfigProviderDB{ | ||
db: redis, | ||
} | ||
} | ||
|
||
func (cpd *ConfigProviderDB) Get() (Config, error) { | ||
cfg, err := cpd.fetchConfig() | ||
if err != nil { | ||
log.Err(err).Msg("could not fetch config") | ||
return Config{}, errors.New("internal error") | ||
} | ||
|
||
return cfg, nil | ||
} | ||
|
||
func (cpd *ConfigProviderDB) Update(in Config) error { | ||
cpd.lock.Lock() | ||
defer cpd.lock.Unlock() | ||
|
||
err := in.Validate() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
cfgJSON, err := json.Marshal(in) | ||
if err != nil { | ||
return err | ||
} | ||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) | ||
defer cancel() | ||
|
||
err = cpd.db.Set(ctx, PricingConfigRedisKey, string(cfgJSON), 0).Err() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (cpd *ConfigProviderDB) fetchConfig() (Config, error) { | ||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) | ||
defer cancel() | ||
val, err := cpd.db.Get(ctx, PricingConfigRedisKey).Result() | ||
if err != nil { | ||
if errors.Is(err, redis.Nil) { | ||
err = cpd.db.Set(ctx, PricingConfigRedisKey, defaultPriceConfig, 0).Err() | ||
if err != nil { | ||
return Config{}, err | ||
} | ||
val = defaultPriceConfig | ||
} else { | ||
return Config{}, err | ||
} | ||
} | ||
|
||
res := Config{} | ||
return res, json.Unmarshal([]byte(val), &res) | ||
} | ||
|
||
type Config struct { | ||
BasePrices PriceByTypeUSD `json:"base_prices"` | ||
CountryModifiers map[pricing.ISO3166CountryCode]Modifier `json:"country_modifiers"` | ||
} | ||
|
||
func (c Config) Validate() error { | ||
err := c.BasePrices.Validate() | ||
if err != nil { | ||
return fmt.Errorf("base price invalid: %w", err) | ||
} | ||
|
||
for k, v := range c.CountryModifiers { | ||
err := k.Validate() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = v.Validate() | ||
if err != nil { | ||
return fmt.Errorf("country %v contains invalid pricing: %w", k, err) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type PriceByTypeUSD struct { | ||
Residential *PriceByServiceTypeUSD `json:"residential"` | ||
Other *PriceByServiceTypeUSD `json:"other"` | ||
} | ||
|
||
func (p PriceByTypeUSD) Validate() error { | ||
if p.Residential == nil || p.Other == nil { | ||
return errors.New("residential and other pricing should not be nil") | ||
} | ||
|
||
err := p.Residential.Validate() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return p.Other.Validate() | ||
} | ||
|
||
type PriceByServiceTypeUSD struct { | ||
Wireguard *PriceUSD `json:"wireguard"` | ||
Scraping *PriceUSD `json:"scraping"` | ||
DataTransfer *PriceUSD `json:"data_transfer"` | ||
} | ||
|
||
func (p PriceByServiceTypeUSD) Validate() error { | ||
if p.Wireguard == nil || p.Scraping == nil || p.DataTransfer == nil { | ||
return errors.New("wireguard, scraping and data_transfer pricing should not be nil") | ||
} | ||
|
||
if err := p.Wireguard.Validate(); err != nil { | ||
return err | ||
} | ||
if err := p.Scraping.Validate(); err != nil { | ||
return err | ||
} | ||
return p.DataTransfer.Validate() | ||
} | ||
|
||
type PriceUSD struct { | ||
PricePerHour float64 `json:"price_per_hour_usd"` | ||
PricePerGiB float64 `json:"price_per_gib_usd"` | ||
} | ||
|
||
func (p PriceUSD) Validate() error { | ||
if p.PricePerGiB < 0 || p.PricePerHour < 0 { | ||
return errors.New("prices should be non negative") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type Modifier struct { | ||
Residential float64 `json:"residential"` | ||
Other float64 `json:"other"` | ||
} | ||
|
||
func (m Modifier) Validate() error { | ||
if m.Residential < 0 || m.Other < 0 { | ||
return errors.New("modifiers should be non negative") | ||
} | ||
return nil | ||
} | ||
|
||
var defaultPriceConfig = `{ | ||
"base_prices": { | ||
"residential": { | ||
"wireguard": { | ||
"price_per_hour_usd": 0.00036, | ||
"price_per_gib_usd": 0.06 | ||
}, | ||
"scraping": { | ||
"price_per_hour_usd": 0.00036, | ||
"price_per_gib_usd": 0.06 | ||
}, | ||
"data_transfer": { | ||
"price_per_hour_usd": 0.00036, | ||
"price_per_gib_usd": 0.06 | ||
} | ||
}, | ||
"other": { | ||
"wireguard": { | ||
"price_per_hour_usd": 0.00036, | ||
"price_per_gib_usd": 0.06 | ||
}, | ||
"scraping": { | ||
"price_per_hour_usd": 0.00036, | ||
"price_per_gib_usd": 0.06 | ||
}, | ||
"data_transfer": { | ||
"price_per_hour_usd": 0.00036, | ||
"price_per_gib_usd": 0.06 | ||
} | ||
} | ||
}, | ||
"country_modifiers": { | ||
"US": { | ||
"residential": 1.5, | ||
"other": 1.2 | ||
} | ||
} | ||
}` |
Oops, something went wrong.