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.
@@ -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
+ }