Skip to content

Commit

Permalink
Allow pinning price settings to an underlying currency (#1294)
Browse files Browse the repository at this point in the history
This PR adds a `pin` manager to the bus that undertakes action depending
on the user's `pricepinning` settings. Those include pins for gouging
settings as well as autopilot settings. When a setting is pinned, it's
pinned to a fixed value in a certain underlying currency defined in the
settings. `renterd` will periodically fetch the exchange rate for `SC`
against this underlying currency and update the prices accordingly, once
the exchange rate fluctuates over a configured threshold.

`NOTE` I'm sure there's some parts that aren't 100% correct now, I
figured maybe we can build the UI side of the feature so we can test it
using the UI rather than doing it manually. I've never configured pinned
prices on a host so I'm not quite sure how that happens. Do we change
the way we express the various cost settings? E.g. `MaxContractPrice`
for instance, that's 5SC but we probably want that to change along with
price fluctuations but we can't really express 5SC in USD.

---------

Co-authored-by: Chris Schinnerl <[email protected]>
  • Loading branch information
peterjan and ChrisSchinnerl authored Jun 21, 2024
1 parent a82d3d3 commit f87d122
Show file tree
Hide file tree
Showing 22 changed files with 1,107 additions and 191 deletions.
80 changes: 44 additions & 36 deletions api/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,53 +62,61 @@ type (
}
)

func (e EventConsensusUpdate) Event() webhooks.Event {
return webhooks.Event{
Module: ModuleConsensus,
Event: EventUpdate,
Payload: e,
var (
WebhookConsensusUpdate = func(url string, headers map[string]string) webhooks.Webhook {
return webhooks.Webhook{
Event: EventUpdate,
Headers: headers,
Module: ModuleConsensus,
URL: url,
}
}
}

func (e EventContractArchive) Event() webhooks.Event {
return webhooks.Event{
Module: ModuleContract,
Event: EventArchive,
Payload: e,
WebhookContractArchive = func(url string, headers map[string]string) webhooks.Webhook {
return webhooks.Webhook{
Event: EventArchive,
Headers: headers,
Module: ModuleContract,
URL: url,
}
}
}

func (e EventContractRenew) Event() webhooks.Event {
return webhooks.Event{
Module: ModuleContract,
Event: EventRenew,
Payload: e,
WebhookContractRenew = func(url string, headers map[string]string) webhooks.Webhook {
return webhooks.Webhook{
Event: EventRenew,
Headers: headers,
Module: ModuleContract,
URL: url,
}
}
}

func (e EventContractSetUpdate) Event() webhooks.Event {
return webhooks.Event{
Module: ModuleContractSet,
Event: EventUpdate,
Payload: e,
WebhookContractSetUpdate = func(url string, headers map[string]string) webhooks.Webhook {
return webhooks.Webhook{
Event: EventUpdate,
Headers: headers,
Module: ModuleContractSet,
URL: url,
}
}
}

func (e EventSettingUpdate) Event() webhooks.Event {
return webhooks.Event{
Module: ModuleSetting,
Event: EventUpdate,
Payload: e,
WebhookSettingUpdate = func(url string, headers map[string]string) webhooks.Webhook {
return webhooks.Webhook{
Event: EventUpdate,
Headers: headers,
Module: ModuleSetting,
URL: url,
}
}
}

func (e EventSettingDelete) Event() webhooks.Event {
return webhooks.Event{
Module: ModuleSetting,
Event: EventDelete,
Payload: e,
WebhookSettingDelete = func(url string, headers map[string]string) webhooks.Webhook {
return webhooks.Webhook{
Event: EventDelete,
Headers: headers,
Module: ModuleSetting,
URL: url,
}
}
}
)

func ParseEventWebhook(event webhooks.Event) (interface{}, error) {
bytes, err := json.Marshal(event.Payload)
Expand Down
72 changes: 72 additions & 0 deletions api/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
const (
SettingContractSet = "contractset"
SettingGouging = "gouging"
SettingPricePinning = "pricepinning"
SettingRedundancy = "redundancy"
SettingS3Authentication = "s3authentication"
SettingUploadPacking = "uploadpacking"
Expand Down Expand Up @@ -80,6 +81,55 @@ type (
MigrationSurchargeMultiplier uint64 `json:"migrationSurchargeMultiplier"`
}

// PricePinSettings holds the configuration for pinning certain settings to
// a specific currency (e.g., USD). It uses a Forex API to fetch the current
// exchange rate, allowing users to set prices in USD instead of SC.
PricePinSettings struct {
// Enabled can be used to either enable or temporarily disable price
// pinning. If enabled, both the currency and the Forex endpoint URL
// must be valid.
Enabled bool `json:"enabled"`

// Currency is the external three-letter currency code.
Currency string `json:"currency"`

// ForexEndpointURL is the endpoint that returns the exchange rate for
// Siacoin against the underlying currency.
ForexEndpointURL string `json:"forexEndpointURL"`

// Threshold is a percentage between 0 and 1 that determines when the
// pinned settings are updated based on the exchange rate at the time.
Threshold float64 `json:"threshold"`

// Autopilots contains the pinned settings for every autopilot.
Autopilots map[string]AutopilotPins `json:"autopilots,omitempty"`

// GougingSettingsPins contains the pinned settings for the gouging
// settings.
GougingSettingsPins GougingSettingsPins `json:"gougingSettingsPins,omitempty"`
}

// AutopilotPins contains the available autopilot settings that can be
// pinned.
AutopilotPins struct {
Allowance Pin `json:"allowance"`
}

// GougingSettingsPins contains the available gouging settings that can be
// pinned.
GougingSettingsPins struct {
MaxDownload Pin `json:"maxDownload"`
MaxRPCPrice Pin `json:"maxRPCPrice"`
MaxStorage Pin `json:"maxStorage"`
MaxUpload Pin `json:"maxUpload"`
}

// A Pin is a pinned price in an external currency.
Pin struct {
Pinned bool `json:"pinned"`
Value float64 `json:"value"`
}

// RedundancySettings contain settings that dictate an object's redundancy.
RedundancySettings struct {
MinShards int `json:"minShards"`
Expand All @@ -98,6 +148,28 @@ type (
}
)

// IsPinned returns true if the pin is enabled and the value is greater than 0.
func (p Pin) IsPinned() bool {
return p.Pinned && p.Value > 0
}

// Validate returns an error if the price pin settings are not considered valid.
func (pps PricePinSettings) Validate() error {
if !pps.Enabled {
return nil
}
if pps.ForexEndpointURL == "" {
return fmt.Errorf("price pin settings must have a forex endpoint URL")
}
if pps.Currency == "" {
return fmt.Errorf("price pin settings must have a currency")
}
if pps.Threshold <= 0 || pps.Threshold >= 1 {
return fmt.Errorf("price pin settings must have a threshold between 0 and 1")
}
return nil
}

// Validate returns an error if the gouging settings are not considered valid.
func (gs GougingSettings) Validate() error {
if gs.HostBlockHeightLeeway < 3 {
Expand Down
7 changes: 7 additions & 0 deletions build/env_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ var (
MigrationSurchargeMultiplier: 10, // 10x
}

// DefaultPricePinSettings define the default price pin settings the bus is
// configured with on startup. These values can be adjusted using the
// settings API.
DefaultPricePinSettings = api.PricePinSettings{
Enabled: false,
}

// DefaultUploadPackingSettings define the default upload packing settings
// the bus is configured with on startup.
DefaultUploadPackingSettings = api.UploadPackingSettings{
Expand Down
7 changes: 7 additions & 0 deletions build/env_testnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ var (
MigrationSurchargeMultiplier: 10, // 10x
}

// DefaultPricePinSettings define the default price pin settings the bus is
// configured with on startup. These values can be adjusted using the
// settings API.
DefaultPricePinSettings = api.PricePinSettings{
Enabled: false,
}

// DefaultUploadPackingSettings define the default upload packing settings
// the bus is configured with on startup.
DefaultUploadPackingSettings = api.UploadPackingSettings{
Expand Down
Loading

0 comments on commit f87d122

Please sign in to comment.