From c64f3f3132e4ed80efe0c96c8c766c41f9e89af1 Mon Sep 17 00:00:00 2001 From: Clement Sam Date: Tue, 9 May 2023 09:36:17 +0000 Subject: [PATCH] {storagenode/console,web/storagenode}: fetch pricing model from storagenode API Instead of the hardcoded payout rates that is assumed for all satellites, this change adds a new endpoint for fetching the pricing model for each satellite. The pricing model is then displayed on the Info & Estimation table on the dashboard Updates https://github.com/storj/storj-private/issues/245 Change-Id: Iac7669e3e6eb690bbaad6e64bbbe42dfd775f078 --- storagenode/console/consoleapi/storagenode.go | 32 +++++++++++++++++++ storagenode/console/consoleserver/server.go | 1 + storagenode/console/service.go | 12 +++++++ .../src/app/components/SNOHeader.vue | 6 ++++ .../components/SatelliteSelectionDropdown.vue | 6 ++++ .../components/payments/EstimationArea.vue | 23 +++++++++---- .../src/app/store/modules/payout.ts | 17 ++++++---- web/storagenode/src/app/types/payout.ts | 3 +- web/storagenode/src/app/views/PayoutArea.vue | 6 ++++ web/storagenode/src/storagenode/api/payout.ts | 26 ++++++++++++++- .../src/storagenode/payouts/payouts.ts | 24 ++++++++++++++ .../src/storagenode/payouts/service.ts | 9 ++++++ 12 files changed, 150 insertions(+), 15 deletions(-) diff --git a/storagenode/console/consoleapi/storagenode.go b/storagenode/console/consoleapi/storagenode.go index 9a8776416..590a714f2 100644 --- a/storagenode/console/consoleapi/storagenode.go +++ b/storagenode/console/consoleapi/storagenode.go @@ -155,6 +155,38 @@ func (dashboard *StorageNode) EstimatedPayout(w http.ResponseWriter, r *http.Req } } +// Pricing returns pricing model for specific satellite. +func (dashboard *StorageNode) Pricing(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var err error + defer mon.Task()(&ctx)(&err) + + w.Header().Set(contentType, applicationJSON) + + params := mux.Vars(r) + id, ok := params["id"] + if !ok { + dashboard.serveJSONError(w, http.StatusInternalServerError, ErrStorageNodeAPI.Wrap(err)) + return + } + satelliteID, err := storj.NodeIDFromString(id) + if err != nil { + dashboard.serveJSONError(w, http.StatusBadRequest, ErrStorageNodeAPI.Wrap(err)) + return + } + + data, err := dashboard.service.GetSatellitePricingModel(ctx, satelliteID) + if err != nil { + dashboard.serveJSONError(w, http.StatusInternalServerError, ErrStorageNodeAPI.Wrap(err)) + return + } + + if err := json.NewEncoder(w).Encode(data); err != nil { + dashboard.log.Error("failed to encode json response", zap.Error(ErrStorageNodeAPI.Wrap(err))) + return + } +} + // serveJSONError writes JSON error to response output stream. func (dashboard *StorageNode) serveJSONError(w http.ResponseWriter, status int, err error) { w.WriteHeader(status) diff --git a/storagenode/console/consoleserver/server.go b/storagenode/console/consoleserver/server.go index 3596c9c5a..5f1b3de91 100644 --- a/storagenode/console/consoleserver/server.go +++ b/storagenode/console/consoleserver/server.go @@ -72,6 +72,7 @@ func NewServer(logger *zap.Logger, assets fs.FS, notifications *notifications.Se storageNodeRouter.HandleFunc("/", storageNodeController.StorageNode).Methods(http.MethodGet) storageNodeRouter.HandleFunc("/satellites", storageNodeController.Satellites).Methods(http.MethodGet) storageNodeRouter.HandleFunc("/satellite/{id}", storageNodeController.Satellite).Methods(http.MethodGet) + storageNodeRouter.HandleFunc("/satellites/{id}/pricing", storageNodeController.Pricing).Methods(http.MethodGet) storageNodeRouter.HandleFunc("/estimated-payout", storageNodeController.EstimatedPayout).Methods(http.MethodGet) notificationController := consoleapi.NewNotifications(server.log, server.notifications) diff --git a/storagenode/console/service.go b/storagenode/console/service.go index 6f6a7b03d..a7097a20b 100644 --- a/storagenode/console/service.go +++ b/storagenode/console/service.go @@ -482,3 +482,15 @@ func (s *Service) VerifySatelliteID(ctx context.Context, satelliteID storj.NodeI return nil } + +// GetSatellitePricingModel returns pricing model for the specified satellite. +func (s *Service) GetSatellitePricingModel(ctx context.Context, satelliteID storj.NodeID) (pricingModel *pricing.Pricing, err error) { + defer mon.Task()(&ctx)(&err) + + pricingModel, err = s.pricingDB.Get(ctx, satelliteID) + if err != nil { + return nil, SNOServiceErr.Wrap(err) + } + + return pricingModel, nil +} diff --git a/web/storagenode/src/app/components/SNOHeader.vue b/web/storagenode/src/app/components/SNOHeader.vue index 48bd782d1..21bab1f59 100644 --- a/web/storagenode/src/app/components/SNOHeader.vue +++ b/web/storagenode/src/app/components/SNOHeader.vue @@ -187,6 +187,12 @@ export default class SNOHeader extends Vue { console.error(error); } + try { + await this.$store.dispatch(PAYOUT_ACTIONS.GET_PRICING_MODEL, selectedSatelliteId); + } catch (error) { + console.error(error); + } + await this.$store.dispatch(APPSTATE_ACTIONS.SET_LOADING, false); try { diff --git a/web/storagenode/src/app/components/SatelliteSelectionDropdown.vue b/web/storagenode/src/app/components/SatelliteSelectionDropdown.vue index 525e7a871..b3d2948fa 100644 --- a/web/storagenode/src/app/components/SatelliteSelectionDropdown.vue +++ b/web/storagenode/src/app/components/SatelliteSelectionDropdown.vue @@ -127,6 +127,12 @@ export default class SatelliteSelectionDropdown extends Vue { console.error(error); } + try { + await this.$store.dispatch(PAYOUT_ACTIONS.GET_PRICING_MODEL, id); + } catch (error) { + console.error(error); + } + try { await this.$store.dispatch(PAYOUT_ACTIONS.GET_TOTAL, id); } catch (error) { diff --git a/web/storagenode/src/app/components/payments/EstimationArea.vue b/web/storagenode/src/app/components/payments/EstimationArea.vue index 040554d58..db20208d2 100644 --- a/web/storagenode/src/app/components/payments/EstimationArea.vue +++ b/web/storagenode/src/app/components/payments/EstimationArea.vue @@ -146,9 +146,6 @@ import { Component, Vue } from 'vue-property-decorator'; import { APPSTATE_ACTIONS } from '@/app/store/modules/appState'; import { - BANDWIDTH_DOWNLOAD_PRICE_PER_TB, - BANDWIDTH_REPAIR_PRICE_PER_TB, - DISK_SPACE_PRICE_PER_TB, PAYOUT_ACTIONS, } from '@/app/store/modules/payout'; import { @@ -156,7 +153,12 @@ import { PayoutInfoRange, } from '@/app/types/payout'; import { Size } from '@/private/memory/size'; -import { EstimatedPayout, PayoutPeriod, TotalPaystubForPeriod } from '@/storagenode/payouts/payouts'; +import { + EstimatedPayout, + PayoutPeriod, + SatellitePricingModel, + TotalPaystubForPeriod, +} from '@/storagenode/payouts/payouts'; import EstimationPeriodDropdown from '@/app/components/payments/EstimationPeriodDropdown.vue'; @@ -263,6 +265,13 @@ export default class EstimationArea extends Vue { return this.$store.state.payoutModule.estimation; } + /** + * Returns satellite pricing model. + */ + public get pricing(): SatellitePricingModel { + return this.$store.state.payoutModule.pricingModel; + } + /** * Returns calculated or stored held amount. */ @@ -375,7 +384,7 @@ export default class EstimationArea extends Vue { new EstimationTableRow( 'Download', 'Egress', - `$${BANDWIDTH_DOWNLOAD_PRICE_PER_TB / 100} / TB`, + `$${this.pricing.egressBandwidth} / TB`, '--', Size.toBase10String(estimatedPayout.egressBandwidth), estimatedPayout.egressBandwidthPayout, @@ -383,7 +392,7 @@ export default class EstimationArea extends Vue { new EstimationTableRow( 'Repair & Audit', 'Egress', - `$${BANDWIDTH_REPAIR_PRICE_PER_TB / 100} / TB`, + `$${this.pricing.repairBandwidth} / TB`, '--', Size.toBase10String(estimatedPayout.egressRepairAudit), estimatedPayout.egressRepairAuditPayout, @@ -391,7 +400,7 @@ export default class EstimationArea extends Vue { new EstimationTableRow( 'Disk Average Month', 'Storage', - `$${DISK_SPACE_PRICE_PER_TB / 100} / TBm`, + `$${this.pricing.diskSpace} / TBm`, Size.toBase10String(estimatedPayout.diskSpace) + 'm', '--', estimatedPayout.diskSpacePayout, diff --git a/web/storagenode/src/app/store/modules/payout.ts b/web/storagenode/src/app/store/modules/payout.ts index 95e3c445e..140e768f4 100644 --- a/web/storagenode/src/app/store/modules/payout.ts +++ b/web/storagenode/src/app/store/modules/payout.ts @@ -12,13 +12,14 @@ import { EstimatedPayout, PayoutPeriod, SatelliteHeldHistory, - SatellitePayoutForPeriod, + SatellitePayoutForPeriod, SatellitePricingModel, TotalPayments, TotalPaystubForPeriod, } from '@/storagenode/payouts/payouts'; import { PayoutService } from '@/storagenode/payouts/service'; export const PAYOUT_MUTATIONS = { + SET_PRICING_MODEL: 'SET_PRICING_MODEL', SET_PAYOUT_INFO: 'SET_PAYOUT_INFO', SET_RANGE: 'SET_RANGE', SET_TOTAL: 'SET_TOTAL', @@ -32,6 +33,7 @@ export const PAYOUT_MUTATIONS = { }; export const PAYOUT_ACTIONS = { + GET_PRICING_MODEL: 'GET_PRICING_MODEL', GET_PAYOUT_INFO: 'GET_PAYOUT_INFO', SET_PERIODS_RANGE: 'SET_PERIODS_RANGE', GET_TOTAL: 'GET_TOTAL', @@ -42,11 +44,6 @@ export const PAYOUT_ACTIONS = { SET_PAYOUT_HISTORY_PERIOD: 'SET_PAYOUT_HISTORY_PERIOD', }; -// TODO: move to config in storagenode/payouts -export const BANDWIDTH_DOWNLOAD_PRICE_PER_TB = 2000; -export const BANDWIDTH_REPAIR_PRICE_PER_TB = 1000; -export const DISK_SPACE_PRICE_PER_TB = 150; - interface PayoutContext { rootState: { node: StorageNodeState; @@ -83,6 +80,9 @@ export function newPayoutModule(service: PayoutService): StoreModule { + const pricing = await service.pricingModel(satelliteId); + + commit(PAYOUT_MUTATIONS.SET_PRICING_MODEL, pricing); + }, [PAYOUT_ACTIONS.GET_PAYOUT_HISTORY]: async function ({ commit, state }: PayoutContext): Promise { if (!state.payoutHistoryPeriod) return; diff --git a/web/storagenode/src/app/types/payout.ts b/web/storagenode/src/app/types/payout.ts index 350806400..dae4a6878 100644 --- a/web/storagenode/src/app/types/payout.ts +++ b/web/storagenode/src/app/types/payout.ts @@ -5,7 +5,7 @@ import { EstimatedPayout, PayoutPeriod, SatelliteHeldHistory, - SatellitePayoutForPeriod, + SatellitePayoutForPeriod, SatellitePricingModel, TotalPayments, TotalPaystubForPeriod, } from '@/storagenode/payouts/payouts'; @@ -36,6 +36,7 @@ export class PayoutState { public payoutHistoryPeriod: string = '', public estimation: EstimatedPayout = new EstimatedPayout(), public payoutHistoryAvailablePeriods: PayoutPeriod[] = [], + public pricingModel: SatellitePricingModel = new SatellitePricingModel(), ) {} } diff --git a/web/storagenode/src/app/views/PayoutArea.vue b/web/storagenode/src/app/views/PayoutArea.vue index e831b1e52..aa24f9535 100644 --- a/web/storagenode/src/app/views/PayoutArea.vue +++ b/web/storagenode/src/app/views/PayoutArea.vue @@ -104,6 +104,12 @@ export default class PayoutArea extends Vue { console.error(error); } + try { + await this.$store.dispatch(PAYOUT_ACTIONS.GET_PRICING_MODEL, this.$store.state.node.selectedSatellite.id); + } catch (error) { + console.error(error); + } + try { await this.$store.dispatch(PAYOUT_ACTIONS.GET_TOTAL); } catch (error) { diff --git a/web/storagenode/src/storagenode/api/payout.ts b/web/storagenode/src/storagenode/api/payout.ts index 727f89da5..21baa6400 100644 --- a/web/storagenode/src/storagenode/api/payout.ts +++ b/web/storagenode/src/storagenode/api/payout.ts @@ -9,7 +9,7 @@ import { Paystub, PreviousMonthEstimatedPayout, SatelliteHeldHistory, - SatellitePayoutForPeriod, + SatellitePayoutForPeriod, SatellitePricingModel, } from '@/storagenode/payouts/payouts'; import { HttpClient } from '@/storagenode/utils/httpClient'; @@ -214,4 +214,28 @@ export class PayoutHttpApi implements PayoutApi { data.currentMonthExpectations, ); } + + public async getPricingModel(satelliteId: string): Promise { + if (!satelliteId) { + return new SatellitePricingModel(); + } + + const path = '/api/sno/satellites/'+ satelliteId +'/pricing'; + + const response = await this.client.get(path); + + if (!response.ok) { + throw new Error('can not get satellite pricing information'); + } + + const data: any = await response.json() || new SatellitePricingModel(); // eslint-disable-line @typescript-eslint/no-explicit-any + + return new SatellitePricingModel( + data.satelliteID, + data.egressBandwidth, + data.repairBandwidth, + data.auditBandwidth, + data.diskSpace, + ); + } } diff --git a/web/storagenode/src/storagenode/payouts/payouts.ts b/web/storagenode/src/storagenode/payouts/payouts.ts index 9e509f1f5..c14820b5a 100644 --- a/web/storagenode/src/storagenode/payouts/payouts.ts +++ b/web/storagenode/src/storagenode/payouts/payouts.ts @@ -31,6 +31,12 @@ export interface PayoutApi { */ getEstimatedPayout(satelliteId: string): Promise; + /** + * Fetch satellite payout rate. + * @throws Error + */ + getPricingModel(satelliteId: string): Promise; + /** * Fetches payout history for all satellites. * @throws Error @@ -320,3 +326,21 @@ export class SatellitePayoutForPeriod { return value / PRICE_DIVIDER; } } + +/** + * Contains satellite payout rates. + */ +export class SatellitePricingModel { + public constructor( + public satelliteID: string = '', + public egressBandwidth: number = 0, + public repairBandwidth: number = 0, + public auditBandwidth: number = 0, + public diskSpace: number = 0, + ) { + this.egressBandwidth = this.egressBandwidth / 100; + this.repairBandwidth = this.repairBandwidth / 100; + this.auditBandwidth = this.auditBandwidth / 100; + this.diskSpace = this.diskSpace / 100; + } +} diff --git a/web/storagenode/src/storagenode/payouts/service.ts b/web/storagenode/src/storagenode/payouts/service.ts index deca3a99f..4e96bf3f6 100644 --- a/web/storagenode/src/storagenode/payouts/service.ts +++ b/web/storagenode/src/storagenode/payouts/service.ts @@ -9,6 +9,7 @@ import { Paystub, SatelliteHeldHistory, SatellitePayoutForPeriod, + SatellitePricingModel, TotalPayments, TotalPaystubForPeriod, } from '@/storagenode/payouts/payouts'; @@ -79,4 +80,12 @@ export class PayoutService { public async estimatedPayout(satelliteId: string): Promise { return await this.payouts.getEstimatedPayout(satelliteId); } + + /** + * Gets satellite pricing model. + * @param satelliteId + */ + public async pricingModel(satelliteId: string): Promise { + return await this.payouts.getPricingModel(satelliteId); + } }