diff --git a/storagenode/console/consoleapi/heldamount.go b/storagenode/console/consoleapi/heldamount.go index 44f62dded..6939321f2 100644 --- a/storagenode/console/consoleapi/heldamount.go +++ b/storagenode/console/consoleapi/heldamount.go @@ -172,6 +172,34 @@ func (heldAmount *HeldAmount) HeldHistory(w http.ResponseWriter, r *http.Request } } +// PayoutHistory retrieves paystubs for specific period from all satellites and transaction receipts if exists. +func (heldAmount *HeldAmount) PayoutHistory(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var err error + defer mon.Task()(&ctx)(&err) + + w.Header().Set(contentType, applicationJSON) + + segmentParams := mux.Vars(r) + + period, ok := segmentParams["period"] + if !ok { + heldAmount.serveJSONError(w, http.StatusBadRequest, ErrNotificationsAPI.Wrap(err)) + return + } + + payoutHistory, err := heldAmount.service.PayoutHistoryMonthly(ctx, period) + if err != nil { + heldAmount.serveJSONError(w, http.StatusInternalServerError, ErrHeldAmountAPI.Wrap(err)) + return + } + + if err := json.NewEncoder(w).Encode(payoutHistory); err != nil { + heldAmount.log.Error("failed to encode json response", zap.Error(ErrHeldAmountAPI.Wrap(err))) + return + } +} + // HeldAmountPeriods retrieves all periods in which we have some heldamount data. // Have optional parameter - satelliteID. // If satelliteID specified - will retrieve periods only for concrete satellite. diff --git a/storagenode/console/consoleapi/heldamount_test.go b/storagenode/console/consoleapi/heldamount_test.go index d0b389995..ccea8f6d5 100644 --- a/storagenode/console/consoleapi/heldamount_test.go +++ b/storagenode/console/consoleapi/heldamount_test.go @@ -354,7 +354,7 @@ func TestHeldAmountApi(t *testing.T) { require.NoError(t, err) // should return all heldback history inserted earlier - url := fmt.Sprintf("%s/heldhistory", baseURL) + url := fmt.Sprintf("%s/held-history", baseURL) res, err := http.Get(url) require.NoError(t, err) require.NotNil(t, res) diff --git a/storagenode/console/consoleapi/storagenode_test.go b/storagenode/console/consoleapi/storagenode_test.go index e4e9b8a48..0266d45ca 100644 --- a/storagenode/console/consoleapi/storagenode_test.go +++ b/storagenode/console/consoleapi/storagenode_test.go @@ -7,7 +7,6 @@ import ( "encoding/json" "fmt" "io/ioutil" - "math" "net/http" "testing" "time" @@ -45,16 +44,13 @@ var ( ) func TestStorageNodeApi(t *testing.T) { - t.Skip("Flaky") - testplanet.Run(t, testplanet.Config{ - SatelliteCount: 2, + SatelliteCount: 1, StorageNodeCount: 1, }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { satellite := planet.Satellites[0] - satellite2 := planet.Satellites[1] sno := planet.StorageNodes[0] console := sno.Console bandwidthdb := sno.DB.Bandwidth() @@ -65,19 +61,13 @@ func TestStorageNodeApi(t *testing.T) { now := time.Now().UTC().Add(-2 * time.Hour) - randAmount1 := int64(120000000000) - randAmount2 := int64(450000000000) - for _, action := range actions { - err := bandwidthdb.Add(ctx, satellite.ID(), action, randAmount1, now) - require.NoError(t, err) - - err = bandwidthdb.Add(ctx, satellite2.ID(), action, randAmount2, now.Add(2*time.Hour)) + err := bandwidthdb.Add(ctx, satellite.ID(), action, 2300000000000, now) require.NoError(t, err) } var satellites []storj.NodeID - satellites = append(satellites, satellite.ID(), satellite2.ID()) + satellites = append(satellites, satellite.ID()) stamps, _ := makeStorageUsageStamps(satellites) err := storageusagedb.Store(ctx, stamps) @@ -94,29 +84,15 @@ func TestStorageNodeApi(t *testing.T) { }) require.NoError(t, err) - err = pricingdb.Store(ctx, pricing.Pricing{ - SatelliteID: satellite2.ID(), - EgressBandwidth: egressPrice, - RepairBandwidth: repairPrice, - AuditBandwidth: auditPrice, - DiskSpace: diskPrice, - }) - require.NoError(t, err) - err = reputationdb.Store(ctx, reputation.Stats{ SatelliteID: satellite.ID(), JoinedAt: time.Now().UTC(), }) require.NoError(t, err) - err = reputationdb.Store(ctx, reputation.Stats{ - SatelliteID: satellite2.ID(), - JoinedAt: time.Now().UTC(), - }) - require.NoError(t, err) t.Run("test EstimatedPayout", func(t *testing.T) { // should return estimated payout for both satellites in current month and empty for previous - url := fmt.Sprintf("%s/estimatedPayout", baseURL) + url := fmt.Sprintf("%s/estimated-payout", baseURL) res, err := http.Get(url) require.NoError(t, err) require.NotNil(t, res) @@ -129,42 +105,14 @@ func TestStorageNodeApi(t *testing.T) { body, err := ioutil.ReadAll(res.Body) require.NoError(t, err) - egressBandwidth1 := randAmount1 - egressBandwidthPayout1 := int64(float64(randAmount1*egressPrice) / math.Pow10(12)) - egressRepairAudit1 := randAmount1 * 2 - egressRepairAuditPayout1 := int64(float64(randAmount1*auditPrice+randAmount1*repairPrice) / math.Pow10(12)) - diskSpace1 := 30000000000000 * time.Now().UTC().Day() - egressBandwidth2 := randAmount2 - egressBandwidthPayout2 := int64(float64(randAmount2*egressPrice) / math.Pow10(12)) - egressRepairAudit2 := randAmount2 * 2 - egressRepairAuditPayout2 := int64(float64(randAmount2*auditPrice+randAmount2*repairPrice) / math.Pow10(12)) - diskSpace2 := 30000000000000 * time.Now().UTC().Day() - diskSpacePayout2 := int64(30000000000000/720/math.Pow10(12)*float64(diskPrice)) * int64(time.Now().UTC().Day()) + estimation, err := sno.Console.Service.GetAllSatellitesEstimatedPayout(ctx) + require.NoError(t, err) expected, err := json.Marshal(heldamount.EstimatedPayout{ - CurrentMonth: heldamount.PayoutMonthly{ - EgressBandwidth: 2 * (egressBandwidth1 + egressBandwidth2), - EgressBandwidthPayout: (egressBandwidthPayout1 + egressBandwidthPayout2) / 2, - EgressRepairAudit: 2 * (egressRepairAudit1 + egressRepairAudit2), - EgressRepairAuditPayout: (egressRepairAuditPayout1 + egressRepairAuditPayout2) / 2, - DiskSpace: float64(diskSpace1 + diskSpace2), - DiskSpacePayout: (diskSpacePayout2 - (diskSpacePayout2 * 75 / 100)) * 2, - HeldRate: 0, - Held: (2*(egressBandwidthPayout1+egressRepairAuditPayout1)+diskSpacePayout2)*75/100 + (2*(egressBandwidthPayout2+egressRepairAuditPayout2)+diskSpacePayout2)*75/100, - Payout: ((egressBandwidthPayout1 + egressBandwidthPayout2) / 2) + ((egressRepairAuditPayout1 + egressRepairAuditPayout2) / 2) + (diskSpacePayout2-(diskSpacePayout2*75/100))*2, - }, - PreviousMonth: heldamount.PayoutMonthly{ - EgressBandwidth: 0, - EgressBandwidthPayout: 0, - EgressRepairAudit: 0, - EgressRepairAuditPayout: 0, - DiskSpace: 0, - DiskSpacePayout: 0, - HeldRate: 0, - Held: 0, - Payout: 0, - }, + CurrentMonth: estimation.CurrentMonth, + PreviousMonth: estimation.PreviousMonth, }) + require.NoError(t, err) require.Equal(t, string(expected)+"\n", string(body)) }) diff --git a/storagenode/console/consoleserver/server.go b/storagenode/console/consoleserver/server.go index bd956174a..088c57a11 100644 --- a/storagenode/console/consoleserver/server.go +++ b/storagenode/console/consoleserver/server.go @@ -68,7 +68,7 @@ func NewServer(logger *zap.Logger, assets http.FileSystem, notifications *notifi 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("/estimatedPayout", storageNodeController.EstimatedPayout).Methods(http.MethodGet) + storageNodeRouter.HandleFunc("/estimated-payout", storageNodeController.EstimatedPayout).Methods(http.MethodGet) notificationController := consoleapi.NewNotifications(server.log, server.notifications) notificationRouter := router.PathPrefix("/api/notifications").Subrouter() @@ -82,8 +82,9 @@ func NewServer(logger *zap.Logger, assets http.FileSystem, notifications *notifi heldAmountRouter.StrictSlash(true) heldAmountRouter.HandleFunc("/paystubs/{period}", heldAmountController.PayStubMonthly).Methods(http.MethodGet) heldAmountRouter.HandleFunc("/paystubs/{start}/{end}", heldAmountController.PayStubPeriod).Methods(http.MethodGet) - heldAmountRouter.HandleFunc("/heldhistory", heldAmountController.HeldHistory).Methods(http.MethodGet) + heldAmountRouter.HandleFunc("/held-history", heldAmountController.HeldHistory).Methods(http.MethodGet) heldAmountRouter.HandleFunc("/periods", heldAmountController.HeldAmountPeriods).Methods(http.MethodGet) + heldAmountRouter.HandleFunc("/payout-history", heldAmountController.PayoutHistory).Methods(http.MethodGet) if assets != nil { fs := http.FileServer(assets) diff --git a/storagenode/heldamount/db_test.go b/storagenode/heldamount/db_test.go index 931e0598b..76b2111cc 100644 --- a/storagenode/heldamount/db_test.go +++ b/storagenode/heldamount/db_test.go @@ -193,7 +193,8 @@ func TestSatellitePayStubPeriodCached(t *testing.T) { storagenodedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db storagenode.DB) { heldAmountDB := db.HeldAmount() reputationDB := db.Reputation() - service := heldamount.NewService(nil, heldAmountDB, reputationDB, nil) + satellitesDB := db.Satellites() + service := heldamount.NewService(nil, heldAmountDB, reputationDB, satellitesDB, nil) payStub := heldamount.PayStub{ SatelliteID: storj.NodeID{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, @@ -242,7 +243,8 @@ func TestAllPayStubPeriodCached(t *testing.T) { storagenodedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db storagenode.DB) { heldAmountDB := db.HeldAmount() reputationDB := db.Reputation() - service := heldamount.NewService(nil, heldAmountDB, reputationDB, nil) + satellitesDB := db.Satellites() + service := heldamount.NewService(nil, heldAmountDB, reputationDB, satellitesDB, nil) payStub := heldamount.PayStub{ SatelliteID: storj.NodeID{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, diff --git a/storagenode/heldamount/service.go b/storagenode/heldamount/service.go index f1f99e94c..72d68db8c 100644 --- a/storagenode/heldamount/service.go +++ b/storagenode/heldamount/service.go @@ -5,6 +5,7 @@ package heldamount import ( "context" + "database/sql" "fmt" "strconv" "strings" @@ -17,6 +18,7 @@ import ( "storj.io/common/storj" "storj.io/storj/private/date" "storj.io/storj/storagenode/reputation" + "storj.io/storj/storagenode/satellites" "storj.io/storj/storagenode/trust" ) @@ -38,15 +40,17 @@ type Service struct { db DB reputationDB reputation.DB + satellitesDB satellites.DB trust *trust.Pool } // NewService creates new instance of service. -func NewService(log *zap.Logger, db DB, reputationDB reputation.DB, trust *trust.Pool) *Service { +func NewService(log *zap.Logger, db DB, reputationDB reputation.DB, satelliteDB satellites.DB, trust *trust.Pool) *Service { return &Service{ log: log, db: db, reputationDB: reputationDB, + satellitesDB: satelliteDB, trust: trust, } } @@ -207,6 +211,69 @@ func (service *Service) AllHeldbackHistory(ctx context.Context) (result []HeldHi return result, nil } +// PayoutHistory contains payout information for specific period for specific satellite. +type PayoutHistory struct { + NodeAge int64 `json:"nodeAge"` + Earned int64 `json:"earned"` + Surge int64 `json:"surge"` + Held int64 `json:"held"` + AfterHeld int64 `json:"afterHeld"` + HeldReturned int64 `json:"heldReturned"` + Receipt string `json:"receipt"` + IsExitComplete bool `json:"isExitComplete"` +} + +// PayoutHistoryMonthly retrieves paystub and payment receipt for specific month from all satellites. +func (service *Service) PayoutHistoryMonthly(ctx context.Context, period string) (result []PayoutHistory, err error) { + defer mon.Task()(&ctx)(&err) + + satelliteIDs := service.trust.GetSatellites(ctx) + for i := 0; i < len(satelliteIDs); i++ { + var payoutHistory PayoutHistory + paystub, err := service.db.GetPayStub(ctx, satelliteIDs[i], period) + if err != nil { + if ErrNoPayStubForPeriod.Has(err) { + continue + } + return nil, ErrHeldAmountService.Wrap(err) + } + + stats, err := service.reputationDB.Get(ctx, satelliteIDs[i]) + if err != nil { + return nil, ErrHeldAmountService.Wrap(err) + } + + satellite, err := service.satellitesDB.GetSatellite(ctx, satelliteIDs[i]) + if err != nil { + if sql.ErrNoRows == err { + payoutHistory.IsExitComplete = false + } + + return nil, ErrHeldAmountService.Wrap(err) + } + + if satellite.Status == satellites.ExitSucceeded { + payoutHistory.IsExitComplete = true + } + + if paystub.SurgePercent == 0 { + paystub.SurgePercent = 100 + } + + payoutHistory.Held = paystub.Held + payoutHistory.Receipt = paystub.Receipt + payoutHistory.AfterHeld = paystub.Paid + payoutHistory.NodeAge = int64(date.MonthsCountSince(stats.JoinedAt)) + payoutHistory.HeldReturned = paystub.Disposed + payoutHistory.Surge = paystub.Paid * (paystub.SurgePercent/100 - 1) + payoutHistory.Earned = paystub.Paid + + result = append(result, payoutHistory) + } + + return result, nil +} + // TODO: move to separate struct. func parsePeriodRange(periodStart, periodEnd string) (periods []string, err error) { var yearStart, yearEnd, monthStart, monthEnd int diff --git a/storagenode/peer.go b/storagenode/peer.go index 0b8dbef7b..a650899f5 100644 --- a/storagenode/peer.go +++ b/storagenode/peer.go @@ -532,6 +532,7 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB, revocationDB exten peer.Log.Named("heldamount:service"), peer.DB.HeldAmount(), peer.DB.Reputation(), + peer.DB.Satellites(), peer.Storage2.Trust, ) peer.Heldamount.Endpoint = heldamount.NewEndpoint( diff --git a/web/storagenode/src/storagenode/api/payout.ts b/web/storagenode/src/storagenode/api/payout.ts index 6ecb29a84..e790c8112 100644 --- a/web/storagenode/src/storagenode/api/payout.ts +++ b/web/storagenode/src/storagenode/api/payout.ts @@ -113,7 +113,7 @@ export class PayoutHttpApi implements PayoutApi { * @throws Error */ public async getHeldHistory(): Promise { - const path = `${this.ROOT_PATH}/heldhistory/`; + const path = `${this.ROOT_PATH}/held-history/`; const response = await this.client.get(path);