// 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 ?
'Active' :
'Inactive';
const row = `
|
${validator.address.substring(0, 12)}...
${validator.address}
|
${status} |
${this.formatClips(validator.stakedAmount || 0)} |
${this.formatClips(validator.delegatedAmount || 0)} |
${(validator.commission / 100).toFixed(2)}% |
~${apy.toFixed(2)}% |
${this.formatClips(userStake)} |
${userStake > 0 ? `` : ''}
|
`;
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('');
this.validators.filter(v => v.active).forEach(validator => {
const 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('');
this.validators.forEach(validator => {
const userStake = this.getUserStakeForValidator(validator.address);
if (userStake > 0) {
const 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 = ``;
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;
}