// 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; }