diff --git a/assets/styles/staking.css b/assets/styles/staking.css new file mode 100644 index 0000000..d54da7c --- /dev/null +++ b/assets/styles/staking.css @@ -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; +} \ No newline at end of file diff --git a/assets/templates/staking.html b/assets/templates/staking.html new file mode 100644 index 0000000..1539545 --- /dev/null +++ b/assets/templates/staking.html @@ -0,0 +1,214 @@ + +
+
+
+
+
+

Staking & Delegation + Earn rewards by staking your CLIPS +

+
+
+ +
+
+
+
+ +
+
+ Total Staked + 0 CLIPS +
+
+
+
+
+
+
+
+
+ +
+
+ Pending Rewards + 0 CLIPS +
+
+
+
+
+
+
+
+
+ +
+
+ Active Validators + 0 +
+
+
+
+
+
+ +
+
+ Network APY + ~8% +
+
+
+
+ + +
+
+
+
+

Quick Actions

+
+
+
+ + + + +
+
+
+
+
+ + +
+
+
+
+

Validators

+
+ +
+
+
+
+ + + + + + + + + + + + + + + + +
ValidatorStatusSelf StakeDelegatedCommissionAPYYour StakeActions
+
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ + +
+
+ + + Available balance: 0 CLIPS +
+
+ + +
+ + +
+
+ + +
+
+
+ + +
+
+ + + Your stake: 0 CLIPS +
+
+ + +
+
+ + Note: Undelegated funds may have a cooldown period before they become available. +
+ + +
+
+ + +
+
+
+ + Requirements: Minimum 10,000 CLIPS stake required to create a validator. +
+
+ + +
+
+ + + Percentage of rewards you'll keep as commission +
+
+ + +
+ + +
+
\ No newline at end of file diff --git a/index.html b/index.html index dfc1343..8f5b4a2 100755 --- a/index.html +++ b/index.html @@ -19,6 +19,7 @@ + diff --git a/modules/paperclip-rpc.js b/modules/paperclip-rpc.js index e35f73e..97aa7fd 100644 --- a/modules/paperclip-rpc.js +++ b/modules/paperclip-rpc.js @@ -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) { this.rpcUrl = 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); }); +// 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; \ No newline at end of file diff --git a/renderer/maingui.js b/renderer/maingui.js index 4ff542b..de9f270 100755 --- a/renderer/maingui.js +++ b/renderer/maingui.js @@ -25,6 +25,9 @@ class MainGUI { case "transactions": $("#mainNavBtnTransactionsWrapper").addClass("iconSelected"); break; + case "staking": + $("#mainNavBtnStakingWrapper").addClass("iconSelected"); + break; case "markets": $("#mainNavBtnMarketsWrapper").addClass("iconSelected"); break; @@ -122,6 +125,11 @@ $("#mainNavBtnTransactions").click(function () { EthoTransactions.renderTransactions(); }); +$("#mainNavBtnStaking").click(function () { + EthoMainGUI.changeAppState("staking"); + PaperclipStaking.showStakingPage(); +}); + $("#mainNavBtnAddressBoook").click(function () { EthoMainGUI.changeAppState("addressBook"); EthoAddressBook.renderAddressBook(); diff --git a/renderer/staking.js b/renderer/staking.js new file mode 100644 index 0000000..86fe1f2 --- /dev/null +++ b/renderer/staking.js @@ -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 ? + '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; +} \ No newline at end of file