feat: Add comprehensive staking interface to PaperclipWallet

- Add staking RPC methods for validators, delegation, and rewards
- Implement complete staking UI with validator selection and delegation
- Add reward claiming functionality and validator creation interface
- Include professional staking dashboard with real-time data
- Integrate staking navigation into existing wallet interface
This commit is contained in:
2025-06-17 14:38:53 -07:00
parent 8414593d47
commit 2c3ad62bc4
6 changed files with 1242 additions and 0 deletions

386
assets/styles/staking.css Normal file
View File

@@ -0,0 +1,386 @@
/* PaperclipChain Staking Interface Styles */
.staking-container {
padding: 20px;
background: #f5f5f5;
min-height: 100vh;
}
.staking-overview {
display: flex;
gap: 20px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.staking-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 12px;
flex: 1;
min-width: 200px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
}
.staking-card:hover {
transform: translateY(-5px);
}
.staking-card h3 {
margin: 0 0 10px 0;
font-size: 14px;
opacity: 0.9;
text-transform: uppercase;
letter-spacing: 1px;
}
.staking-card .amount {
font-size: 24px;
font-weight: bold;
margin: 0;
}
.staking-card .subtitle {
font-size: 12px;
opacity: 0.8;
margin-top: 5px;
}
.staking-card.success {
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
}
.staking-card.warning {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
}
.staking-card.info {
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
}
.quick-actions {
background: white;
padding: 20px;
border-radius: 12px;
margin-bottom: 30px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.quick-actions h3 {
margin-top: 0;
color: #333;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
.action-buttons {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.btn-staking {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.btn-staking:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
color: white;
}
.btn-staking.warning {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
}
.btn-staking.warning:hover {
box-shadow: 0 5px 15px rgba(255, 152, 0, 0.4);
}
.btn-staking.success {
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
}
.btn-staking.success:hover {
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
}
.btn-staking.info {
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
}
.btn-staking.info:hover {
box-shadow: 0 5px 15px rgba(33, 150, 243, 0.4);
}
.validators-section {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.validators-section h3 {
margin-top: 0;
color: #333;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
.refresh-btn {
background: #f8f9fa;
border: 1px solid #dee2e6;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: background 0.3s ease;
}
.refresh-btn:hover {
background: #e9ecef;
}
.validators-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.validators-table th,
.validators-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.validators-table th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
border-top: 1px solid #eee;
}
.validators-table tbody tr:hover {
background: #f8f9fa;
}
.validator-info {
display: flex;
flex-direction: column;
}
.validator-info strong {
color: #333;
}
.validator-info small {
color: #6c757d;
font-size: 11px;
}
.status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
}
.status-badge.active {
background: #d4edda;
color: #155724;
}
.status-badge.inactive {
background: #f8d7da;
color: #721c24;
}
.btn-validator-action {
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
margin-right: 5px;
transition: all 0.3s ease;
}
.btn-validator-action.delegate {
background: #007bff;
color: white;
}
.btn-validator-action.delegate:hover {
background: #0056b3;
}
.btn-validator-action.undelegate {
background: #ffc107;
color: #212529;
}
.btn-validator-action.undelegate:hover {
background: #e0a800;
}
.btn-validator-action:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Modal Styles */
.staking-modal .modal-body {
padding: 20px;
}
.staking-modal .form-group {
margin-bottom: 20px;
}
.staking-modal label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.staking-modal input,
.staking-modal select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.staking-modal input:focus,
.staking-modal select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.staking-modal .help-text {
font-size: 12px;
color: #6c757d;
margin-top: 5px;
}
.staking-modal .alert {
padding: 10px 15px;
border-radius: 6px;
margin-bottom: 15px;
}
.staking-modal .alert.warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
}
.staking-modal .alert.info {
background: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
.modal-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
}
.modal-buttons .btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
}
.modal-buttons .btn-secondary {
background: #6c757d;
color: white;
}
.modal-buttons .btn-primary {
background: #667eea;
color: white;
}
.modal-buttons .btn:hover {
opacity: 0.9;
}
/* Responsive Design */
@media (max-width: 768px) {
.staking-overview {
flex-direction: column;
}
.action-buttons {
flex-direction: column;
}
.validators-table {
font-size: 12px;
}
.validators-table th,
.validators-table td {
padding: 8px;
}
}
/* Loading States */
.loading {
text-align: center;
padding: 40px;
color: #6c757d;
}
.loading i {
font-size: 24px;
margin-bottom: 10px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Success/Error Messages */
.message {
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}

View File

@@ -0,0 +1,214 @@
<!-- PaperclipChain Staking Interface -->
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-header">
<h2>Staking & Delegation
<small>Earn rewards by staking your CLIPS</small>
</h2>
</div>
<div class="card-body">
<!-- Staking Overview -->
<div class="row mb-4">
<div class="col-md-3">
<div class="info-box bg-clips">
<div class="info-box-icon">
<i class="fas fa-coins"></i>
</div>
<div class="info-box-content">
<span class="info-box-text">Total Staked</span>
<span class="info-box-number" id="totalStakedAmount">0 CLIPS</span>
<div class="progress">
<div class="progress-bar" style="width: 0%" id="totalStakedProgress"></div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box bg-success">
<div class="info-box-icon">
<i class="fas fa-trophy"></i>
</div>
<div class="info-box-content">
<span class="info-box-text">Pending Rewards</span>
<span class="info-box-number" id="pendingRewards">0 CLIPS</span>
<div class="progress">
<div class="progress-bar bg-success" style="width: 0%" id="rewardsProgress"></div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box bg-warning">
<div class="info-box-icon">
<i class="fas fa-users"></i>
</div>
<div class="info-box-content">
<span class="info-box-text">Active Validators</span>
<span class="info-box-number" id="activeValidators">0</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="info-box bg-info">
<div class="info-box-icon">
<i class="fas fa-percentage"></i>
</div>
<div class="info-box-content">
<span class="info-box-text">Network APY</span>
<span class="info-box-number" id="networkAPY">~8%</span>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Quick Actions</h3>
</div>
<div class="card-body">
<div class="btn-group" role="group">
<button type="button" class="btn btn-clips" id="btnDelegate">
<i class="fas fa-arrow-up"></i> Delegate
</button>
<button type="button" class="btn btn-warning" id="btnUndelegate">
<i class="fas fa-arrow-down"></i> Undelegate
</button>
<button type="button" class="btn btn-success" id="btnClaimRewards">
<i class="fas fa-gift"></i> Claim Rewards
</button>
<button type="button" class="btn btn-info" id="btnCreateValidator">
<i class="fas fa-plus"></i> Become Validator
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Validators List -->
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Validators</h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" id="btnRefreshValidators" data-toggle="tooltip" title="Refresh">
<i class="fas fa-sync"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-bordered" id="validatorsTable">
<thead>
<tr>
<th>Validator</th>
<th>Status</th>
<th>Self Stake</th>
<th>Delegated</th>
<th>Commission</th>
<th>APY</th>
<th>Your Stake</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="validatorsTableBody">
<!-- Validators will be populated here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Delegate Modal -->
<div id="dlgDelegate" class="modalDialog" data-izimodal-title="Delegate to Validator" data-izimodal-subtitle="Stake your CLIPS to earn rewards" data-izimodal-icon="icon-home">
<div class="modalBody">
<div class="form-group">
<label for="delegateValidator">Select Validator:</label>
<select class="form-control" id="delegateValidator">
<option value="">Select a validator...</option>
</select>
</div>
<div class="form-group">
<label for="delegateAmount">Amount to Delegate (CLIPS):</label>
<input type="number" class="form-control" id="delegateAmount" placeholder="Minimum 100 CLIPS" min="100" step="0.000001">
<small class="form-text text-muted">Available balance: <span id="availableBalance">0 CLIPS</span></small>
</div>
<div class="form-group">
<label for="delegateWallet">From Wallet:</label>
<select class="form-control" id="delegateWallet">
<!-- Wallets will be populated here -->
</select>
</div>
<button type="button" class="btn btn-secondary btn-dialog-cancel" id="btnDelegateCancel">Cancel</button>
<button type="button" class="btn btn-clips btn-dialog-confirm" id="btnDelegateConfirm">Delegate</button>
</div>
</div>
<!-- Undelegate Modal -->
<div id="dlgUndelegate" class="modalDialog" data-izimodal-title="Undelegate from Validator" data-izimodal-subtitle="Withdraw your staked CLIPS" data-izimodal-icon="icon-home">
<div class="modalBody">
<div class="form-group">
<label for="undelegateValidator">Validator:</label>
<select class="form-control" id="undelegateValidator">
<option value="">Select a validator...</option>
</select>
</div>
<div class="form-group">
<label for="undelegateAmount">Amount to Undelegate (CLIPS):</label>
<input type="number" class="form-control" id="undelegateAmount" placeholder="Enter amount" min="0.000001" step="0.000001">
<small class="form-text text-muted">Your stake: <span id="yourStakeAmount">0 CLIPS</span></small>
</div>
<div class="form-group">
<label for="undelegateWallet">To Wallet:</label>
<select class="form-control" id="undelegateWallet">
<!-- Wallets will be populated here -->
</select>
</div>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i>
<strong>Note:</strong> Undelegated funds may have a cooldown period before they become available.
</div>
<button type="button" class="btn btn-secondary btn-dialog-cancel" id="btnUndelegateCancel">Cancel</button>
<button type="button" class="btn btn-warning btn-dialog-confirm" id="btnUndelegateConfirm">Undelegate</button>
</div>
</div>
<!-- Create Validator Modal -->
<div id="dlgCreateValidator" class="modalDialog" data-izimodal-title="Become a Validator" data-izimodal-subtitle="Create a new validator node" data-izimodal-icon="icon-home">
<div class="modalBody">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<strong>Requirements:</strong> Minimum 10,000 CLIPS stake required to create a validator.
</div>
<div class="form-group">
<label for="validatorStake">Initial Stake (CLIPS):</label>
<input type="number" class="form-control" id="validatorStake" placeholder="Minimum 10,000 CLIPS" min="10000" step="0.000001">
</div>
<div class="form-group">
<label for="validatorCommission">Commission Rate (%):</label>
<input type="number" class="form-control" id="validatorCommission" placeholder="0-50%" min="0" max="50" step="0.01" value="5">
<small class="form-text text-muted">Percentage of rewards you'll keep as commission</small>
</div>
<div class="form-group">
<label for="validatorWallet">Validator Wallet:</label>
<select class="form-control" id="validatorWallet">
<!-- Wallets will be populated here -->
</select>
</div>
<button type="button" class="btn btn-secondary btn-dialog-cancel" id="btnCreateValidatorCancel">Cancel</button>
<button type="button" class="btn btn-info btn-dialog-confirm" id="btnCreateValidatorConfirm">Create Validator</button>
</div>
</div>

View File

@@ -19,6 +19,7 @@
<link rel="stylesheet" href="./assets/styles/style.css"> <link rel="stylesheet" href="./assets/styles/style.css">
<link rel="stylesheet" href="./assets/styles/forms.css"> <link rel="stylesheet" href="./assets/styles/forms.css">
<link rel="stylesheet" href="./assets/styles/about.css"> <link rel="stylesheet" href="./assets/styles/about.css">
<link rel="stylesheet" href="./assets/styles/staking.css">
<!-- Insert this line above script imports --> <!-- Insert this line above script imports -->
<script> <script>
if (typeof module === 'object') { if (typeof module === 'object') {
@@ -124,6 +125,10 @@
<i class="fas fa-exchange-alt fa-1x"></i> <i class="fas fa-exchange-alt fa-1x"></i>
<span class="txlist"> Transactions </span> <span class="txlist"> Transactions </span>
</a> </a>
<a class="item" style="padding-top: 30px; padding-left: 13px" id="mainNavBtnStaking" href="#" data-tippy="Staking & Delegation" data-tippy-delay="100">
<i class="fas fa-coins fa-1x"></i>
<span class="staking"> Staking </span>
</a>
<a class="item" style="padding-top: 30px; padding-left: 13px" id="mainNavBtnMarkets" href="#" data-tippy="Markets" data-tippy-delay="100"> <a class="item" style="padding-top: 30px; padding-left: 13px" id="mainNavBtnMarkets" href="#" data-tippy="Markets" data-tippy-delay="100">
<i class="fas fa-poll fa-1x"></i> <i class="fas fa-poll fa-1x"></i>
<span class="Markets"> Market </span> <span class="Markets"> Market </span>
@@ -157,6 +162,7 @@
require('./renderer/addressBook.js'); require('./renderer/addressBook.js');
require('./renderer/transactions.js'); require('./renderer/transactions.js');
require('./renderer/tableTransactions.js'); require('./renderer/tableTransactions.js');
require('./renderer/staking.js');
</script> </script>
</div> </div>

View File

@@ -199,6 +199,93 @@ class PaperclipRPC {
} }
} }
// Staking Methods
async getValidators() {
try {
const result = await this._makeRPCCall("abci_query", {
path: `"validators"`
});
if (result.result && result.result.response && result.result.response.value) {
const validators = Buffer.from(result.result.response.value, 'base64').toString();
return JSON.parse(validators);
}
return [];
} catch (error) {
this._writeLog(`Failed to get validators: ${error.message}`);
return [];
}
}
async getStakingInfo(address) {
try {
const result = await this._makeRPCCall("abci_query", {
path: `"staker:${address}"`
});
if (result.result && result.result.response && result.result.response.value) {
const stakingInfo = Buffer.from(result.result.response.value, 'base64').toString();
return JSON.parse(stakingInfo);
}
return null;
} catch (error) {
this._writeLog(`Failed to get staking info for ${address}: ${error.message}`);
return null;
}
}
async getValidatorInfo(validatorAddress) {
try {
const result = await this._makeRPCCall("abci_query", {
path: `"validator:${validatorAddress}"`
});
if (result.result && result.result.response && result.result.response.value) {
const validatorInfo = Buffer.from(result.result.response.value, 'base64').toString();
return JSON.parse(validatorInfo);
}
return null;
} catch (error) {
this._writeLog(`Failed to get validator info for ${validatorAddress}: ${error.message}`);
return null;
}
}
async getStakingRewards(address) {
try {
const result = await this._makeRPCCall("abci_query", {
path: `"rewards:${address}"`
});
if (result.result && result.result.response && result.result.response.value) {
const rewards = Buffer.from(result.result.response.value, 'base64').toString();
return parseInt(rewards) || 0;
}
return 0;
} catch (error) {
this._writeLog(`Failed to get staking rewards for ${address}: ${error.message}`);
return 0;
}
}
async getTotalStaked() {
try {
const result = await this._makeRPCCall("abci_query", {
path: `"total_staked"`
});
if (result.result && result.result.response && result.result.response.value) {
const totalStaked = Buffer.from(result.result.response.value, 'base64').toString();
return parseInt(totalStaked) || 0;
}
return 0;
} catch (error) {
this._writeLog(`Failed to get total staked: ${error.message}`);
return 0;
}
}
setRPCUrl(url) { setRPCUrl(url) {
this.rpcUrl = url; this.rpcUrl = url;
this._writeLog(`RPC URL updated to: ${url}`); this._writeLog(`RPC URL updated to: ${url}`);
@@ -254,4 +341,25 @@ ipcMain.handle("paperclip-get-block", async (event, height) => {
return await PaperclipNode.getBlock(height); return await PaperclipNode.getBlock(height);
}); });
// Staking IPC handlers
ipcMain.handle("paperclip-get-validators", async () => {
return await PaperclipNode.getValidators();
});
ipcMain.handle("paperclip-get-staking-info", async (event, address) => {
return await PaperclipNode.getStakingInfo(address);
});
ipcMain.handle("paperclip-get-validator-info", async (event, validatorAddress) => {
return await PaperclipNode.getValidatorInfo(validatorAddress);
});
ipcMain.handle("paperclip-get-staking-rewards", async (event, address) => {
return await PaperclipNode.getStakingRewards(address);
});
ipcMain.handle("paperclip-get-total-staked", async () => {
return await PaperclipNode.getTotalStaked();
});
module.exports = PaperclipNode; module.exports = PaperclipNode;

View File

@@ -25,6 +25,9 @@ class MainGUI {
case "transactions": case "transactions":
$("#mainNavBtnTransactionsWrapper").addClass("iconSelected"); $("#mainNavBtnTransactionsWrapper").addClass("iconSelected");
break; break;
case "staking":
$("#mainNavBtnStakingWrapper").addClass("iconSelected");
break;
case "markets": case "markets":
$("#mainNavBtnMarketsWrapper").addClass("iconSelected"); $("#mainNavBtnMarketsWrapper").addClass("iconSelected");
break; break;
@@ -122,6 +125,11 @@ $("#mainNavBtnTransactions").click(function () {
EthoTransactions.renderTransactions(); EthoTransactions.renderTransactions();
}); });
$("#mainNavBtnStaking").click(function () {
EthoMainGUI.changeAppState("staking");
PaperclipStaking.showStakingPage();
});
$("#mainNavBtnAddressBoook").click(function () { $("#mainNavBtnAddressBoook").click(function () {
EthoMainGUI.changeAppState("addressBook"); EthoMainGUI.changeAppState("addressBook");
EthoAddressBook.renderAddressBook(); EthoAddressBook.renderAddressBook();

520
renderer/staking.js Normal file
View File

@@ -0,0 +1,520 @@
// PaperclipChain Staking Interface
// Handles all staking-related functionality in the wallet
const { ipcRenderer } = require('electron');
class StakingManager {
constructor() {
this.validators = [];
this.stakingInfo = null;
this.totalStaked = 0;
this.pendingRewards = 0;
this.refreshInterval = null;
}
async init() {
console.log('Initializing staking manager...');
await this.loadStakingData();
this.setupEventHandlers();
this.startAutoRefresh();
}
async loadStakingData() {
try {
// Load validators
this.validators = await ipcRenderer.invoke('paperclip-get-validators');
// Load total staked
this.totalStaked = await ipcRenderer.invoke('paperclip-get-total-staked');
// Load user staking info if wallet is selected
if (ClipsWallet.addressList.length > 0) {
const currentAddress = ClipsWallet.addressList[0]; // Use first wallet as current
this.stakingInfo = await ipcRenderer.invoke('paperclip-get-staking-info', currentAddress);
this.pendingRewards = await ipcRenderer.invoke('paperclip-get-staking-rewards', currentAddress);
}
this.updateUI();
} catch (error) {
console.error('Failed to load staking data:', error);
this.showError('Failed to load staking data: ' + error.message);
}
}
updateUI() {
// Update overview cards
$('#totalStakedAmount').text(this.formatClips(this.totalStaked));
$('#pendingRewards').text(this.formatClips(this.pendingRewards));
$('#activeValidators').text(this.validators.filter(v => v.active).length);
// Update validators table
this.updateValidatorsTable();
// Update progress bars
this.updateProgressBars();
}
updateValidatorsTable() {
const tbody = $('#validatorsTableBody');
tbody.empty();
this.validators.forEach(validator => {
const apy = this.calculateValidatorAPY(validator);
const userStake = this.getUserStakeForValidator(validator.address);
const status = validator.active ?
'<span class="badge badge-success">Active</span>' :
'<span class="badge badge-danger">Inactive</span>';
const row = `
<tr data-validator="${validator.address}">
<td>
<div class="validator-info">
<strong>${validator.address.substring(0, 12)}...</strong>
<br><small class="text-muted">${validator.address}</small>
</div>
</td>
<td>${status}</td>
<td>${this.formatClips(validator.stakedAmount || 0)}</td>
<td>${this.formatClips(validator.delegatedAmount || 0)}</td>
<td>${(validator.commission / 100).toFixed(2)}%</td>
<td>~${apy.toFixed(2)}%</td>
<td>${this.formatClips(userStake)}</td>
<td>
<div class="btn-group-sm">
<button class="btn btn-sm btn-clips btn-delegate" data-validator="${validator.address}" ${!validator.active ? 'disabled' : ''}>
Delegate
</button>
${userStake > 0 ? `<button class="btn btn-sm btn-warning btn-undelegate" data-validator="${validator.address}">Undelegate</button>` : ''}
</div>
</td>
</tr>
`;
tbody.append(row);
});
// Initialize DataTable if not already initialized
if (!$.fn.DataTable.isDataTable('#validatorsTable')) {
$('#validatorsTable').DataTable({
pageLength: 10,
responsive: true,
order: [[2, 'desc']] // Sort by self stake desc
});
} else {
$('#validatorsTable').DataTable().draw();
}
}
updateProgressBars() {
// Calculate total network stake percentage (mock calculation)
const networkSupply = 1000000000; // 1B CLIPS total supply (mock)
const stakedPercent = (this.totalStaked / networkSupply) * 100;
$('#totalStakedProgress').css('width', Math.min(stakedPercent, 100) + '%');
// Update rewards progress (mock)
const rewardsPercent = Math.min((this.pendingRewards / 1000) * 100, 100);
$('#rewardsProgress').css('width', rewardsPercent + '%');
}
setupEventHandlers() {
// Navigation
$('#mainNavBtnStaking').off('click').on('click', () => {
this.showStakingPage();
});
// Refresh button
$('#btnRefreshValidators').off('click').on('click', () => {
this.loadStakingData();
});
// Quick action buttons
$('#btnDelegate').off('click').on('click', () => {
this.showDelegateModal();
});
$('#btnUndelegate').off('click').on('click', () => {
this.showUndelegateModal();
});
$('#btnClaimRewards').off('click').on('click', () => {
this.claimRewards();
});
$('#btnCreateValidator').off('click').on('click', () => {
this.showCreateValidatorModal();
});
// Table action buttons (delegated event handling)
$(document).off('click', '.btn-delegate').on('click', '.btn-delegate', (e) => {
const validatorAddress = $(e.target).data('validator');
this.showDelegateModal(validatorAddress);
});
$(document).off('click', '.btn-undelegate').on('click', '.btn-undelegate', (e) => {
const validatorAddress = $(e.target).data('validator');
this.showUndelegateModal(validatorAddress);
});
// Modal handlers
$('#btnDelegateConfirm').off('click').on('click', () => {
this.performDelegation();
});
$('#btnUndelegateConfirm').off('click').on('click', () => {
this.performUndelegation();
});
$('#btnCreateValidatorConfirm').off('click').on('click', () => {
this.createValidator();
});
// Modal cancel buttons
$('#btnDelegateCancel, #btnUndelegateCancel, #btnCreateValidatorCancel').off('click').on('click', function() {
$(this).closest('.modalDialog').iziModal('close');
});
}
showStakingPage() {
EthoMainGUI.renderTemplate('staking.html', {});
this.loadStakingData();
this.setupEventHandlers();
}
showDelegateModal(validatorAddress = null) {
// Populate validator dropdown
const validatorSelect = $('#delegateValidator');
validatorSelect.empty().append('<option value="">Select a validator...</option>');
this.validators.filter(v => v.active).forEach(validator => {
const option = `<option value="${validator.address}">${validator.address.substring(0, 20)}... (${(validator.commission / 100).toFixed(2)}% commission)</option>`;
validatorSelect.append(option);
});
if (validatorAddress) {
validatorSelect.val(validatorAddress);
}
// Populate wallet dropdown
this.populateWalletDropdown('#delegateWallet');
// Update available balance
this.updateAvailableBalance('#availableBalance');
$('#dlgDelegate').iziModal('open');
}
showUndelegateModal(validatorAddress = null) {
// Populate validator dropdown with only validators user has stake with
const validatorSelect = $('#undelegateValidator');
validatorSelect.empty().append('<option value="">Select a validator...</option>');
this.validators.forEach(validator => {
const userStake = this.getUserStakeForValidator(validator.address);
if (userStake > 0) {
const option = `<option value="${validator.address}">${validator.address.substring(0, 20)}... (${this.formatClips(userStake)} staked)</option>`;
validatorSelect.append(option);
}
});
if (validatorAddress) {
validatorSelect.val(validatorAddress);
this.updateYourStakeAmount(validatorAddress);
}
// Populate wallet dropdown
this.populateWalletDropdown('#undelegateWallet');
// Event handler for validator selection change
validatorSelect.off('change').on('change', (e) => {
this.updateYourStakeAmount(e.target.value);
});
$('#dlgUndelegate').iziModal('open');
}
showCreateValidatorModal() {
// Populate wallet dropdown
this.populateWalletDropdown('#validatorWallet');
$('#dlgCreateValidator').iziModal('open');
}
async performDelegation() {
try {
const validatorAddress = $('#delegateValidator').val();
const amount = parseFloat($('#delegateAmount').val());
const walletAddress = $('#delegateWallet').val();
if (!validatorAddress || !amount || !walletAddress || amount < 100) {
this.showError('Please fill all fields correctly. Minimum delegation is 100 CLIPS.');
return;
}
if (!ClipsWallet.getAddressExists(walletAddress)) {
this.showError('Selected wallet not found.');
return;
}
// Create delegation transaction
const txData = {
sender: walletAddress,
receiver: validatorAddress,
amount: Math.floor(amount * 1000000), // Convert to micro-CLIPS
txType: 'delegate',
gas: 30000,
gasPrice: 1
};
// Sign and broadcast transaction
const result = await this.sendStakingTransaction(txData, { address: walletAddress });
if (result.success) {
this.showSuccess('Delegation successful!');
$('#dlgDelegate').iziModal('close');
await this.loadStakingData();
} else {
this.showError('Delegation failed: ' + result.error);
}
} catch (error) {
console.error('Delegation failed:', error);
this.showError('Delegation failed: ' + error.message);
}
}
async performUndelegation() {
try {
const validatorAddress = $('#undelegateValidator').val();
const amount = parseFloat($('#undelegateAmount').val());
const walletAddress = $('#undelegateWallet').val();
if (!validatorAddress || !amount || !walletAddress || amount <= 0) {
this.showError('Please fill all fields correctly.');
return;
}
if (!ClipsWallet.getAddressExists(walletAddress)) {
this.showError('Selected wallet not found.');
return;
}
// Create undelegation transaction
const txData = {
sender: walletAddress,
receiver: validatorAddress,
amount: Math.floor(amount * 1000000), // Convert to micro-CLIPS
txType: 'undelegate',
gas: 30000,
gasPrice: 1
};
// Sign and broadcast transaction
const result = await this.sendStakingTransaction(txData, { address: walletAddress });
if (result.success) {
this.showSuccess('Undelegation successful!');
$('#dlgUndelegate').iziModal('close');
await this.loadStakingData();
} else {
this.showError('Undelegation failed: ' + result.error);
}
} catch (error) {
console.error('Undelegation failed:', error);
this.showError('Undelegation failed: ' + error.message);
}
}
async createValidator() {
try {
const stakeAmount = parseFloat($('#validatorStake').val());
const commission = parseFloat($('#validatorCommission').val());
const walletAddress = $('#validatorWallet').val();
if (!stakeAmount || !walletAddress || stakeAmount < 10000 || commission < 0 || commission > 50) {
this.showError('Please fill all fields correctly. Minimum stake is 10,000 CLIPS, commission must be 0-50%.');
return;
}
if (!ClipsWallet.getAddressExists(walletAddress)) {
this.showError('Selected wallet not found.');
return;
}
// Create validator creation transaction
const txData = {
sender: walletAddress,
receiver: '',
amount: Math.floor(stakeAmount * 1000000), // Convert to micro-CLIPS
txType: 'create_validator',
gas: 30000,
gasPrice: 1,
data: JSON.stringify({ commission: Math.floor(commission * 100) }) // Convert to basis points
};
// Sign and broadcast transaction
const result = await this.sendStakingTransaction(txData, { address: walletAddress });
if (result.success) {
this.showSuccess('Validator created successfully!');
$('#dlgCreateValidator').iziModal('close');
await this.loadStakingData();
} else {
this.showError('Validator creation failed: ' + result.error);
}
} catch (error) {
console.error('Validator creation failed:', error);
this.showError('Validator creation failed: ' + error.message);
}
}
async claimRewards() {
try {
if (ClipsWallet.addressList.length === 0) {
this.showError('No wallet available.');
return;
}
if (this.pendingRewards <= 0) {
this.showError('No rewards to claim.');
return;
}
const currentAddress = ClipsWallet.addressList[0];
// Create claim rewards transaction
const txData = {
sender: currentAddress,
receiver: '',
amount: 0,
txType: 'claim_rewards',
gas: 30000,
gasPrice: 1
};
// Sign and broadcast transaction
const result = await this.sendStakingTransaction(txData, { address: currentAddress });
if (result.success) {
this.showSuccess('Rewards claimed successfully!');
await this.loadStakingData();
} else {
this.showError('Claim failed: ' + result.error);
}
} catch (error) {
console.error('Claim failed:', error);
this.showError('Claim failed: ' + error.message);
}
}
async sendStakingTransaction(txData, wallet) {
try {
// Get current nonce
const nonce = await ipcRenderer.invoke('paperclip-get-nonce', wallet.address);
txData.nonce = nonce + 1;
// Sign transaction (this would use the actual wallet signing)
const signedTx = await ClipsWallet.signTransaction(txData, wallet);
// Broadcast transaction
const result = await ipcRenderer.invoke('paperclip-broadcast-tx', signedTx);
return result;
} catch (error) {
throw error;
}
}
// Utility functions
populateWalletDropdown(selector) {
const walletSelect = $(selector);
walletSelect.empty();
ClipsWallet.addressList.forEach((address, index) => {
const option = `<option value="${address}">Wallet ${index + 1} (${address.substring(0, 12)}...)</option>`;
walletSelect.append(option);
});
}
async updateAvailableBalance(selector) {
if (ClipsWallet.addressList.length > 0) {
try {
const currentAddress = ClipsWallet.addressList[0];
const balance = await ipcRenderer.invoke('paperclip-get-balance', currentAddress);
$(selector).text(this.formatClips(balance));
} catch (error) {
$(selector).text('0 CLIPS');
}
} else {
$(selector).text('0 CLIPS');
}
}
updateYourStakeAmount(validatorAddress) {
const userStake = this.getUserStakeForValidator(validatorAddress);
$('#yourStakeAmount').text(this.formatClips(userStake));
}
getUserStakeForValidator(validatorAddress) {
if (!this.stakingInfo || !this.stakingInfo.delegations) {
return 0;
}
const delegation = this.stakingInfo.delegations.find(d => d.validator === validatorAddress);
return delegation ? delegation.amount : 0;
}
calculateValidatorAPY(validator) {
// Simplified APY calculation
const baseAPY = 8.0; // 8% base APY
const totalStake = (validator.stakedAmount || 0) + (validator.delegatedAmount || 0);
if (totalStake === 0) return 0;
// Adjust APY based on commission
const adjustedAPY = baseAPY * (1 - (validator.commission || 0) / 10000);
return Math.max(adjustedAPY, 0);
}
formatClips(microClips) {
return (microClips / 1000000).toFixed(6) + ' CLIPS';
}
startAutoRefresh() {
// Refresh staking data every 30 seconds
this.refreshInterval = setInterval(() => {
this.loadStakingData();
}, 30000);
}
stopAutoRefresh() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
showError(message) {
iziToast.error({
title: 'Staking Error',
message: message,
position: 'topRight'
});
}
showSuccess(message) {
iziToast.success({
title: 'Staking Success',
message: message,
position: 'topRight'
});
}
}
// Initialize staking manager
const PaperclipStaking = new StakingManager();
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = PaperclipStaking;
}