feat: add initial price tracker
This commit is contained in:
parent
736dc8aa9d
commit
dd296bd78a
|
@ -3,10 +3,24 @@ package config
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var _ Validator = (*SiaConfig)(nil)
|
var _ Validator = (*SiaConfig)(nil)
|
||||||
|
var _ Defaults = (*SiaConfig)(nil)
|
||||||
|
|
||||||
type SiaConfig struct {
|
type SiaConfig struct {
|
||||||
Key string `mapstructure:"key"`
|
Key string `mapstructure:"key"`
|
||||||
URL string `mapstructure:"url"`
|
URL string `mapstructure:"url"`
|
||||||
|
PriceHistoryDays uint64 `mapstructure:"price_history_days"`
|
||||||
|
MaxUploadPrice float64 `mapstructure:"max_upload_price"`
|
||||||
|
MaxDownloadPrice float64 `mapstructure:"max_download_price"`
|
||||||
|
MaxStoragePrice float64 `mapstructure:"max_storage_price"`
|
||||||
|
MaxContractPrice float64 `mapstructure:"max_contract_price"`
|
||||||
|
MaxRPCPrice float64 `mapstructure:"max_rpc_price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SiaConfig) Defaults() map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"max_rpc_price": 1,
|
||||||
|
"price_history_days": 90,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SiaConfig) Validate() error {
|
func (s SiaConfig) Validate() error {
|
||||||
|
@ -16,5 +30,26 @@ func (s SiaConfig) Validate() error {
|
||||||
if s.URL == "" {
|
if s.URL == "" {
|
||||||
return errors.New("core.storage.sia.url is required")
|
return errors.New("core.storage.sia.url is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.MaxUploadPrice <= 0 {
|
||||||
|
return errors.New("core.storage.sia.max_upload_price must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.MaxDownloadPrice <= 0 {
|
||||||
|
return errors.New("core.storage.sia.max_download_price must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.MaxStoragePrice <= 0 {
|
||||||
|
return errors.New("core.storage.sia.max_storage_price must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.MaxContractPrice <= 0 {
|
||||||
|
return errors.New("core.storage.sia.max_contract_price must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.MaxRPCPrice <= 0 {
|
||||||
|
return errors.New("core.storage.sia.max_rpc_price must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registerModel(&SCPriceHistory{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type SCPriceHistory struct {
|
||||||
|
gorm.Model
|
||||||
|
CreatedAt time.Time `gorm:"index:idx_rate"`
|
||||||
|
Rate float64 `gorm:"index:idx_rate"`
|
||||||
|
}
|
|
@ -0,0 +1,238 @@
|
||||||
|
package renter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/db/models"
|
||||||
|
|
||||||
|
"github.com/siacentral/apisdkgo"
|
||||||
|
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/config"
|
||||||
|
"git.lumeweb.com/LumeWeb/portal/cron"
|
||||||
|
"github.com/go-co-op/gocron/v2"
|
||||||
|
siasdk "github.com/siacentral/apisdkgo/sia"
|
||||||
|
"go.sia.tech/core/types"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ cron.CronableService = (*PriceTracker)(nil)
|
||||||
|
|
||||||
|
const usdSymbol = "usd"
|
||||||
|
|
||||||
|
type PriceTracker struct {
|
||||||
|
config *config.Manager
|
||||||
|
logger *zap.Logger
|
||||||
|
cron *cron.CronServiceDefault
|
||||||
|
db *gorm.DB
|
||||||
|
renter *RenterDefault
|
||||||
|
api *siasdk.APIClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PriceTracker) LoadInitialTasks(cron cron.CronService) error {
|
||||||
|
job := gocron.DurationJob(time.Minute)
|
||||||
|
_, err := cron.Scheduler().NewJob(
|
||||||
|
job,
|
||||||
|
gocron.NewTask(p.recordRate),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PriceTracker) recordRate() {
|
||||||
|
rate, _, err := p.api.GetExchangeRate()
|
||||||
|
if err != nil {
|
||||||
|
p.logger.Error("failed to get exchange rate", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
siaPrice, ok := rate[usdSymbol]
|
||||||
|
if !ok {
|
||||||
|
p.logger.Error("exchange rate not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var history models.SCPriceHistory
|
||||||
|
|
||||||
|
history.Rate = siaPrice
|
||||||
|
|
||||||
|
if tx := p.db.Create(&history); tx.Error != nil {
|
||||||
|
p.logger.Error("failed to save price history", zap.Error(tx.Error))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.updatePrices(); err != nil {
|
||||||
|
p.logger.Error("failed to update prices", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PriceTracker) updatePrices() error {
|
||||||
|
var averageRate float64
|
||||||
|
x := 1
|
||||||
|
sql := `
|
||||||
|
SELECT AVG(rate) as average_rate FROM (
|
||||||
|
SELECT rate FROM (
|
||||||
|
SELECT rate, ROW_NUMBER() OVER (PARTITION BY DATE(created_at) ORDER BY created_at DESC) as rn
|
||||||
|
FROM sc_price_histories
|
||||||
|
WHERE created_at >= NOW() - INTERVAL ? day
|
||||||
|
) tmp WHERE rn = 1
|
||||||
|
) final;
|
||||||
|
`
|
||||||
|
err := p.db.Raw(sql, x).Scan(&averageRate).Error
|
||||||
|
if err != nil {
|
||||||
|
p.logger.Error("failed to fetch average rate", zap.Error(err), zap.Int("days", x))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if averageRate == 0 {
|
||||||
|
p.logger.Error("average rate is 0")
|
||||||
|
return errors.New("average rate is 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
gouge, err := p.renter.GougingSettings(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
p.logger.Error("failed to fetch gouging settings", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gouge.MaxDownloadPrice, err = siacoinsFromFloat(p.config.Config().Core.Sia.MaxDownloadPrice / averageRate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gouge.MaxUploadPrice, err = siacoinsFromFloat(p.config.Config().Core.Sia.MaxUploadPrice / averageRate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gouge.MaxContractPrice, err = siacoinsFromFloat(p.config.Config().Core.Sia.MaxContractPrice / averageRate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gouge.MaxStoragePrice, err = siacoinsFromFloat(p.config.Config().Core.Sia.MaxStoragePrice / averageRate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gouge.MaxRPCPrice, err = siacoinsFromFloat(p.config.Config().Core.Sia.MaxRPCPrice / averageRate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.renter.UpdateGougingSettings(context.Background(), gouge)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PriceTracker) importPrices() error {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
// Query to count the number of historical records
|
||||||
|
err := p.db.Model(&models.SCPriceHistory{}).Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
p.logger.Error("failed to count historical records", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
daysOfHistory := p.config.Config().Core.Sia.PriceHistoryDays
|
||||||
|
|
||||||
|
// Check if the count is less than x
|
||||||
|
if uint64(count) < daysOfHistory {
|
||||||
|
// Calculate how many records need to be fetched and created
|
||||||
|
missingRecords := daysOfHistory - uint64(count)
|
||||||
|
for i := uint64(0); i < missingRecords; i++ {
|
||||||
|
currentDate := time.Now().UTC().AddDate(0, 0, int(-i))
|
||||||
|
timestamp := time.Date(currentDate.Year(), currentDate.Month(), currentDate.Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
// Fetch the historical exchange rate for the calculated timestamp
|
||||||
|
rates, err := p.api.GetHistoricalExchangeRate(timestamp)
|
||||||
|
if err != nil {
|
||||||
|
p.logger.Error("failed to fetch historical exchange rate", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assuming you want to store rates for a specific currency, say "USD"
|
||||||
|
rate, exists := rates[usdSymbol]
|
||||||
|
if !exists {
|
||||||
|
p.logger.Error("USD rate not found for timestamp", zap.String("timestamp", timestamp.String()))
|
||||||
|
return errors.New("USD rate not found for timestamp")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new record in the database for each fetched rate
|
||||||
|
priceRecord := &models.SCPriceHistory{
|
||||||
|
Rate: rate,
|
||||||
|
CreatedAt: timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.db.Create(&priceRecord).Error
|
||||||
|
if err != nil {
|
||||||
|
p.logger.Error("failed to create historical record", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PriceTrackerParams struct {
|
||||||
|
fx.In
|
||||||
|
Config *config.Manager
|
||||||
|
Logger *zap.Logger
|
||||||
|
Cron *cron.CronServiceDefault
|
||||||
|
Db *gorm.DB
|
||||||
|
Renter *RenterDefault
|
||||||
|
PriceApi *siasdk.APIClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PriceTracker) init() error {
|
||||||
|
p.cron.RegisterService(p)
|
||||||
|
p.api = apisdkgo.NewSiaClient()
|
||||||
|
|
||||||
|
err := p.importPrices()
|
||||||
|
if err != nil {
|
||||||
|
p.logger.Error("failed to import prices", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.updatePrices()
|
||||||
|
if err != nil {
|
||||||
|
p.logger.Error("failed to update prices", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPriceTracker(params PriceTrackerParams) *PriceTracker {
|
||||||
|
return &PriceTracker{
|
||||||
|
config: params.Config,
|
||||||
|
logger: params.Logger,
|
||||||
|
cron: params.Cron,
|
||||||
|
db: params.Db,
|
||||||
|
renter: params.Renter,
|
||||||
|
api: params.PriceApi,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func siacoinsFromFloat(f float64) (types.Currency, error) {
|
||||||
|
r := new(big.Rat).SetFloat64(f)
|
||||||
|
r.Mul(r, new(big.Rat).SetInt(types.HastingsPerSiacoin.Big()))
|
||||||
|
i := new(big.Int).Div(r.Num(), r.Denom())
|
||||||
|
if i.Sign() < 0 {
|
||||||
|
return types.ZeroCurrency, errors.New("value cannot be negative")
|
||||||
|
} else if i.BitLen() > 128 {
|
||||||
|
return types.ZeroCurrency, errors.New("value overflows Currency representation")
|
||||||
|
}
|
||||||
|
return types.NewCurrency(i.Uint64(), new(big.Int).Rsh(i, 64).Uint64()), nil
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"git.lumeweb.com/LumeWeb/portal/config"
|
"git.lumeweb.com/LumeWeb/portal/config"
|
||||||
|
|
||||||
"git.lumeweb.com/LumeWeb/portal/cron"
|
"git.lumeweb.com/LumeWeb/portal/cron"
|
||||||
|
sia "github.com/siacentral/apisdkgo"
|
||||||
rhpv2 "go.sia.tech/core/rhp/v2"
|
rhpv2 "go.sia.tech/core/rhp/v2"
|
||||||
"go.sia.tech/renterd/api"
|
"go.sia.tech/renterd/api"
|
||||||
busClient "go.sia.tech/renterd/bus/client"
|
busClient "go.sia.tech/renterd/bus/client"
|
||||||
|
@ -55,9 +56,14 @@ type MultiPartUploadParams struct {
|
||||||
var Module = fx.Module("renter",
|
var Module = fx.Module("renter",
|
||||||
fx.Options(
|
fx.Options(
|
||||||
fx.Provide(NewRenterService),
|
fx.Provide(NewRenterService),
|
||||||
|
fx.Provide(sia.NewSiaClient),
|
||||||
|
fx.Provide(NewPriceTracker),
|
||||||
fx.Invoke(func(r *RenterDefault) error {
|
fx.Invoke(func(r *RenterDefault) error {
|
||||||
return r.init()
|
return r.init()
|
||||||
}),
|
}),
|
||||||
|
fx.Invoke(func(r *PriceTracker) error {
|
||||||
|
return r.init()
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -257,3 +263,18 @@ func (r *RenterDefault) UploadObjectMultipart(ctx context.Context, params *Multi
|
||||||
func (r *RenterDefault) DeleteObject(ctx context.Context, bucket string, fileName string) error {
|
func (r *RenterDefault) DeleteObject(ctx context.Context, bucket string, fileName string) error {
|
||||||
return r.workerClient.DeleteObject(ctx, bucket, fileName, api.DeleteObjectOptions{})
|
return r.workerClient.DeleteObject(ctx, bucket, fileName, api.DeleteObjectOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RenterDefault) UpdateGougingSettings(ctx context.Context, settings api.GougingSettings) error {
|
||||||
|
return r.busClient.UpdateSetting(ctx, api.SettingGouging, settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RenterDefault) GougingSettings(ctx context.Context) (api.GougingSettings, error) {
|
||||||
|
var settings api.GougingSettings
|
||||||
|
err := r.GetSetting(ctx, api.SettingGouging, &settings)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return api.GougingSettings{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings, nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue