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:
386
assets/styles/staking.css
Normal file
386
assets/styles/staking.css
Normal 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;
|
||||||
|
}
|
||||||
214
assets/templates/staking.html
Normal file
214
assets/templates/staking.html
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
520
renderer/staking.js
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user