Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Paginate Next Reward Payment #185

Merged
merged 5 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,8 +571,8 @@ func loadConfig() (*config, []string, error) {
return nil, nil, err
}

// Ensure the password to unlock the wallet is provided.
// Wallet password is required to pay dividends to pool contributors.
// Ensure the passphrase to unlock the wallet is provided.
// Wallet passphrase is required to pay dividends to pool contributors.
if cfg.WalletPass == "" {
str := "%s: the walletpass option is not set"
err := fmt.Errorf(str, funcName)
Expand Down
25 changes: 25 additions & 0 deletions gui/assets/public/js/pagination.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,28 @@ if ( $('#account-clients-page-select').length ) {
}
});
};

if ($('#reward-quotas-page-select').length) {
$('#reward-quotas-page-select').pagination({
dataSource: "/rewardquotas",
hideWhenLessThanOnePage: true,
nextText: '<div class="pagination-arrow pagination-arrow-right"></div>',
prevText: '<div class="pagination-arrow pagination-arrow-left"></div>',
locator: "rewardquotas",
totalNumberLocator: function (response) {
return response.count;
},
callback: function (data) {
var html = '';

if (data.length > 0) {
$.each(data, function (_, item) {
html += '<tr><td>' + item.percent + '</td><td><span class="dcr-label">' + item.accountid + '</span></td></tr>';
});
} else {
html += '<tr><td colspan="100%"><span class="no-data">No reward payments due</span></td></tr>';
}
$('#reward-quotas-table').html(html);
}
});
};
85 changes: 0 additions & 85 deletions gui/assets/public/js/socket.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,10 @@
var quotaSort;

document.addEventListener("DOMContentLoaded", function () {
workTable = document.getElementById('work-quota-table');

if (workTable != null) {
quotaSort = new Tablesort(workTable, {
descending: true
});
}

ws = new WebSocket('wss://' + window.location.host + '/ws');
ws.addEventListener('message', function (e) {
var msg = JSON.parse(e.data);
updateElement("pool-hash-rate", msg.poolhashrate);
updateElement("last-work-height", msg.lastworkheight);
updateElement("last-payment-height", msg.lastpaymentheight);
if (msg.workquotas == null) {
msg.workquotas = [];
}
updateWorkQuotas(msg.workquotas);
});
});

Expand All @@ -37,74 +23,3 @@ function flashElement(el) {
function removeElement(el) {
el.parentNode.removeChild(el);
}

function updateWorkQuotas(quotas) {
quotasTable = document.getElementById('work-quota-table');

if (quotasTable == null) {
return;
}

quotasTableBody = quotasTable.querySelector('tbody');

// Ensure all received quotas are in the table
var changeMade = false;
for (i = 0; i < quotas.length; i++) {
var exists = false;
var rows = quotasTableBody.querySelectorAll('tr');
for (j = 0; j < rows.length; j++) {
var accountID = rows[j].getAttribute('data-row-id');
if (quotas[i].accountid == accountID) {
// Row already exists for this account ID
exists = true;
// Update percentage for this account if required
var percent = rows[j].cells[0];
if (quotas[i].percent != percent.innerHTML) {
percent.innerHTML = quotas[i].percent;
flashElement(percent);
changeMade = true;
}
break;
}
}

if (exists == false) {
// Add a new row for this account
var newRow = quotasTableBody.insertRow(0);
newRow.setAttribute("data-row-id", quotas[i].accountid);
newRow.insertCell(0).innerHTML = quotas[i].percent;
var span = document.createElement('span');
span.innerHTML = quotas[i].accountid;
span.className = "dcr-label";
newRow.insertCell(1).append(span);
flashElement(newRow);
changeMade = true;
}
}

// Find any rows which are no longer included in quotas
var rowsToRemove = [];
var rows = quotasTableBody.querySelectorAll('tr');
for (j = 0; j < rows.length; j++) {
var exists = false;
for (i = 0; i < quotas.length; i++) {
if (rows[j].getAttribute('data-row-id') == quotas[i].accountid) {
exists = true;
break;
}
}
if (exists == false) {
rowsToRemove.push(rows[j]);
}
}

// Remove the unnecessary rows
for (j = 0; j < rowsToRemove.length; j++) {
removeElement(rowsToRemove[j]);
changeMade = true;
}

if (changeMade) {
quotaSort.refresh();
}
}
5 changes: 0 additions & 5 deletions gui/assets/public/js/vendor/tablesort.min.js

This file was deleted.

5 changes: 0 additions & 5 deletions gui/assets/public/js/vendor/tablesort.number.min.js

This file was deleted.

4 changes: 0 additions & 4 deletions gui/assets/templates/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,11 @@
<link rel="stylesheet" type="text/css" href="/assets/css/dcrpool.css">
<link rel="stylesheet" type="text/css" href="/assets/css/pagination.css">

<script src='/assets/js/vendor/tablesort.min.js'></script>
<script src='/assets/js/vendor/tablesort.number.min.js'></script>

<script src="/assets/js/vendor/jquery-3.4.1.min.js"></script>
<script src="/assets/js/vendor/pagination-2.1.5.min.js"></script>
<script src="/assets/js/vendor/popper-1.16.0.min.js"></script>
<script src="/assets/js/vendor/bootstrap-4.4.1.min.js"></script>
<script src="/assets/js/vendor/flickity.pkgd.min.js"></script>


<!-- Custom favicon -->
<!-- Apple PWA -->
Expand Down
41 changes: 21 additions & 20 deletions gui/assets/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -170,26 +170,27 @@ <h1>Mined by Pool</h1>
<div class="col-lg-6 col-12 p-3">
<div class="block__content">
<h1>Next Reward Payment</h1>
<div class="overflow-auto">
<table id="work-quota-table" class="table">
<thead>
<tr>
<th data-sort-method='number' data-sort-default>Reward Percentage</th>
<th data-sort-method='none'>Account</th>
</tr>
</thead>
<tbody>
{{ range .WorkQuotas }}
<tr data-row-id="{{ .AccountID }}">
<td>{{ .Percent }}</td>
<td>
<span class="dcr-label">{{ .AccountID }}</span>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<table class="table">
<thead>
<tr>
<th>Reward Percentage</th>
<th>Account</th>
</tr>
</thead>
<tbody id="reward-quotas-table">
{{ range .RewardQuotas }}
<tr>
<td>{{ .Percent }}</td>
<td>
<span class="dcr-label">{{ .AccountID }}</span>
</td>
</tr>
{{end}}
</tbody>
</table>

<div id="reward-quotas-page-select" class="page-select"></div>

</div>
</div>

Expand Down
52 changes: 30 additions & 22 deletions gui/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package gui

import (
"math/big"
"sort"
"sync"

"github.com/decred/dcrpool/pool"
Expand All @@ -31,10 +32,10 @@ type minedWork struct {
AccountID string `json:"-"`
}

// workQuota represents dividend garnered by pool accounts through work
// contributed. It is json annotated so it can easily be encoded and sent over a
// websocket or pagination request.
type workQuota struct {
// rewardQuota represents the percentage of reward payment garnered by a pool
// account through work contributed. It is json annotated so it can easily be
// encoded and sent over a websocket or pagination request.
type rewardQuota struct {
AccountID string `json:"accountid"`
Percent string `json:"percent"`
}
Expand All @@ -47,8 +48,8 @@ type Cache struct {
blockExplorerURL string
minedWork []minedWork
minedWorkMtx sync.RWMutex
workQuotas []workQuota
workQuotasMtx sync.RWMutex
rewardQuotas []rewardQuota
rewardQuotasMtx sync.RWMutex
poolHash string
poolHashMtx sync.RWMutex
clients map[string][]client
Expand All @@ -61,7 +62,7 @@ func InitCache(work []*pool.AcceptedWork, quotas []*pool.Quota,

cache := Cache{blockExplorerURL: blockExplorerURL}
cache.updateMinedWork(work)
cache.updateQuotas(quotas)
cache.updateRewardQuotas(quotas)
cache.updateClients(clients)
return &cache
}
Expand Down Expand Up @@ -92,26 +93,33 @@ func (c *Cache) getMinedWork() []minedWork {
return c.minedWork
}

// updateQuotas refreshes the cached list of pending dividend payments.
func (c *Cache) updateQuotas(quotas []*pool.Quota) {
quotaData := make([]workQuota, 0, len(quotas))
for _, quota := range quotas {
quotaData = append(quotaData, workQuota{
AccountID: truncateAccountID(quota.AccountID),
Percent: ratToPercent(quota.Percentage),
// updateRewardQuotas uses a list of work quotas to refresh the cached list of
// pending reward payment quotas.
func (c *Cache) updateRewardQuotas(quotas []*pool.Quota) {

// Sort list so the largest percentages will be shown first.
sort.Slice(quotas, func(i, j int) bool {
return quotas[i].Percentage.Cmp(quotas[j].Percentage) > 0
})

quotaData := make([]rewardQuota, 0, len(quotas))
for _, q := range quotas {
quotaData = append(quotaData, rewardQuota{
AccountID: truncateAccountID(q.AccountID),
Percent: ratToPercent(q.Percentage),
})
}

c.workQuotasMtx.Lock()
c.workQuotas = quotaData
c.workQuotasMtx.Unlock()
c.rewardQuotasMtx.Lock()
c.rewardQuotas = quotaData
c.rewardQuotasMtx.Unlock()
}

// updateQuotas retrieves the cached list of pending dividend payments.
func (c *Cache) getQuotas() []workQuota {
c.workQuotasMtx.RLock()
defer c.workQuotasMtx.RUnlock()
return c.workQuotas
// getRewardQuotas retrieves the cached list of pending reward payment quotas.
func (c *Cache) getRewardQuotas() []rewardQuota {
c.rewardQuotasMtx.RLock()
defer c.rewardQuotasMtx.RUnlock()
return c.rewardQuotas
}

// getPoolHash retrieves the total hashrate of all connected mining clients.
Expand Down
3 changes: 2 additions & 1 deletion gui/gui.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ func (ui *GUI) route() {

// Paginated endpoints allow the GUI to request pages of data.
guiRouter.HandleFunc("/blocks", ui.PaginatedBlocks).Methods("GET")
guiRouter.HandleFunc("/rewardquotas", ui.PaginatedRewardQuotas).Methods("GET")
guiRouter.HandleFunc("/account/{accountID}/blocks", ui.PaginatedBlocksByAccount).Methods("GET")
guiRouter.HandleFunc("/account/{accountID}/clients", ui.PaginatedClientsByAccount).Methods("GET")

Expand Down Expand Up @@ -358,7 +359,7 @@ func (ui *GUI) Run(ctx context.Context) {
if err != nil {
log.Error(err)
} else {
ui.cache.updateQuotas(quotas)
ui.cache.updateRewardQuotas(quotas)
}

clients := ui.cfg.FetchClients()
Expand Down
18 changes: 11 additions & 7 deletions gui/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ type indexPageData struct {
PoolStatsData poolStatsData
MinerPorts map[string]uint32
MinedWork []minedWork
PoolDomain string
WorkQuotas []workQuota
RewardQuotas []rewardQuota
Address string
ModalError string
}
Expand All @@ -42,6 +41,12 @@ func (ui *GUI) renderIndex(w http.ResponseWriter, r *http.Request, modalError st
}
}

// Get the next reward payment percentages (max 10).
rewardQuotas := ui.cache.getRewardQuotas()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be concerned about how many results are returned here now that pagination has been implemented for reward quotas?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for the initial render. When the page is first loaded we only want to display the top ten (ie. the first page).

When the pagination Javascript loads, clickable buttons are displayed which allow the user to load additional data on the next pages. If the Javascript does not load, only the first 10 results will be visible (hence #181).

If this is not limited, it is a potential DoS vector. We had a similar problem with VSP where information about 100s of tickets was passed into a template which caused the templating engine to take 10+ seconds to render.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, thanks.

if len(rewardQuotas) > 10 {
rewardQuotas = rewardQuotas[0:10]
}

data := indexPageData{
HeaderData: headerData{
CSRF: csrf.TemplateField(r),
Expand All @@ -57,11 +62,10 @@ func (ui *GUI) renderIndex(w http.ResponseWriter, r *http.Request, modalError st
PoolFee: ui.cfg.PoolFee,
SoloPool: ui.cfg.SoloPool,
},
WorkQuotas: ui.cache.getQuotas(),
MinedWork: recentWork,
PoolDomain: ui.cfg.Domain,
MinerPorts: ui.cfg.MinerPorts,
ModalError: modalError,
RewardQuotas: rewardQuotas,
MinedWork: recentWork,
MinerPorts: ui.cfg.MinerPorts,
ModalError: modalError,
}

ui.renderTemplate(w, "index", data)
Expand Down
Loading