diff --git a/gui/account.go b/gui/account.go index 7b45d805..337ca5a2 100644 --- a/gui/account.go +++ b/gui/account.go @@ -14,14 +14,16 @@ import ( // accountPageData contains all of the necessary information to render the // account template. type accountPageData struct { - HeaderData headerData - MinedWork []*minedWork - ArchivedPayments []*archivedPayment - PendingPayments []*pendingPayment - ConnectedClients []*client - AccountID string - Address string - BlockExplorerURL string + HeaderData headerData + MinedWork []*minedWork + ArchivedPaymentsTotal string + ArchivedPayments []*archivedPayment + PendingPaymentsTotal string + PendingPayments []*pendingPayment + ConnectedClients []*client + AccountID string + Address string + BlockExplorerURL string } // account is the handler for "GET /account". Renders the account template if @@ -43,6 +45,12 @@ func (ui *GUI) account(w http.ResponseWriter, r *http.Request) { return } + totalPending := ui.cache.getPendingPaymentsTotal(accountID) + totalArchived := ui.cache.getArchivedPaymentsTotal(accountID) + + // We don't need to handle errors on the following cache access because we + // are passing hard-coded, good params. + // Get the 10 most recently mined blocks by this account. _, recentWork, _ := ui.cache.getMinedWorkByAccount(0, 9, accountID) @@ -61,13 +69,15 @@ func (ui *GUI) account(w http.ResponseWriter, r *http.Request) { Designation: ui.cfg.Designation, ShowMenu: true, }, - MinedWork: recentWork, - PendingPayments: pendingPmts, - ArchivedPayments: archivedPmts, - ConnectedClients: clients, - AccountID: accountID, - Address: address, - BlockExplorerURL: ui.cfg.BlockExplorerURL, + MinedWork: recentWork, + PendingPaymentsTotal: totalPending, + PendingPayments: pendingPmts, + ArchivedPaymentsTotal: totalArchived, + ArchivedPayments: archivedPmts, + ConnectedClients: clients, + AccountID: accountID, + Address: address, + BlockExplorerURL: ui.cfg.BlockExplorerURL, } ui.renderTemplate(w, "account", data) diff --git a/gui/admin.go b/gui/admin.go index 65fe0b6c..c2488b8e 100644 --- a/gui/admin.go +++ b/gui/admin.go @@ -7,6 +7,7 @@ package gui import ( "net/http" + "github.com/decred/dcrpool/pool" "github.com/gorilla/csrf" "github.com/gorilla/sessions" ) @@ -14,9 +15,13 @@ import ( // adminPageData contains all of the necessary information to render the admin // template. type adminPageData struct { - HeaderData headerData - PoolStatsData poolStatsData - ConnectedClients map[string][]*client + HeaderData headerData + PoolStatsData poolStatsData + ConnectedClients map[string][]*client + ArchivedPaymentsTotal string + ArchivedPayments []*archivedPayment + PendingPaymentsTotal string + PendingPayments []*pendingPayment } // adminPage is the handler for "GET /admin". If the current session is @@ -32,6 +37,18 @@ func (ui *GUI) adminPage(w http.ResponseWriter, r *http.Request) { clients := ui.cache.getClients() + totalPending := ui.cache.getPendingPaymentsTotal(pool.PoolFeesK) + totalArchived := ui.cache.getArchivedPaymentsTotal(pool.PoolFeesK) + + // We don't need to handle errors on the following cache access because we + // are passing hard-coded, good params. + + // Get the 10 most recent pending payments. + _, pendingPmts, _ := ui.cache.getPendingPayments(0, 9, pool.PoolFeesK) + + // Get the 10 most recent archived payments. + _, archivedPmts, _ := ui.cache.getArchivedPayments(0, 9, pool.PoolFeesK) + pageData := adminPageData{ HeaderData: headerData{ CSRF: csrf.TemplateField(r), @@ -47,7 +64,11 @@ func (ui *GUI) adminPage(w http.ResponseWriter, r *http.Request) { PoolFee: ui.cfg.PoolFee, SoloPool: ui.cfg.SoloPool, }, - ConnectedClients: clients, + ConnectedClients: clients, + PendingPaymentsTotal: totalPending, + PendingPayments: pendingPmts, + ArchivedPaymentsTotal: totalArchived, + ArchivedPayments: archivedPmts, } ui.renderTemplate(w, "admin", pageData) diff --git a/gui/assets/public/css/dcrpool.css b/gui/assets/public/css/dcrpool.css index b36550dc..66bcf4e0 100644 --- a/gui/assets/public/css/dcrpool.css +++ b/gui/assets/public/css/dcrpool.css @@ -8781,6 +8781,11 @@ table td { padding-bottom: 10px; } +.block__content h2 { + color: #3D5873; + font-size: 22px; +} + .dcr-label { background: #D3F0FD 0% 0% no-repeat padding-box; border-radius: 3px; diff --git a/gui/assets/public/js/admin-pagination.js b/gui/assets/public/js/admin-pagination.js new file mode 100644 index 00000000..6f9ea53a --- /dev/null +++ b/gui/assets/public/js/admin-pagination.js @@ -0,0 +1,39 @@ +$.fn["pagination"].defaults.locator = "data"; +$.fn["pagination"].defaults.totalNumberLocator = function(response) { return response.count; }; +$.fn["pagination"].defaults.nextText = '
'; +$.fn["pagination"].defaults.prevText = '
'; +$.fn["pagination"].defaults.hideWhenLessThanOnePage = true; + +if ($('#pending-payments-page-select').length) { + $('#pending-payments-page-select').pagination({ + dataSource: "/admin/payments/pending", + callback: function (data) { + var html = ''; + if (data.length > 0) { + $.each(data, function (_, item) { + html += '' + item.workheight + '' + item.createdon + '' + item.amount + ''; + }); + } else { + html += 'No pending payments'; + } + $('#pending-payments-table').html(html); + } + }); +}; + +if ($('#archived-payments-page-select').length) { + $('#archived-payments-page-select').pagination({ + dataSource: "/admin/payments/archived", + callback: function (data) { + var html = ''; + if (data.length > 0) { + $.each(data, function (_, item) { + html += '' + item.workheight + '' + item.paidheight + '' + item.createdon + '' + item.amount + '' + item.txid + ''; + }); + } else { + html += 'No received payments'; + } + $('#archived-payments-table').html(html); + } + }); +}; \ No newline at end of file diff --git a/gui/assets/templates/account.html b/gui/assets/templates/account.html index e10dfbdd..90e7e7cf 100644 --- a/gui/assets/templates/account.html +++ b/gui/assets/templates/account.html @@ -35,84 +35,9 @@

Account Information

- - {{ if .PendingPayments }} -
-
-
- -

Pending Payments

- - - - - - - - - - - {{ range .PendingPayments }} - - - - - - {{end}} - -
Work HeightCreated OnAmount
{{ .WorkHeight }}{{ .CreatedOn }}{{ .Amount }}
- -
-
-
-
- {{end}} + {{template "payments" . }} -
-
-
-

Payments Received

- - - - - - - - - - - - - {{ range .ArchivedPayments }} - - - - - - - - {{else}} - - - - {{end}} - -
Work HeightPayment HeightCreated OnAmountTx ID
{{ .WorkHeight }}{{ .PaidHeight }}{{ .CreatedOn }}{{ .Amount }}{{ .TxID }} -
No payments received by account
- -
- -
-
- -
-
diff --git a/gui/assets/templates/admin.html b/gui/assets/templates/admin.html index b9d31380..53e90f6b 100644 --- a/gui/assets/templates/admin.html +++ b/gui/assets/templates/admin.html @@ -62,9 +62,12 @@

All Connected Miners

+ {{template "payments" . }} +
+ diff --git a/gui/assets/templates/payments.html b/gui/assets/templates/payments.html new file mode 100644 index 00000000..3e989397 --- /dev/null +++ b/gui/assets/templates/payments.html @@ -0,0 +1,93 @@ +{{define "payments"}} + + {{ if .PendingPayments }} +
+
+
+ +
+

Pending Payments

+ +

+ Total: {{ .PendingPaymentsTotal }} +

+
+ + + + + + + + + + + {{ range .PendingPayments }} + + + + + + {{end}} + +
Work HeightCreated OnAmount
{{ .WorkHeight }}{{ .CreatedOn }}{{ .Amount }}
+ +
+ +
+
+
+ {{end}} + +
+
+
+ +
+

Payments Received

+ +

+ Total: {{ .ArchivedPaymentsTotal }} +

+
+ + + + + + + + + + + + + {{ range .ArchivedPayments }} + + + + + + + + {{else}} + + + + {{end}} + +
Work HeightPayment HeightCreated OnAmountTx ID
{{ .WorkHeight }}{{ .PaidHeight }}{{ .CreatedOn }}{{ .Amount }}{{ .TxID }} +
No payments received
+ +
+ +
+
+ +
+ +{{end}} \ No newline at end of file diff --git a/gui/cache.go b/gui/cache.go index a0e22dd7..089a0ffb 100644 --- a/gui/cache.go +++ b/gui/cache.go @@ -10,6 +10,7 @@ import ( "sort" "sync" + "github.com/decred/dcrd/dcrutil/v3" "github.com/decred/dcrpool/pool" ) @@ -68,19 +69,21 @@ type archivedPayment struct { // access. Data returned by the getters is already formatted for display in the // GUI, so the formatting does not need to be repeated. type Cache struct { - blockExplorerURL string - minedWork []*minedWork - minedWorkMtx sync.RWMutex - rewardQuotas []*rewardQuota - rewardQuotasMtx sync.RWMutex - poolHash string - poolHashMtx sync.RWMutex - clients map[string][]*client - clientsMtx sync.RWMutex - pendingPayments map[string][]*pendingPayment - pendingPaymentsMtx sync.RWMutex - archivedPayments map[string][]*archivedPayment - archivedPaymentsMtx sync.RWMutex + blockExplorerURL string + minedWork []*minedWork + minedWorkMtx sync.RWMutex + rewardQuotas []*rewardQuota + rewardQuotasMtx sync.RWMutex + poolHash string + poolHashMtx sync.RWMutex + clients map[string][]*client + clientsMtx sync.RWMutex + pendingPayments map[string][]*pendingPayment + pendingPaymentTotals map[string]dcrutil.Amount + pendingPaymentsMtx sync.RWMutex + archivedPayments map[string][]*archivedPayment + archivedPaymentTotals map[string]dcrutil.Amount + archivedPaymentsMtx sync.RWMutex } // InitCache initialises and returns a cache for use in the GUI. @@ -281,6 +284,7 @@ func (c *Cache) updatePayments(pendingPmts []*pool.Payment, archivedPmts []*pool return pendingPmts[i].Height > pendingPmts[j].Height }) + pendingPaymentTotals := make(map[string]dcrutil.Amount) pendingPayments := make(map[string][]*pendingPayment) for _, p := range pendingPmts { accountID := p.Account @@ -292,9 +296,14 @@ func (c *Cache) updatePayments(pendingPmts []*pool.Payment, archivedPmts []*pool CreatedOn: formatUnixTime(p.CreatedOn), }, ) + if _, ok := pendingPaymentTotals[accountID]; !ok { + pendingPaymentTotals[accountID] = dcrutil.Amount(0) + } + pendingPaymentTotals[accountID] += p.Amount } c.pendingPaymentsMtx.Lock() + c.pendingPaymentTotals = pendingPaymentTotals c.pendingPayments = pendingPayments c.pendingPaymentsMtx.Unlock() @@ -303,6 +312,7 @@ func (c *Cache) updatePayments(pendingPmts []*pool.Payment, archivedPmts []*pool return archivedPmts[i].Height > archivedPmts[j].Height }) + archivedPaymentTotals := make(map[string]dcrutil.Amount) archivedPayments := make(map[string][]*archivedPayment) for _, p := range archivedPmts { accountID := p.Account @@ -317,15 +327,21 @@ func (c *Cache) updatePayments(pendingPmts []*pool.Payment, archivedPmts []*pool TxURL: txURL(c.blockExplorerURL, p.TransactionID), TxID: fmt.Sprintf("%.10s...", p.TransactionID), }) + if _, ok := archivedPaymentTotals[accountID]; !ok { + archivedPaymentTotals[accountID] = dcrutil.Amount(0) + } + archivedPaymentTotals[accountID] += p.Amount } c.archivedPaymentsMtx.Lock() + c.archivedPaymentTotals = archivedPaymentTotals c.archivedPayments = archivedPayments c.archivedPaymentsMtx.Unlock() } -// getPendingPayments retrieves the cached list of unpaid payments for a given -// account ID. +// getArchivedPayments accesses the cached list of unpaid payments for a given +// account ID. Returns the total number of payments and the set of payments +// requested with first and last params. func (c *Cache) getPendingPayments(first, last int, accountID string) (int, []*pendingPayment, error) { c.pendingPaymentsMtx.RLock() defer c.pendingPaymentsMtx.RUnlock() @@ -345,8 +361,9 @@ func (c *Cache) getPendingPayments(first, last int, accountID string) (int, []*p return count, pendingPmts[first:min(last, count)], nil } -// getArchivedPayments retrieves the cached list of paid payments for a given -// account ID. +// getArchivedPayments accesses the cached list of paid payments for a given +// account ID. Returns the total number of payments and the set of payments +// requested with first and last params. func (c *Cache) getArchivedPayments(first, last int, accountID string) (int, []*archivedPayment, error) { c.archivedPaymentsMtx.RLock() defer c.archivedPaymentsMtx.RUnlock() @@ -365,3 +382,13 @@ func (c *Cache) getArchivedPayments(first, last int, accountID string) (int, []* return count, archivedPmts[first:min(last, count)], nil } + +func (c *Cache) getArchivedPaymentsTotal(accountID string) string { + total := c.archivedPaymentTotals[accountID] + return amount(total) +} + +func (c *Cache) getPendingPaymentsTotal(accountID string) string { + total := c.pendingPaymentTotals[accountID] + return amount(total) +} diff --git a/gui/gui.go b/gui/gui.go index 5fd73566..a466a428 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -154,6 +154,10 @@ func (ui *GUI) route() { guiRouter.HandleFunc("/account/{accountID}/payments/pending", ui.paginatedPendingPaymentsByAccount).Methods("GET") guiRouter.HandleFunc("/account/{accountID}/payments/archived", ui.paginatedArchivedPaymentsByAccount).Methods("GET") + // Paginated endpoints which require admin authentication. + guiRouter.HandleFunc("/admin/payments/pending", ui.paginatedPendingPoolPayments).Methods("GET") + guiRouter.HandleFunc("/admin/payments/archived", ui.paginatedArchivedPoolPayments).Methods("GET") + // Websocket endpoint allows the GUI to receive updated values. guiRouter.HandleFunc("/ws", ui.websocketServer.registerClient).Methods("GET") } diff --git a/gui/pagination.go b/gui/pagination.go index efd53880..cdc80581 100644 --- a/gui/pagination.go +++ b/gui/pagination.go @@ -10,7 +10,9 @@ import ( "net/http" "strconv" + "github.com/decred/dcrpool/pool" "github.com/gorilla/mux" + "github.com/gorilla/sessions" ) type paginationPayload struct { @@ -212,3 +214,71 @@ func (ui *GUI) paginatedArchivedPaymentsByAccount(w http.ResponseWriter, r *http Data: payments, }) } + +// paginatedArchivedPoolPayments is the handler for "GET +// /admin/payments/archived". It uses parameters pageNumber, pageSize and +// accountID to prepare a json payload describing payments made to the pool, as +// well as the total count of all paid payments. +// Returns an error if the current session is not authenticated as an admin. +func (ui *GUI) paginatedArchivedPoolPayments(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value(sessionKey).(*sessions.Session) + + if session.Values["IsAdmin"] != true { + log.Warn("Unauthorized access") + w.WriteHeader(http.StatusUnauthorized) + return + } + + first, last, err := getPaginationParams(r) + if err != nil { + log.Warn(err) + w.WriteHeader(http.StatusBadRequest) + return + } + + count, payments, err := ui.cache.getArchivedPayments(first, last, pool.PoolFeesK) + if err != nil { + log.Warn(err) + w.WriteHeader(http.StatusBadRequest) + return + } + + sendJSONResponse(w, paginationPayload{ + Count: count, + Data: payments, + }) +} + +// paginatedPendingPoolPayments is the handler for "GET +// /admin/payments/pending". It uses parameters pageNumber, pageSize and +// accountID to prepare a json payload describing unpaid payments due to the +// pool, as well as the total count of all unpaid payments. +// Returns an error if the current session is not authenticated as an admin. +func (ui *GUI) paginatedPendingPoolPayments(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value(sessionKey).(*sessions.Session) + + if session.Values["IsAdmin"] != true { + log.Warn("Unauthorized access") + w.WriteHeader(http.StatusUnauthorized) + return + } + + first, last, err := getPaginationParams(r) + if err != nil { + log.Warn(err) + w.WriteHeader(http.StatusBadRequest) + return + } + + count, payments, err := ui.cache.getPendingPayments(first, last, pool.PoolFeesK) + if err != nil { + log.Warn(err) + w.WriteHeader(http.StatusBadRequest) + return + } + + sendJSONResponse(w, paginationPayload{ + Count: count, + Data: payments, + }) +} diff --git a/pool/db.go b/pool/db.go index 8507da08..29ac01fe 100644 --- a/pool/db.go +++ b/pool/db.go @@ -50,8 +50,8 @@ var ( soloPool = []byte("solopool") // csrfSecret is the CSRF secret key. csrfSecret = []byte("csrfsecret") - // poolFeesK is the key used to track pool fee payouts. - poolFeesK = "fees" + // PoolFeesK is the key used to track pool fee payouts. + PoolFeesK = "fees" // backup is the database backup file name. backupFile = "backup.kv" ) diff --git a/pool/paymentmgr.go b/pool/paymentmgr.go index b94d935d..9f7a95db 100644 --- a/pool/paymentmgr.go +++ b/pool/paymentmgr.go @@ -468,7 +468,7 @@ func (pm *PaymentMgr) calculatePayments(ratios map[string]*big.Rat, source *Paym } // Add a payout entry for pool fees. - feePayment := NewPayment(poolFeesK, source, fee, height, estMaturity) + feePayment := NewPayment(PoolFeesK, source, fee, height, estMaturity) payments = append(payments, feePayment) return payments, feePayment.CreatedOn, nil @@ -838,7 +838,7 @@ func (pm *PaymentMgr) payDividends(ctx context.Context, height uint32, treasuryA // Generate the outputs paying dividends and fees. for _, pmt := range set { - if pmt.Account == poolFeesK { + if pmt.Account == PoolFeesK { _, ok := outputs[feeAddr.String()] if !ok { outputs[feeAddr.String()] = pmt.Amount diff --git a/pool/paymentmgr_test.go b/pool/paymentmgr_test.go index 3322c4db..85a52520 100644 --- a/pool/paymentmgr_test.go +++ b/pool/paymentmgr_test.go @@ -664,7 +664,7 @@ func testPaymentMgr(t *testing.T, db *bolt.DB) { if pmt.Account == yID { yt += pmt.Amount } - if pmt.Account == poolFeesK { + if pmt.Account == PoolFeesK { ft += pmt.Amount } } @@ -782,7 +782,7 @@ func testPaymentMgr(t *testing.T, db *bolt.DB) { if pmt.Account == yID { yt += pmt.Amount } - if pmt.Account == poolFeesK { + if pmt.Account == PoolFeesK { ft += pmt.Amount } } @@ -894,7 +894,7 @@ func testPaymentMgr(t *testing.T, db *bolt.DB) { if pmt.Account == yID { yt += pmt.Amount } - if pmt.Account == poolFeesK { + if pmt.Account == PoolFeesK { ft += pmt.Amount } }