bittensor-cli 9.5.0__py3-none-any.whl → 9.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bittensor_cli/src/bittensor/templates/main-filters.j2 +24 -0
- bittensor_cli/src/bittensor/templates/main-header.j2 +36 -0
- bittensor_cli/src/bittensor/templates/neuron-details.j2 +111 -0
- bittensor_cli/src/bittensor/templates/price-multi.j2 +113 -0
- bittensor_cli/src/bittensor/templates/price-single.j2 +99 -0
- bittensor_cli/src/bittensor/templates/subnet-details-header.j2 +49 -0
- bittensor_cli/src/bittensor/templates/subnet-details.j2 +32 -0
- bittensor_cli/src/bittensor/templates/subnet-metrics.j2 +57 -0
- bittensor_cli/src/bittensor/templates/subnets-table.j2 +28 -0
- bittensor_cli/src/bittensor/templates/table.j2 +267 -0
- bittensor_cli/src/bittensor/templates/view.css +1058 -0
- bittensor_cli/src/bittensor/templates/view.j2 +43 -0
- bittensor_cli/src/bittensor/templates/view.js +1053 -0
- {bittensor_cli-9.5.0.dist-info → bittensor_cli-9.5.1.dist-info}/METADATA +1 -1
- {bittensor_cli-9.5.0.dist-info → bittensor_cli-9.5.1.dist-info}/RECORD +18 -5
- {bittensor_cli-9.5.0.dist-info → bittensor_cli-9.5.1.dist-info}/WHEEL +0 -0
- {bittensor_cli-9.5.0.dist-info → bittensor_cli-9.5.1.dist-info}/entry_points.txt +0 -0
- {bittensor_cli-9.5.0.dist-info → bittensor_cli-9.5.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1053 @@
|
|
1
|
+
/* ===================== Global Variables ===================== */
|
2
|
+
const root_symbol_html = 'τ';
|
3
|
+
let verboseNumbers = false;
|
4
|
+
|
5
|
+
/* ===================== Clipboard Functions ===================== */
|
6
|
+
/**
|
7
|
+
* Copies text to clipboard and shows visual feedback
|
8
|
+
* @param {string} text The text to copy
|
9
|
+
* @param {HTMLElement} element Optional element to show feedback on
|
10
|
+
*/
|
11
|
+
function copyToClipboard(text, element) {
|
12
|
+
navigator.clipboard.writeText(text)
|
13
|
+
.then(() => {
|
14
|
+
const targetElement = element || (event && event.target);
|
15
|
+
|
16
|
+
if (targetElement) {
|
17
|
+
const copyIndicator = targetElement.querySelector('.copy-indicator');
|
18
|
+
|
19
|
+
if (copyIndicator) {
|
20
|
+
const originalText = copyIndicator.textContent;
|
21
|
+
copyIndicator.textContent = 'Copied!';
|
22
|
+
copyIndicator.style.color = '#FF9900';
|
23
|
+
|
24
|
+
setTimeout(() => {
|
25
|
+
copyIndicator.textContent = originalText;
|
26
|
+
copyIndicator.style.color = '';
|
27
|
+
}, 1000);
|
28
|
+
} else {
|
29
|
+
const originalText = targetElement.textContent;
|
30
|
+
targetElement.textContent = 'Copied!';
|
31
|
+
targetElement.style.color = '#FF9900';
|
32
|
+
|
33
|
+
setTimeout(() => {
|
34
|
+
targetElement.textContent = originalText;
|
35
|
+
targetElement.style.color = '';
|
36
|
+
}, 1000);
|
37
|
+
}
|
38
|
+
}
|
39
|
+
})
|
40
|
+
.catch(err => {
|
41
|
+
console.error('Failed to copy:', err);
|
42
|
+
});
|
43
|
+
}
|
44
|
+
|
45
|
+
|
46
|
+
/* ===================== Initialization and DOMContentLoaded Handler ===================== */
|
47
|
+
document.addEventListener('DOMContentLoaded', function() {
|
48
|
+
try {
|
49
|
+
const initialDataElement = document.getElementById('initial-data');
|
50
|
+
if (!initialDataElement) {
|
51
|
+
throw new Error('Initial data element (#initial-data) not found.');
|
52
|
+
}
|
53
|
+
window.initialData = {
|
54
|
+
wallet_info: JSON.parse(initialDataElement.getAttribute('data-wallet-info')),
|
55
|
+
subnets: JSON.parse(initialDataElement.getAttribute('data-subnets'))
|
56
|
+
};
|
57
|
+
} catch (error) {
|
58
|
+
console.error('Error loading initial data:', error);
|
59
|
+
}
|
60
|
+
|
61
|
+
// Return to the main list of subnets.
|
62
|
+
const backButton = document.querySelector('.back-button');
|
63
|
+
if (backButton) {
|
64
|
+
backButton.addEventListener('click', function() {
|
65
|
+
// First check if neuron details are visible and close them if needed
|
66
|
+
const neuronDetails = document.getElementById('neuron-detail-container');
|
67
|
+
if (neuronDetails && neuronDetails.style.display !== 'none') {
|
68
|
+
closeNeuronDetails();
|
69
|
+
return; // Stop here, don't go back to main page yet
|
70
|
+
}
|
71
|
+
|
72
|
+
// Otherwise go back to main subnet list
|
73
|
+
document.getElementById('main-content').style.display = 'block';
|
74
|
+
document.getElementById('subnet-page').style.display = 'none';
|
75
|
+
});
|
76
|
+
}
|
77
|
+
|
78
|
+
|
79
|
+
// Splash screen logic
|
80
|
+
const splash = document.getElementById('splash-screen');
|
81
|
+
const mainContent = document.getElementById('main-content');
|
82
|
+
mainContent.style.display = 'none';
|
83
|
+
|
84
|
+
setTimeout(() => {
|
85
|
+
splash.classList.add('fade-out');
|
86
|
+
splash.addEventListener('transitionend', () => {
|
87
|
+
splash.style.display = 'none';
|
88
|
+
mainContent.style.display = 'block';
|
89
|
+
}, { once: true });
|
90
|
+
}, 2000);
|
91
|
+
|
92
|
+
initializeFormattedNumbers();
|
93
|
+
|
94
|
+
// Keep main page's "verbose" checkbox and the Subnet page's "verbose" checkbox in sync
|
95
|
+
const mainVerboseCheckbox = document.getElementById('show-verbose');
|
96
|
+
const subnetVerboseCheckbox = document.getElementById('verbose-toggle');
|
97
|
+
if (mainVerboseCheckbox && subnetVerboseCheckbox) {
|
98
|
+
mainVerboseCheckbox.addEventListener('change', function() {
|
99
|
+
subnetVerboseCheckbox.checked = this.checked;
|
100
|
+
toggleVerboseNumbers();
|
101
|
+
});
|
102
|
+
subnetVerboseCheckbox.addEventListener('change', function() {
|
103
|
+
mainVerboseCheckbox.checked = this.checked;
|
104
|
+
toggleVerboseNumbers();
|
105
|
+
});
|
106
|
+
}
|
107
|
+
|
108
|
+
// Initialize tile view as default
|
109
|
+
const tilesContainer = document.getElementById('subnet-tiles-container');
|
110
|
+
const tableContainer = document.querySelector('.subnets-table-container');
|
111
|
+
|
112
|
+
// Generate and show tiles
|
113
|
+
generateSubnetTiles();
|
114
|
+
tilesContainer.style.display = 'flex';
|
115
|
+
tableContainer.style.display = 'none';
|
116
|
+
});
|
117
|
+
|
118
|
+
/* ===================== Main Page Functions ===================== */
|
119
|
+
/**
|
120
|
+
* Sort the main Subnets table by the specified column index.
|
121
|
+
* Toggles ascending/descending on each click.
|
122
|
+
* @param {number} columnIndex Index of the column to sort.
|
123
|
+
*/
|
124
|
+
function sortMainTable(columnIndex) {
|
125
|
+
const table = document.querySelector('.subnets-table');
|
126
|
+
const headers = table.querySelectorAll('th');
|
127
|
+
const header = headers[columnIndex];
|
128
|
+
|
129
|
+
// Determine new sort direction
|
130
|
+
let isDescending = header.getAttribute('data-sort') !== 'desc';
|
131
|
+
|
132
|
+
// Clear sort markers on all columns, then set the new one
|
133
|
+
headers.forEach(th => { th.removeAttribute('data-sort'); });
|
134
|
+
header.setAttribute('data-sort', isDescending ? 'desc' : 'asc');
|
135
|
+
|
136
|
+
// Sort rows based on numeric value (or netuid in col 0)
|
137
|
+
const tbody = table.querySelector('tbody');
|
138
|
+
const rows = Array.from(tbody.querySelectorAll('tr'));
|
139
|
+
rows.sort((rowA, rowB) => {
|
140
|
+
const cellA = rowA.cells[columnIndex];
|
141
|
+
const cellB = rowB.cells[columnIndex];
|
142
|
+
|
143
|
+
// Special handling for the first column with netuid in data-value
|
144
|
+
if (columnIndex === 0) {
|
145
|
+
const netuidA = parseInt(cellA.getAttribute('data-value'), 10);
|
146
|
+
const netuidB = parseInt(cellB.getAttribute('data-value'), 10);
|
147
|
+
return isDescending ? (netuidB - netuidA) : (netuidA - netuidB);
|
148
|
+
}
|
149
|
+
|
150
|
+
// Otherwise parse float from data-value
|
151
|
+
const valueA = parseFloat(cellA.getAttribute('data-value')) || 0;
|
152
|
+
const valueB = parseFloat(cellB.getAttribute('data-value')) || 0;
|
153
|
+
return isDescending ? (valueB - valueA) : (valueA - valueB);
|
154
|
+
});
|
155
|
+
|
156
|
+
// Re-inject rows in sorted order
|
157
|
+
tbody.innerHTML = '';
|
158
|
+
rows.forEach(row => tbody.appendChild(row));
|
159
|
+
}
|
160
|
+
|
161
|
+
/**
|
162
|
+
* Filters the main Subnets table rows based on user search and "Show Only Staked" checkbox.
|
163
|
+
*/
|
164
|
+
function filterSubnets() {
|
165
|
+
const searchText = document.getElementById('subnet-search').value.toLowerCase();
|
166
|
+
const showStaked = document.getElementById('show-staked').checked;
|
167
|
+
const showTiles = document.getElementById('show-tiles').checked;
|
168
|
+
|
169
|
+
// Filter table rows
|
170
|
+
const rows = document.querySelectorAll('.subnet-row');
|
171
|
+
rows.forEach(row => {
|
172
|
+
const name = row.querySelector('.subnet-name').textContent.toLowerCase();
|
173
|
+
const stakeStatus = row.querySelector('.stake-status').textContent; // "Staked" or "Not Staked"
|
174
|
+
|
175
|
+
let isVisible = name.includes(searchText);
|
176
|
+
if (showStaked) {
|
177
|
+
// If "Show only Staked" is checked, the row must have "Staked" to be visible
|
178
|
+
isVisible = isVisible && (stakeStatus === 'Staked');
|
179
|
+
}
|
180
|
+
row.style.display = isVisible ? '' : 'none';
|
181
|
+
});
|
182
|
+
|
183
|
+
// Filter tiles if they're being shown
|
184
|
+
if (showTiles) {
|
185
|
+
const tiles = document.querySelectorAll('.subnet-tile');
|
186
|
+
tiles.forEach(tile => {
|
187
|
+
const name = tile.querySelector('.tile-name').textContent.toLowerCase();
|
188
|
+
const netuid = tile.querySelector('.tile-netuid').textContent;
|
189
|
+
const isStaked = tile.classList.contains('staked');
|
190
|
+
|
191
|
+
let isVisible = name.includes(searchText) || netuid.includes(searchText);
|
192
|
+
if (showStaked) {
|
193
|
+
isVisible = isVisible && isStaked;
|
194
|
+
}
|
195
|
+
tile.style.display = isVisible ? '' : 'none';
|
196
|
+
});
|
197
|
+
}
|
198
|
+
}
|
199
|
+
|
200
|
+
|
201
|
+
/* ===================== Subnet Detail Page Functions ===================== */
|
202
|
+
/**
|
203
|
+
* Displays the Subnet page (detailed view) for the selected netuid.
|
204
|
+
* Hides the main content and populates all the metrics / stakes / network table.
|
205
|
+
* @param {number} netuid The netuid of the subnet to show in detail.
|
206
|
+
*/
|
207
|
+
function showSubnetPage(netuid) {
|
208
|
+
try {
|
209
|
+
window.currentSubnet = netuid;
|
210
|
+
window.scrollTo(0, 0);
|
211
|
+
|
212
|
+
const subnet = window.initialData.subnets.find(s => s.netuid === parseInt(netuid, 10));
|
213
|
+
if (!subnet) {
|
214
|
+
throw new Error(`Subnet not found for netuid: ${netuid}`);
|
215
|
+
}
|
216
|
+
window.currentSubnetSymbol = subnet.symbol;
|
217
|
+
|
218
|
+
// Insert the "metagraph" table beneath the "stakes" table in the hidden container
|
219
|
+
const networkTableHTML = `
|
220
|
+
<div class="network-table-container" style="display: none;">
|
221
|
+
<div class="network-search-container">
|
222
|
+
<input type="text" class="network-search" placeholder="Search for name, hotkey, or coldkey ss58..."
|
223
|
+
oninput="filterNetworkTable(this.value)" id="network-search">
|
224
|
+
</div>
|
225
|
+
<table class="network-table">
|
226
|
+
<thead>
|
227
|
+
<tr>
|
228
|
+
<th>Name</th>
|
229
|
+
<th>Stake Weight</th>
|
230
|
+
<th>Stake <span style="color: #FF9900">${subnet.symbol}</span></th>
|
231
|
+
<th>Stake <span style="color: #FF9900">${root_symbol_html}</span></th>
|
232
|
+
<th>Dividends</th>
|
233
|
+
<th>Incentive</th>
|
234
|
+
<th>Emissions <span class="per-day">/day</span></th>
|
235
|
+
<th>Hotkey</th>
|
236
|
+
<th>Coldkey</th>
|
237
|
+
</tr>
|
238
|
+
</thead>
|
239
|
+
<tbody>
|
240
|
+
${generateNetworkTableRows(subnet.metagraph_info)}
|
241
|
+
</tbody>
|
242
|
+
</table>
|
243
|
+
</div>
|
244
|
+
`;
|
245
|
+
|
246
|
+
// Show/hide main content vs. subnet detail
|
247
|
+
document.getElementById('main-content').style.display = 'none';
|
248
|
+
document.getElementById('subnet-page').style.display = 'block';
|
249
|
+
|
250
|
+
document.querySelector('#subnet-title').textContent = `${subnet.netuid} - ${subnet.name}`;
|
251
|
+
document.querySelector('#subnet-price').innerHTML = formatNumber(subnet.price, subnet.symbol);
|
252
|
+
document.querySelector('#subnet-market-cap').innerHTML = formatNumber(subnet.market_cap, root_symbol_html);
|
253
|
+
document.querySelector('#subnet-total-stake').innerHTML= formatNumber(subnet.total_stake, subnet.symbol);
|
254
|
+
document.querySelector('#subnet-emission').innerHTML = formatNumber(subnet.emission, root_symbol_html);
|
255
|
+
|
256
|
+
|
257
|
+
const metagraphInfo = subnet.metagraph_info;
|
258
|
+
document.querySelector('#network-alpha-in').innerHTML = formatNumber(metagraphInfo.alpha_in, subnet.symbol);
|
259
|
+
document.querySelector('#network-tau-in').innerHTML = formatNumber(metagraphInfo.tao_in, root_symbol_html);
|
260
|
+
document.querySelector('#network-moving-price').innerHTML = formatNumber(metagraphInfo.moving_price, subnet.symbol);
|
261
|
+
|
262
|
+
// Registration status
|
263
|
+
const registrationElement = document.querySelector('#network-registration');
|
264
|
+
registrationElement.textContent = metagraphInfo.registration_allowed ? 'Open' : 'Closed';
|
265
|
+
registrationElement.classList.toggle('closed', !metagraphInfo.registration_allowed);
|
266
|
+
|
267
|
+
// Commit-Reveal Weight status
|
268
|
+
const crElement = document.querySelector('#network-cr');
|
269
|
+
crElement.textContent = metagraphInfo.commit_reveal_weights_enabled ? 'Enabled' : 'Disabled';
|
270
|
+
crElement.classList.toggle('disabled', !metagraphInfo.commit_reveal_weights_enabled);
|
271
|
+
|
272
|
+
// Blocks since last step, out of tempo
|
273
|
+
document.querySelector('#network-blocks-since-step').innerHTML =
|
274
|
+
`${metagraphInfo.blocks_since_last_step}/${metagraphInfo.tempo}`;
|
275
|
+
|
276
|
+
// Number of neurons vs. max
|
277
|
+
document.querySelector('#network-neurons').innerHTML =
|
278
|
+
`${metagraphInfo.num_uids}/${metagraphInfo.max_uids}`;
|
279
|
+
|
280
|
+
// Update "Your Stakes" table
|
281
|
+
const stakesTableBody = document.querySelector('#stakes-table-body');
|
282
|
+
stakesTableBody.innerHTML = '';
|
283
|
+
if (subnet.your_stakes && subnet.your_stakes.length > 0) {
|
284
|
+
subnet.your_stakes.forEach(stake => {
|
285
|
+
const row = document.createElement('tr');
|
286
|
+
row.innerHTML = `
|
287
|
+
<td class="hotkey-cell">
|
288
|
+
<div class="hotkey-container">
|
289
|
+
<span class="hotkey-identity" style="color: #FF9900">${stake.hotkey_identity}</span>
|
290
|
+
<!-- Remove the unused event param -->
|
291
|
+
<span class="copy-button" onclick="copyToClipboard('${stake.hotkey}')">copy</span>
|
292
|
+
</div>
|
293
|
+
</td>
|
294
|
+
<td>${formatNumber(stake.amount, subnet.symbol)}</td>
|
295
|
+
<td>${formatNumber(stake.ideal_value, root_symbol_html)}</td>
|
296
|
+
<td>${formatNumber(stake.slippage_value, root_symbol_html)} (${stake.slippage_percentage.toFixed(2)}%)</td>
|
297
|
+
<td>${formatNumber(stake.emission, subnet.symbol + '/day')}</td>
|
298
|
+
<td>${formatNumber(stake.tao_emission, root_symbol_html + '/day')}</td>
|
299
|
+
<td class="registered-cell">
|
300
|
+
<span class="${stake.is_registered ? 'registered-yes' : 'registered-no'}">
|
301
|
+
${stake.is_registered ? 'Yes' : 'No'}
|
302
|
+
</span>
|
303
|
+
</td>
|
304
|
+
<td class="actions-cell">
|
305
|
+
<button class="manage-button">Coming soon</button>
|
306
|
+
</td>
|
307
|
+
`;
|
308
|
+
stakesTableBody.appendChild(row);
|
309
|
+
});
|
310
|
+
} else {
|
311
|
+
// If no user stake in this subnet
|
312
|
+
stakesTableBody.innerHTML = `
|
313
|
+
<tr class="no-stakes-row">
|
314
|
+
<td colspan="8">No stakes found for this subnet</td>
|
315
|
+
</tr>
|
316
|
+
`;
|
317
|
+
}
|
318
|
+
|
319
|
+
// Remove any previously injected network table then add the new one
|
320
|
+
const existingNetworkTable = document.querySelector('.network-table-container');
|
321
|
+
if (existingNetworkTable) {
|
322
|
+
existingNetworkTable.remove();
|
323
|
+
}
|
324
|
+
document.querySelector('.stakes-table-container').insertAdjacentHTML('afterend', networkTableHTML);
|
325
|
+
|
326
|
+
// Format the new numbers
|
327
|
+
initializeFormattedNumbers();
|
328
|
+
|
329
|
+
// Initialize connectivity visualization (the dots / lines "animation")
|
330
|
+
setTimeout(() => { initNetworkVisualization(); }, 100);
|
331
|
+
|
332
|
+
// Toggle whether we are showing the "Your Stakes" or "Metagraph" table
|
333
|
+
toggleStakeView();
|
334
|
+
|
335
|
+
// Initialize sorting on newly injected table columns
|
336
|
+
initializeSorting();
|
337
|
+
|
338
|
+
// Auto-sort by Stake descending on the network table for convenience
|
339
|
+
setTimeout(() => {
|
340
|
+
const networkTable = document.querySelector('.network-table');
|
341
|
+
if (networkTable) {
|
342
|
+
const stakeColumn = networkTable.querySelector('th:nth-child(2)');
|
343
|
+
if (stakeColumn) {
|
344
|
+
sortTable(networkTable, 1, stakeColumn, true);
|
345
|
+
stakeColumn.setAttribute('data-sort', 'desc');
|
346
|
+
}
|
347
|
+
}
|
348
|
+
}, 100);
|
349
|
+
|
350
|
+
console.log('Subnet page updated successfully');
|
351
|
+
} catch (error) {
|
352
|
+
console.error('Error updating subnet page:', error);
|
353
|
+
}
|
354
|
+
}
|
355
|
+
|
356
|
+
/**
|
357
|
+
* Generates the rows for the "Neurons" table (shown when the user unchecks "Show Stakes").
|
358
|
+
* Each row, when clicked, calls showNeuronDetails(i).
|
359
|
+
* @param {Object} metagraphInfo The "metagraph_info" of the subnet that holds hotkeys, etc.
|
360
|
+
*/
|
361
|
+
function generateNetworkTableRows(metagraphInfo) {
|
362
|
+
const rows = [];
|
363
|
+
console.log('Generating network table rows with data:', metagraphInfo);
|
364
|
+
|
365
|
+
for (let i = 0; i < metagraphInfo.hotkeys.length; i++) {
|
366
|
+
// Subnet symbol is used to show token vs. root stake
|
367
|
+
const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
|
368
|
+
const subnetSymbol = subnet ? subnet.symbol : '';
|
369
|
+
|
370
|
+
// Possibly show hotkey/coldkey truncated for readability
|
371
|
+
const truncatedHotkey = truncateAddress(metagraphInfo.hotkeys[i]);
|
372
|
+
const truncatedColdkey = truncateAddress(metagraphInfo.coldkeys[i]);
|
373
|
+
const identityName = metagraphInfo.updated_identities[i] || '~';
|
374
|
+
|
375
|
+
// Root stake is being scaled by 0.18 arbitrarily here
|
376
|
+
const adjustedRootStake = metagraphInfo.tao_stake[i] * 0.18;
|
377
|
+
|
378
|
+
rows.push(`
|
379
|
+
<tr onclick="showNeuronDetails(${i})">
|
380
|
+
<td class="identity-cell">${identityName}</td>
|
381
|
+
<td data-value="${metagraphInfo.total_stake[i]}">
|
382
|
+
<span class="formatted-number" data-value="${metagraphInfo.total_stake[i]}" data-symbol="${subnetSymbol}"></span>
|
383
|
+
</td>
|
384
|
+
<td data-value="${metagraphInfo.alpha_stake[i]}">
|
385
|
+
<span class="formatted-number" data-value="${metagraphInfo.alpha_stake[i]}" data-symbol="${subnetSymbol}"></span>
|
386
|
+
</td>
|
387
|
+
<td data-value="${adjustedRootStake}">
|
388
|
+
<span class="formatted-number" data-value="${adjustedRootStake}" data-symbol="${root_symbol_html}"></span>
|
389
|
+
</td>
|
390
|
+
<td data-value="${metagraphInfo.dividends[i]}">
|
391
|
+
<span class="formatted-number" data-value="${metagraphInfo.dividends[i]}" data-symbol=""></span>
|
392
|
+
</td>
|
393
|
+
<td data-value="${metagraphInfo.incentives[i]}">
|
394
|
+
<span class="formatted-number" data-value="${metagraphInfo.incentives[i]}" data-symbol=""></span>
|
395
|
+
</td>
|
396
|
+
<td data-value="${metagraphInfo.emission[i]}">
|
397
|
+
<span class="formatted-number" data-value="${metagraphInfo.emission[i]}" data-symbol="${subnetSymbol}"></span>
|
398
|
+
</td>
|
399
|
+
<td class="address-cell">
|
400
|
+
<div class="hotkey-container" data-full-address="${metagraphInfo.hotkeys[i]}">
|
401
|
+
<span class="truncated-address">${truncatedHotkey}</span>
|
402
|
+
<span class="copy-button" onclick="event.stopPropagation(); copyToClipboard('${metagraphInfo.hotkeys[i]}')">copy</span>
|
403
|
+
</div>
|
404
|
+
</td>
|
405
|
+
<td class="address-cell">
|
406
|
+
<div class="hotkey-container" data-full-address="${metagraphInfo.coldkeys[i]}">
|
407
|
+
<span class="truncated-address">${truncatedColdkey}</span>
|
408
|
+
<span class="copy-button" onclick="event.stopPropagation(); copyToClipboard('${metagraphInfo.coldkeys[i]}')">copy</span>
|
409
|
+
</div>
|
410
|
+
</td>
|
411
|
+
</tr>
|
412
|
+
`);
|
413
|
+
}
|
414
|
+
return rows.join('');
|
415
|
+
}
|
416
|
+
|
417
|
+
/**
|
418
|
+
* Handles toggling between the "Your Stakes" view and the "Neurons" view on the Subnet page.
|
419
|
+
* The "Show Stakes" checkbox (#stake-toggle) controls which table is visible.
|
420
|
+
*/
|
421
|
+
function toggleStakeView() {
|
422
|
+
const showStakes = document.getElementById('stake-toggle').checked;
|
423
|
+
const stakesTable = document.querySelector('.stakes-table-container');
|
424
|
+
const networkTable = document.querySelector('.network-table-container');
|
425
|
+
const sectionHeader = document.querySelector('.view-header');
|
426
|
+
const neuronDetails = document.getElementById('neuron-detail-container');
|
427
|
+
const addStakeButton = document.querySelector('.add-stake-button');
|
428
|
+
const exportCsvButton = document.querySelector('.export-csv-button');
|
429
|
+
const stakesHeader = document.querySelector('.stakes-header');
|
430
|
+
|
431
|
+
// First, close neuron details if they're open
|
432
|
+
if (neuronDetails && neuronDetails.style.display !== 'none') {
|
433
|
+
neuronDetails.style.display = 'none';
|
434
|
+
}
|
435
|
+
|
436
|
+
// Always show the section header and stakes header when toggling views
|
437
|
+
if (sectionHeader) sectionHeader.style.display = 'block';
|
438
|
+
if (stakesHeader) stakesHeader.style.display = 'flex';
|
439
|
+
|
440
|
+
if (showStakes) {
|
441
|
+
// Show the Stakes table, hide the Neurons table
|
442
|
+
stakesTable.style.display = 'block';
|
443
|
+
networkTable.style.display = 'none';
|
444
|
+
sectionHeader.textContent = 'Your Stakes';
|
445
|
+
if (addStakeButton) {
|
446
|
+
addStakeButton.style.display = 'none';
|
447
|
+
}
|
448
|
+
if (exportCsvButton) {
|
449
|
+
exportCsvButton.style.display = 'none';
|
450
|
+
}
|
451
|
+
} else {
|
452
|
+
// Show the Neurons table, hide the Stakes table
|
453
|
+
stakesTable.style.display = 'none';
|
454
|
+
networkTable.style.display = 'block';
|
455
|
+
sectionHeader.textContent = 'Metagraph';
|
456
|
+
if (addStakeButton) {
|
457
|
+
addStakeButton.style.display = 'block';
|
458
|
+
}
|
459
|
+
if (exportCsvButton) {
|
460
|
+
exportCsvButton.style.display = 'block';
|
461
|
+
}
|
462
|
+
}
|
463
|
+
}
|
464
|
+
|
465
|
+
/**
|
466
|
+
* Called when you click a row in the "Neurons" table, to display more detail about that neuron.
|
467
|
+
* This hides the "Neurons" table and shows the #neuron-detail-container.
|
468
|
+
* @param {number} rowIndex The index of the neuron in the arrays (hotkeys, coldkeys, etc.)
|
469
|
+
*/
|
470
|
+
function showNeuronDetails(rowIndex) {
|
471
|
+
try {
|
472
|
+
// Hide the network table & stakes table
|
473
|
+
const networkTable = document.querySelector('.network-table-container');
|
474
|
+
if (networkTable) networkTable.style.display = 'none';
|
475
|
+
const stakesTable = document.querySelector('.stakes-table-container');
|
476
|
+
if (stakesTable) stakesTable.style.display = 'none';
|
477
|
+
|
478
|
+
// Hide the stakes header with the action buttons
|
479
|
+
const stakesHeader = document.querySelector('.stakes-header');
|
480
|
+
if (stakesHeader) stakesHeader.style.display = 'none';
|
481
|
+
|
482
|
+
// Hide the view header that says "Neurons"
|
483
|
+
const viewHeader = document.querySelector('.view-header');
|
484
|
+
if (viewHeader) viewHeader.style.display = 'none';
|
485
|
+
|
486
|
+
// Show the neuron detail panel
|
487
|
+
const detailContainer = document.getElementById('neuron-detail-container');
|
488
|
+
if (detailContainer) detailContainer.style.display = 'block';
|
489
|
+
|
490
|
+
// Pull out the current subnet
|
491
|
+
const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
|
492
|
+
if (!subnet) {
|
493
|
+
console.error('No subnet data for netuid:', window.currentSubnet);
|
494
|
+
return;
|
495
|
+
}
|
496
|
+
|
497
|
+
const metagraphInfo = subnet.metagraph_info;
|
498
|
+
const subnetSymbol = subnet.symbol || '';
|
499
|
+
|
500
|
+
// Pull axon data, for IP info
|
501
|
+
const axonData = metagraphInfo.processed_axons ? metagraphInfo.processed_axons[rowIndex] : null;
|
502
|
+
let ipInfoString;
|
503
|
+
|
504
|
+
// Update IP info card - hide header if IP info is present
|
505
|
+
const ipInfoCard = document.getElementById('neuron-ipinfo').closest('.metric-card');
|
506
|
+
if (axonData && axonData.ip !== 'N/A') {
|
507
|
+
// If we have valid IP info, hide the "IP Info" label
|
508
|
+
if (ipInfoCard && ipInfoCard.querySelector('.metric-label')) {
|
509
|
+
ipInfoCard.querySelector('.metric-label').style.display = 'none';
|
510
|
+
}
|
511
|
+
// Format IP info with green labels
|
512
|
+
ipInfoString = `<span style="color: #FF9900">IP:</span> ${axonData.ip}<br>` +
|
513
|
+
`<span style="color: #FF9900">Port:</span> ${axonData.port}<br>` +
|
514
|
+
`<span style="color: #FF9900">Type:</span> ${axonData.ip_type}`;
|
515
|
+
} else {
|
516
|
+
// If no IP info, show the label
|
517
|
+
if (ipInfoCard && ipInfoCard.querySelector('.metric-label')) {
|
518
|
+
ipInfoCard.querySelector('.metric-label').style.display = 'block';
|
519
|
+
}
|
520
|
+
ipInfoString = '<span style="color: #ff4444; font-size: 1.2em;">N/A</span>';
|
521
|
+
}
|
522
|
+
|
523
|
+
// Basic identity and hotkey/coldkey info
|
524
|
+
const name = metagraphInfo.updated_identities[rowIndex] || '~';
|
525
|
+
const hotkey = metagraphInfo.hotkeys[rowIndex];
|
526
|
+
const coldkey = metagraphInfo.coldkeys[rowIndex];
|
527
|
+
const rank = metagraphInfo.rank ? metagraphInfo.rank[rowIndex] : 0;
|
528
|
+
const trust = metagraphInfo.trust ? metagraphInfo.trust[rowIndex] : 0;
|
529
|
+
const pruning = metagraphInfo.pruning_score ? metagraphInfo.pruning_score[rowIndex] : 0;
|
530
|
+
const vPermit = metagraphInfo.validator_permit ? metagraphInfo.validator_permit[rowIndex] : false;
|
531
|
+
const lastUpd = metagraphInfo.last_update ? metagraphInfo.last_update[rowIndex] : 0;
|
532
|
+
const consensus = metagraphInfo.consensus ? metagraphInfo.consensus[rowIndex] : 0;
|
533
|
+
const regBlock = metagraphInfo.block_at_registration ? metagraphInfo.block_at_registration[rowIndex] : 0;
|
534
|
+
const active = metagraphInfo.active ? metagraphInfo.active[rowIndex] : false;
|
535
|
+
|
536
|
+
// Update UI fields
|
537
|
+
document.getElementById('neuron-name').textContent = name;
|
538
|
+
document.getElementById('neuron-name').style.color = '#FF9900';
|
539
|
+
|
540
|
+
document.getElementById('neuron-hotkey').textContent = hotkey;
|
541
|
+
document.getElementById('neuron-coldkey').textContent = coldkey;
|
542
|
+
document.getElementById('neuron-trust').textContent = trust.toFixed(4);
|
543
|
+
document.getElementById('neuron-pruning-score').textContent = pruning.toFixed(4);
|
544
|
+
|
545
|
+
// Validator
|
546
|
+
const validatorElem = document.getElementById('neuron-validator-permit');
|
547
|
+
if (vPermit) {
|
548
|
+
validatorElem.style.color = '#2ECC71';
|
549
|
+
validatorElem.textContent = 'True';
|
550
|
+
} else {
|
551
|
+
validatorElem.style.color = '#ff4444';
|
552
|
+
validatorElem.textContent = 'False';
|
553
|
+
}
|
554
|
+
|
555
|
+
document.getElementById('neuron-last-update').textContent = lastUpd;
|
556
|
+
document.getElementById('neuron-consensus').textContent = consensus.toFixed(4);
|
557
|
+
document.getElementById('neuron-reg-block').textContent = regBlock;
|
558
|
+
document.getElementById('neuron-ipinfo').innerHTML = ipInfoString;
|
559
|
+
|
560
|
+
const activeElem = document.getElementById('neuron-active');
|
561
|
+
if (active) {
|
562
|
+
activeElem.style.color = '#2ECC71';
|
563
|
+
activeElem.textContent = 'Yes';
|
564
|
+
} else {
|
565
|
+
activeElem.style.color = '#ff4444';
|
566
|
+
activeElem.textContent = 'No';
|
567
|
+
}
|
568
|
+
|
569
|
+
// Add stake data ("total_stake", "alpha_stake", "tao_stake")
|
570
|
+
document.getElementById('neuron-stake-total').setAttribute(
|
571
|
+
'data-value', metagraphInfo.total_stake[rowIndex]
|
572
|
+
);
|
573
|
+
document.getElementById('neuron-stake-total').setAttribute(
|
574
|
+
'data-symbol', subnetSymbol
|
575
|
+
);
|
576
|
+
|
577
|
+
document.getElementById('neuron-stake-token').setAttribute(
|
578
|
+
'data-value', metagraphInfo.alpha_stake[rowIndex]
|
579
|
+
);
|
580
|
+
document.getElementById('neuron-stake-token').setAttribute(
|
581
|
+
'data-symbol', subnetSymbol
|
582
|
+
);
|
583
|
+
|
584
|
+
// Multiply tao_stake by 0.18
|
585
|
+
const originalStakeRoot = metagraphInfo.tao_stake[rowIndex];
|
586
|
+
const calculatedStakeRoot = originalStakeRoot * 0.18;
|
587
|
+
|
588
|
+
document.getElementById('neuron-stake-root').setAttribute(
|
589
|
+
'data-value', calculatedStakeRoot
|
590
|
+
);
|
591
|
+
document.getElementById('neuron-stake-root').setAttribute(
|
592
|
+
'data-symbol', root_symbol_html
|
593
|
+
);
|
594
|
+
// Also set the inner text right away, so we show a correct format on load
|
595
|
+
document.getElementById('neuron-stake-root').innerHTML =
|
596
|
+
formatNumber(calculatedStakeRoot, root_symbol_html);
|
597
|
+
|
598
|
+
// Dividends, Incentive
|
599
|
+
document.getElementById('neuron-dividends').setAttribute(
|
600
|
+
'data-value', metagraphInfo.dividends[rowIndex]
|
601
|
+
);
|
602
|
+
document.getElementById('neuron-dividends').setAttribute('data-symbol', '');
|
603
|
+
|
604
|
+
document.getElementById('neuron-incentive').setAttribute(
|
605
|
+
'data-value', metagraphInfo.incentives[rowIndex]
|
606
|
+
);
|
607
|
+
document.getElementById('neuron-incentive').setAttribute('data-symbol', '');
|
608
|
+
|
609
|
+
// Emissions
|
610
|
+
document.getElementById('neuron-emissions').setAttribute(
|
611
|
+
'data-value', metagraphInfo.emission[rowIndex]
|
612
|
+
);
|
613
|
+
document.getElementById('neuron-emissions').setAttribute('data-symbol', subnetSymbol);
|
614
|
+
|
615
|
+
// Rank
|
616
|
+
document.getElementById('neuron-rank').textContent = rank.toFixed(4);
|
617
|
+
|
618
|
+
// Re-run formatting so the newly updated data-values appear in numeric form
|
619
|
+
initializeFormattedNumbers();
|
620
|
+
} catch (err) {
|
621
|
+
console.error('Error showing neuron details:', err);
|
622
|
+
}
|
623
|
+
}
|
624
|
+
|
625
|
+
/**
|
626
|
+
* Closes the neuron detail panel and goes back to whichever table was selected ("Stakes" or "Metagraph").
|
627
|
+
*/
|
628
|
+
function closeNeuronDetails() {
|
629
|
+
// Hide neuron details
|
630
|
+
const detailContainer = document.getElementById('neuron-detail-container');
|
631
|
+
if (detailContainer) detailContainer.style.display = 'none';
|
632
|
+
|
633
|
+
// Show the stakes header with action buttons
|
634
|
+
const stakesHeader = document.querySelector('.stakes-header');
|
635
|
+
if (stakesHeader) stakesHeader.style.display = 'flex';
|
636
|
+
|
637
|
+
// Show the view header again
|
638
|
+
const viewHeader = document.querySelector('.view-header');
|
639
|
+
if (viewHeader) viewHeader.style.display = 'block';
|
640
|
+
|
641
|
+
// Show the appropriate table based on toggle state
|
642
|
+
const showStakes = document.getElementById('stake-toggle').checked;
|
643
|
+
const stakesTable = document.querySelector('.stakes-table-container');
|
644
|
+
const networkTable = document.querySelector('.network-table-container');
|
645
|
+
|
646
|
+
if (showStakes) {
|
647
|
+
stakesTable.style.display = 'block';
|
648
|
+
networkTable.style.display = 'none';
|
649
|
+
|
650
|
+
// Hide action buttons when showing stakes
|
651
|
+
const addStakeButton = document.querySelector('.add-stake-button');
|
652
|
+
const exportCsvButton = document.querySelector('.export-csv-button');
|
653
|
+
if (addStakeButton) addStakeButton.style.display = 'none';
|
654
|
+
if (exportCsvButton) exportCsvButton.style.display = 'none';
|
655
|
+
} else {
|
656
|
+
stakesTable.style.display = 'none';
|
657
|
+
networkTable.style.display = 'block';
|
658
|
+
|
659
|
+
// Show action buttons when showing metagraph
|
660
|
+
const addStakeButton = document.querySelector('.add-stake-button');
|
661
|
+
const exportCsvButton = document.querySelector('.export-csv-button');
|
662
|
+
if (addStakeButton) addStakeButton.style.display = 'block';
|
663
|
+
if (exportCsvButton) exportCsvButton.style.display = 'block';
|
664
|
+
}
|
665
|
+
}
|
666
|
+
|
667
|
+
|
668
|
+
/* ===================== Number Formatting Functions ===================== */
|
669
|
+
/**
|
670
|
+
* Toggles the numeric display between "verbose" and "short" notations
|
671
|
+
* across all .formatted-number elements on the page.
|
672
|
+
*/
|
673
|
+
function toggleVerboseNumbers() {
|
674
|
+
// We read from the main or subnet checkboxes
|
675
|
+
verboseNumbers =
|
676
|
+
document.getElementById('verbose-toggle')?.checked ||
|
677
|
+
document.getElementById('show-verbose')?.checked ||
|
678
|
+
false;
|
679
|
+
|
680
|
+
// Reformat all visible .formatted-number elements
|
681
|
+
document.querySelectorAll('.formatted-number').forEach(element => {
|
682
|
+
const value = parseFloat(element.dataset.value);
|
683
|
+
const symbol = element.dataset.symbol;
|
684
|
+
element.innerHTML = formatNumber(value, symbol);
|
685
|
+
});
|
686
|
+
|
687
|
+
// If we're currently on the Subnet detail page, update those numbers too
|
688
|
+
if (document.getElementById('subnet-page').style.display !== 'none') {
|
689
|
+
updateAllNumbers();
|
690
|
+
}
|
691
|
+
}
|
692
|
+
|
693
|
+
/**
|
694
|
+
* Scans all .formatted-number elements and replaces their text with
|
695
|
+
* the properly formatted version (short or verbose).
|
696
|
+
*/
|
697
|
+
function initializeFormattedNumbers() {
|
698
|
+
document.querySelectorAll('.formatted-number').forEach(element => {
|
699
|
+
const value = parseFloat(element.dataset.value);
|
700
|
+
const symbol = element.dataset.symbol;
|
701
|
+
element.innerHTML = formatNumber(value, symbol);
|
702
|
+
});
|
703
|
+
}
|
704
|
+
|
705
|
+
/**
|
706
|
+
* Called by toggleVerboseNumbers() to reformat key metrics on the Subnet page
|
707
|
+
* that might not be directly wrapped in .formatted-number but need to be updated anyway.
|
708
|
+
*/
|
709
|
+
function updateAllNumbers() {
|
710
|
+
try {
|
711
|
+
const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
|
712
|
+
if (!subnet) {
|
713
|
+
console.error('Could not find subnet data for netuid:', window.currentSubnet);
|
714
|
+
return;
|
715
|
+
}
|
716
|
+
// Reformat a few items in the Subnet detail header
|
717
|
+
document.querySelector('#subnet-market-cap').innerHTML =
|
718
|
+
formatNumber(subnet.market_cap, root_symbol_html);
|
719
|
+
document.querySelector('#subnet-total-stake').innerHTML =
|
720
|
+
formatNumber(subnet.total_stake, subnet.symbol);
|
721
|
+
document.querySelector('#subnet-emission').innerHTML =
|
722
|
+
formatNumber(subnet.emission, root_symbol_html);
|
723
|
+
|
724
|
+
// Reformat the Metagraph table data
|
725
|
+
const netinfo = subnet.metagraph_info;
|
726
|
+
document.querySelector('#network-alpha-in').innerHTML =
|
727
|
+
formatNumber(netinfo.alpha_in, subnet.symbol);
|
728
|
+
document.querySelector('#network-tau-in').innerHTML =
|
729
|
+
formatNumber(netinfo.tao_in, root_symbol_html);
|
730
|
+
|
731
|
+
// Reformat items in "Your Stakes" table
|
732
|
+
document.querySelectorAll('#stakes-table-body .formatted-number').forEach(element => {
|
733
|
+
const value = parseFloat(element.dataset.value);
|
734
|
+
const symbol = element.dataset.symbol;
|
735
|
+
element.innerHTML = formatNumber(value, symbol);
|
736
|
+
});
|
737
|
+
} catch (error) {
|
738
|
+
console.error('Error updating numbers:', error);
|
739
|
+
}
|
740
|
+
}
|
741
|
+
|
742
|
+
/**
|
743
|
+
* Format a numeric value into either:
|
744
|
+
* - a short format (e.g. 1.23k, 3.45m) if verboseNumbers==false
|
745
|
+
* - a more precise format (1,234.5678) if verboseNumbers==true
|
746
|
+
* @param {number} num The numeric value to format.
|
747
|
+
* @param {string} symbol A short suffix or currency symbol (e.g. 'τ') that we append.
|
748
|
+
*/
|
749
|
+
function formatNumber(num, symbol = '') {
|
750
|
+
if (num === undefined || num === null || isNaN(num)) {
|
751
|
+
return '0.00 ' + `<span style="color: #FF9900">${symbol}</span>`;
|
752
|
+
}
|
753
|
+
num = parseFloat(num);
|
754
|
+
if (num === 0) {
|
755
|
+
return '0.00 ' + `<span style="color: #FF9900">${symbol}</span>`;
|
756
|
+
}
|
757
|
+
|
758
|
+
// If user requested verbose
|
759
|
+
if (verboseNumbers) {
|
760
|
+
return num.toLocaleString('en-US', {
|
761
|
+
minimumFractionDigits: 4,
|
762
|
+
maximumFractionDigits: 4
|
763
|
+
}) + ' ' + `<span style="color: #FF9900">${symbol}</span>`;
|
764
|
+
}
|
765
|
+
|
766
|
+
// Otherwise show short scale for large numbers
|
767
|
+
const absNum = Math.abs(num);
|
768
|
+
if (absNum >= 1000) {
|
769
|
+
const suffixes = ['', 'k', 'm', 'b', 't'];
|
770
|
+
const magnitude = Math.min(4, Math.floor(Math.log10(absNum) / 3));
|
771
|
+
const scaledNum = num / Math.pow(10, magnitude * 3);
|
772
|
+
return scaledNum.toFixed(2) + suffixes[magnitude] + ' ' +
|
773
|
+
`<span style="color: #FF9900">${symbol}</span>`;
|
774
|
+
} else {
|
775
|
+
// For small numbers <1000, just show 4 decimals
|
776
|
+
return num.toFixed(4) + ' ' + `<span style="color: #FF9900">${symbol}</span>`;
|
777
|
+
}
|
778
|
+
}
|
779
|
+
|
780
|
+
/**
|
781
|
+
* Truncates a string address into the format "ABC..XYZ" for a bit more readability
|
782
|
+
* @param {string} address
|
783
|
+
* @returns {string} truncated address form
|
784
|
+
*/
|
785
|
+
function truncateAddress(address) {
|
786
|
+
if (!address || address.length <= 7) {
|
787
|
+
return address; // no need to truncate if very short
|
788
|
+
}
|
789
|
+
return `${address.substring(0, 3)}..${address.substring(address.length - 3)}`;
|
790
|
+
}
|
791
|
+
|
792
|
+
/**
|
793
|
+
* Format a number in compact notation (K, M, B) for tile display
|
794
|
+
*/
|
795
|
+
function formatTileNumbers(num) {
|
796
|
+
if (num >= 1000000000) {
|
797
|
+
return (num / 1000000000).toFixed(1) + 'B';
|
798
|
+
} else if (num >= 1000000) {
|
799
|
+
return (num / 1000000).toFixed(1) + 'M';
|
800
|
+
} else if (num >= 1000) {
|
801
|
+
return (num / 1000).toFixed(1) + 'K';
|
802
|
+
} else {
|
803
|
+
return num.toFixed(1);
|
804
|
+
}
|
805
|
+
}
|
806
|
+
|
807
|
+
|
808
|
+
/* ===================== Table Sorting and Filtering Functions ===================== */
|
809
|
+
/**
|
810
|
+
* Switches the Metagraph or Stakes table from sorting ascending to descending on a column, and vice versa.
|
811
|
+
* @param {HTMLTableElement} table The table element itself
|
812
|
+
* @param {number} columnIndex The column index to sort by
|
813
|
+
* @param {HTMLTableHeaderCellElement} header The <th> element clicked
|
814
|
+
* @param {boolean} forceDescending If true and no existing sort marker, will do a descending sort by default
|
815
|
+
*/
|
816
|
+
function sortTable(table, columnIndex, header, forceDescending = false) {
|
817
|
+
const tbody = table.querySelector('tbody');
|
818
|
+
const rows = Array.from(tbody.querySelectorAll('tr'));
|
819
|
+
|
820
|
+
// If forcing descending and the header has no 'data-sort', default to 'desc'
|
821
|
+
let isDescending;
|
822
|
+
if (forceDescending && !header.hasAttribute('data-sort')) {
|
823
|
+
isDescending = true;
|
824
|
+
} else {
|
825
|
+
isDescending = header.getAttribute('data-sort') !== 'desc';
|
826
|
+
}
|
827
|
+
|
828
|
+
// Clear data-sort from all headers in the table
|
829
|
+
table.querySelectorAll('th').forEach(th => {
|
830
|
+
th.removeAttribute('data-sort');
|
831
|
+
});
|
832
|
+
// Mark the clicked header with new direction
|
833
|
+
header.setAttribute('data-sort', isDescending ? 'desc' : 'asc');
|
834
|
+
|
835
|
+
// Sort numerically
|
836
|
+
rows.sort((rowA, rowB) => {
|
837
|
+
const cellA = rowA.cells[columnIndex];
|
838
|
+
const cellB = rowB.cells[columnIndex];
|
839
|
+
|
840
|
+
// Attempt to parse float from data-value or fallback to textContent
|
841
|
+
let valueA = parseFloat(cellA.getAttribute('data-value')) ||
|
842
|
+
parseFloat(cellA.textContent.replace(/[^\\d.-]/g, '')) ||
|
843
|
+
0;
|
844
|
+
let valueB = parseFloat(cellB.getAttribute('data-value')) ||
|
845
|
+
parseFloat(cellB.textContent.replace(/[^\\d.-]/g, '')) ||
|
846
|
+
0;
|
847
|
+
|
848
|
+
return isDescending ? (valueB - valueA) : (valueA - valueB);
|
849
|
+
});
|
850
|
+
|
851
|
+
// Reinsert sorted rows
|
852
|
+
tbody.innerHTML = '';
|
853
|
+
rows.forEach(row => tbody.appendChild(row));
|
854
|
+
}
|
855
|
+
|
856
|
+
/**
|
857
|
+
* Adds sortable behavior to certain columns in the "stakes-table" or "network-table".
|
858
|
+
* Called after these tables are created in showSubnetPage().
|
859
|
+
*/
|
860
|
+
function initializeSorting() {
|
861
|
+
const networkTable = document.querySelector('.network-table');
|
862
|
+
if (networkTable) {
|
863
|
+
initializeTableSorting(networkTable);
|
864
|
+
}
|
865
|
+
const stakesTable = document.querySelector('.stakes-table');
|
866
|
+
if (stakesTable) {
|
867
|
+
initializeTableSorting(stakesTable);
|
868
|
+
}
|
869
|
+
}
|
870
|
+
|
871
|
+
/**
|
872
|
+
* Helper function that attaches sort handlers to appropriate columns in a table.
|
873
|
+
* @param {HTMLTableElement} table The table element to set up sorting for.
|
874
|
+
*/
|
875
|
+
function initializeTableSorting(table) {
|
876
|
+
const headers = table.querySelectorAll('th');
|
877
|
+
headers.forEach((header, index) => {
|
878
|
+
// We only want some columns to be sortable, as in original code
|
879
|
+
if (table.classList.contains('stakes-table') && index >= 1 && index <= 5) {
|
880
|
+
header.classList.add('sortable');
|
881
|
+
header.addEventListener('click', () => {
|
882
|
+
sortTable(table, index, header, true);
|
883
|
+
});
|
884
|
+
} else if (table.classList.contains('network-table') && index < 6) {
|
885
|
+
header.classList.add('sortable');
|
886
|
+
header.addEventListener('click', () => {
|
887
|
+
sortTable(table, index, header, true);
|
888
|
+
});
|
889
|
+
}
|
890
|
+
});
|
891
|
+
}
|
892
|
+
|
893
|
+
/**
|
894
|
+
* Filters rows in the Metagraph table by name, hotkey, or coldkey.
|
895
|
+
* Invoked by the oninput event of the #network-search field.
|
896
|
+
* @param {string} searchValue The substring typed by the user.
|
897
|
+
*/
|
898
|
+
function filterNetworkTable(searchValue) {
|
899
|
+
const searchTerm = searchValue.toLowerCase().trim();
|
900
|
+
const rows = document.querySelectorAll('.network-table tbody tr');
|
901
|
+
|
902
|
+
rows.forEach(row => {
|
903
|
+
const nameCell = row.querySelector('.identity-cell');
|
904
|
+
const hotkeyContainer = row.querySelector('.hotkey-container[data-full-address]');
|
905
|
+
const coldkeyContainer = row.querySelectorAll('.hotkey-container[data-full-address]')[1];
|
906
|
+
|
907
|
+
const name = nameCell ? nameCell.textContent.toLowerCase() : '';
|
908
|
+
const hotkey = hotkeyContainer ? hotkeyContainer.getAttribute('data-full-address').toLowerCase() : '';
|
909
|
+
const coldkey= coldkeyContainer ? coldkeyContainer.getAttribute('data-full-address').toLowerCase() : '';
|
910
|
+
|
911
|
+
const matches = (name.includes(searchTerm) || hotkey.includes(searchTerm) || coldkey.includes(searchTerm));
|
912
|
+
row.style.display = matches ? '' : 'none';
|
913
|
+
});
|
914
|
+
}
|
915
|
+
|
916
|
+
|
917
|
+
/* ===================== Network Visualization Functions ===================== */
|
918
|
+
/**
|
919
|
+
* Initializes the network visualization on the canvas element.
|
920
|
+
*/
|
921
|
+
function initNetworkVisualization() {
|
922
|
+
try {
|
923
|
+
const canvas = document.getElementById('network-canvas');
|
924
|
+
if (!canvas) {
|
925
|
+
console.error('Canvas element (#network-canvas) not found');
|
926
|
+
return;
|
927
|
+
}
|
928
|
+
const ctx = canvas.getContext('2d');
|
929
|
+
|
930
|
+
const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
|
931
|
+
if (!subnet) {
|
932
|
+
console.error('Could not find subnet data for netuid:', window.currentSubnet);
|
933
|
+
return;
|
934
|
+
}
|
935
|
+
const numNeurons = subnet.metagraph_info.num_uids;
|
936
|
+
const nodes = [];
|
937
|
+
|
938
|
+
// Randomly place nodes, each with a small velocity
|
939
|
+
for (let i = 0; i < numNeurons; i++) {
|
940
|
+
nodes.push({
|
941
|
+
x: Math.random() * canvas.width,
|
942
|
+
y: Math.random() * canvas.height,
|
943
|
+
radius: 2,
|
944
|
+
vx: (Math.random() - 0.5) * 0.5,
|
945
|
+
vy: (Math.random() - 0.5) * 0.5
|
946
|
+
});
|
947
|
+
}
|
948
|
+
|
949
|
+
// Animation loop
|
950
|
+
function animate() {
|
951
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
952
|
+
|
953
|
+
ctx.beginPath();
|
954
|
+
ctx.strokeStyle = 'rgba(255, 153, 0, 0.2)';
|
955
|
+
for (let i = 0; i < nodes.length; i++) {
|
956
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
957
|
+
const dx = nodes[i].x - nodes[j].x;
|
958
|
+
const dy = nodes[i].y - nodes[j].y;
|
959
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
960
|
+
if (distance < 30) {
|
961
|
+
ctx.moveTo(nodes[i].x, nodes[i].y);
|
962
|
+
ctx.lineTo(nodes[j].x, nodes[j].y);
|
963
|
+
}
|
964
|
+
}
|
965
|
+
}
|
966
|
+
ctx.stroke();
|
967
|
+
|
968
|
+
nodes.forEach(node => {
|
969
|
+
node.x += node.vx;
|
970
|
+
node.y += node.vy;
|
971
|
+
|
972
|
+
// Bounce them off the edges
|
973
|
+
if (node.x <= 0 || node.x >= canvas.width) node.vx *= -1;
|
974
|
+
if (node.y <= 0 || node.y >= canvas.height) node.vy *= -1;
|
975
|
+
|
976
|
+
ctx.beginPath();
|
977
|
+
ctx.fillStyle = '#FF9900';
|
978
|
+
ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
|
979
|
+
ctx.fill();
|
980
|
+
});
|
981
|
+
|
982
|
+
requestAnimationFrame(animate);
|
983
|
+
}
|
984
|
+
animate();
|
985
|
+
} catch (error) {
|
986
|
+
console.error('Error in network visualization:', error);
|
987
|
+
}
|
988
|
+
}
|
989
|
+
|
990
|
+
|
991
|
+
/* ===================== Tile View Functions ===================== */
|
992
|
+
/**
|
993
|
+
* Toggles between the tile view and table view of subnets.
|
994
|
+
*/
|
995
|
+
function toggleTileView() {
|
996
|
+
const showTiles = document.getElementById('show-tiles').checked;
|
997
|
+
const tilesContainer = document.getElementById('subnet-tiles-container');
|
998
|
+
const tableContainer = document.querySelector('.subnets-table-container');
|
999
|
+
|
1000
|
+
if (showTiles) {
|
1001
|
+
// Show tiles, hide table
|
1002
|
+
tilesContainer.style.display = 'flex';
|
1003
|
+
tableContainer.style.display = 'none';
|
1004
|
+
|
1005
|
+
// Generate tiles if they don't exist yet
|
1006
|
+
if (tilesContainer.children.length === 0) {
|
1007
|
+
generateSubnetTiles();
|
1008
|
+
}
|
1009
|
+
|
1010
|
+
// Apply current filters to the tiles
|
1011
|
+
filterSubnets();
|
1012
|
+
} else {
|
1013
|
+
// Show table, hide tiles
|
1014
|
+
tilesContainer.style.display = 'none';
|
1015
|
+
tableContainer.style.display = 'block';
|
1016
|
+
}
|
1017
|
+
}
|
1018
|
+
|
1019
|
+
/**
|
1020
|
+
* Generates the subnet tiles based on the initialData.
|
1021
|
+
*/
|
1022
|
+
function generateSubnetTiles() {
|
1023
|
+
const tilesContainer = document.getElementById('subnet-tiles-container');
|
1024
|
+
tilesContainer.innerHTML = ''; // Clear existing tiles
|
1025
|
+
|
1026
|
+
// Sort subnets by market cap (descending)
|
1027
|
+
const sortedSubnets = [...window.initialData.subnets].sort((a, b) => b.market_cap - a.market_cap);
|
1028
|
+
|
1029
|
+
sortedSubnets.forEach(subnet => {
|
1030
|
+
const isStaked = subnet.your_stakes && subnet.your_stakes.length > 0;
|
1031
|
+
const marketCapFormatted = formatTileNumbers(subnet.market_cap);
|
1032
|
+
|
1033
|
+
const tile = document.createElement('div');
|
1034
|
+
tile.className = `subnet-tile ${isStaked ? 'staked' : ''}`;
|
1035
|
+
tile.onclick = () => showSubnetPage(subnet.netuid);
|
1036
|
+
|
1037
|
+
// Calculate background intensity based on market cap relative to max
|
1038
|
+
const maxMarketCap = sortedSubnets[0].market_cap;
|
1039
|
+
const intensity = Math.max(5, Math.min(15, 5 + (subnet.market_cap / maxMarketCap) * 10));
|
1040
|
+
|
1041
|
+
tile.innerHTML = `
|
1042
|
+
<span class="tile-netuid">${subnet.netuid}</span>
|
1043
|
+
<span class="tile-symbol">${subnet.symbol}</span>
|
1044
|
+
<span class="tile-name">${subnet.name}</span>
|
1045
|
+
<span class="tile-market-cap">${marketCapFormatted} ${root_symbol_html}</span>
|
1046
|
+
`;
|
1047
|
+
|
1048
|
+
// Set background intensity
|
1049
|
+
tile.style.background = `rgba(255, 255, 255, 0.0${intensity.toFixed(0)})`;
|
1050
|
+
|
1051
|
+
tilesContainer.appendChild(tile);
|
1052
|
+
});
|
1053
|
+
}
|