From d0a0f5b9fd4e015b89cec8dcefcd5e2d51098228 Mon Sep 17 00:00:00 2001 From: Riki J Iskandar Date: Thu, 22 Jan 2026 02:19:54 +0700 Subject: [PATCH 1/4] feat: overhaul wallet UI with premium borderless aesthetics, glare reduction, and build fixes --- assets/theme.css | 33 +++-- src/App.tsx | 80 ++++++++++-- src/components/WalletTab.tsx | 82 ++++++------ src/components/dashboard/ActionBar.tsx | 44 ++++--- src/components/onboarding/Welcome.tsx | 137 ++++++++++--------- src/components/staking/Staking.tsx | 174 +++++++++++++------------ src/index.css | 116 ++++++++++++++++- 7 files changed, 424 insertions(+), 242 deletions(-) diff --git a/assets/theme.css b/assets/theme.css index a467ac1..78fb5ff 100644 --- a/assets/theme.css +++ b/assets/theme.css @@ -12,15 +12,14 @@ CHROME EXTENSION POPUP LAYOUT FIXES ======================================== */ -/* Fixed popup dimensions - increased height for dropdown visibility */ +/* Responsive popup layout - allows full screen in tab while maintaining popup look */ html, body { - width: 380px !important; - min-width: 380px !important; - height: 620px !important; - min-height: 620px !important; - max-height: 620px !important; - overflow: visible !important; - position: relative !important; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; + position: relative; } #root { @@ -36,7 +35,6 @@ html, body { display: flex !important; flex-direction: column !important; height: 100% !important; - max-height: 620px !important; overflow: visible !important; position: relative !important; } @@ -66,10 +64,9 @@ footer { justify-content: space-around !important; } -/* Add padding to main content to prevent footer overlap */ -#root > div, -.bg-background.text-foreground { - padding-bottom: 60px !important; +/* Main content area padding - removed global !important padding that caused onboarding clipping */ +#root > div { + padding-bottom: 0; } /* Main content area - scrollable */ @@ -158,7 +155,7 @@ main { header, .bg-surface\/50, .bg-surface\/80 { backdrop-filter: saturate(180%) blur(20px) !important; -webkit-backdrop-filter: saturate(180%) blur(20px) !important; - border: 1px solid var(--card-border) !important; + border: none !important; } /* Rounded corners iOS style */ @@ -211,8 +208,8 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.08); } -/* Card styles with glassmorphism */ -.p-6, .p-4, .p-3 { +/* Card styles with glassmorphism - moved to specific class to avoid hijacking */ +.glass-card { background: var(--surface); backdrop-filter: saturate(180%) blur(20px); -webkit-backdrop-filter: saturate(180%) blur(20px); @@ -242,8 +239,8 @@ input:focus, textarea:focus, select:focus { -webkit-tap-highlight-color: transparent; } -/* List items */ -.space-y-2 > *, .space-y-3 > *, .space-y-4 > * { +/* List items - moved to specific class to avoid hijacking layout containers */ +.ios-list > * { background: var(--surface); backdrop-filter: saturate(180%) blur(20px); -webkit-backdrop-filter: saturate(180%) blur(20px); diff --git a/src/App.tsx b/src/App.tsx index 7ae4b8a..f6348db 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -64,12 +64,38 @@ function App() { const checkSession = async () => { const hasWallet = await VaultManager.hasWallet(); if (hasWallet) { - setIsLocked(true); - navigate('/'); + // Check if we already have an active session + try { + const unlockedWallets = await VaultManager.getWallets(); + if (unlockedWallets && unlockedWallets.length > 0) { + setWallets(unlockedWallets); + setIsLocked(false); + + // Restore active wallet + const lastActive = localStorage.getItem('lastActiveWalletAddress'); + const foundIdx = unlockedWallets.findIndex(w => w.address === lastActive); + if (foundIdx !== -1) { + setActiveWalletIndex(foundIdx); + } + + if (location.pathname === '/' || location.pathname === '/onboarding') { + navigate('/dashboard'); + } + } else { + setIsLocked(true); + if (location.pathname === '/onboarding') { + navigate('/'); + } + } + } catch { + setIsLocked(true); + if (location.pathname === '/onboarding') { + navigate('/'); + } + } } else { // No wallet -> Onboarding. setIsLocked(false); - // If in popup mode (small width) and no wallet, open full tab for better onboarding experience if (window.innerWidth < 400 && (location.pathname === '/onboarding' || location.pathname === '/')) { openExpandedView('/wallet/create'); window.close(); @@ -193,16 +219,40 @@ function App() { return
Loading...
; } + const isLandingPage = (location.pathname.includes('/wallet/create') || location.pathname === '/onboarding') && wallets.length === 0; + + if (isLandingPage) { + return ( +
+
+ + 0} + onCancel={() => navigate('/dashboard')} + showLinkModal={false} + onCloseLinkModal={() => { }} + /> + } /> + } /> + +
+
+ ); + } + return (
{/* Header */} {!isLocked && activeWallet && ( -
+
Lumen Lumen @@ -266,15 +316,17 @@ function App() { ) : } /> 0} - onCancel={() => navigate('/dashboard')} - /* No modal for create flow */ - showLinkModal={false} - onCloseLinkModal={() => { }} - /> +
+ 0} + onCancel={() => navigate('/dashboard')} + /* No modal for create flow */ + showLinkModal={false} + onCloseLinkModal={() => { }} + /> +
} /> : diff --git a/src/components/WalletTab.tsx b/src/components/WalletTab.tsx index 72ee80f..f071e80 100644 --- a/src/components/WalletTab.tsx +++ b/src/components/WalletTab.tsx @@ -22,13 +22,7 @@ interface WalletTabProps { onCloseLinkModal?: () => void; } -const PqcShield = () => ( - - - -); - -export const WalletTab: React.FC = ({ onWalletReady, activeKeys, isAdding, showLinkModal, onCloseLinkModal }) => { +export const WalletTab: React.FC = ({ onWalletReady, activeKeys, isAdding, onCancel, showLinkModal, onCloseLinkModal }) => { /* Flows: 'welcome' -> 'create-method' -> 'mnemonic-display' -> 'mnemonic-verify' -> 'set-password' -> DONE */ /* Or: 'welcome' -> 'import' -> 'set-password' -> DONE */ const [view, setView] = useState<'welcome' | 'create-method' | 'mnemonic-display' | 'mnemonic-verify' | 'import' | 'set-password'>(isAdding ? 'create-method' : 'welcome'); @@ -203,56 +197,65 @@ export const WalletTab: React.FC = ({ onWalletReady, activeKeys, {/* REMOVED INLINE BANNER - Now using Modal */} -
- {/* Clean Modern Balance Card */} -
+
+ {/* Premium Balance Card */} +
+ {/* Mesh Gradient Overlay */} +
+
{/* Header Row */} -
-
- Total Balance +
+
+
+ Total Balance
-
- - Secured +
+
+ Secured
- + {/* Balance Display */} -
-
- - {hideBalance ? '••••••' : balance} +
+
+ + {hideBalance ? '••••••' : balance.split('.')[0]} - LMN + {!hideBalance && ( + .{balance.split('.')[1] || '00'} + )} + LMN +
+
+
+ ≈ $0.00 USD
-
≈ $0.00 USD
- - {/* Address Section */} -
-
-
-
Address
-
{activeKeys.address}
+ + {/* Address Box */} +
+
+
+
+
Address
+
{activeKeys.address}
- @@ -291,6 +294,7 @@ export const WalletTab: React.FC = ({ onWalletReady, activeKeys, setView('create-method')} onImportExisting={() => setView('import')} + onBack={onCancel} /> ); } diff --git a/src/components/dashboard/ActionBar.tsx b/src/components/dashboard/ActionBar.tsx index 1604b52..71bb5eb 100644 --- a/src/components/dashboard/ActionBar.tsx +++ b/src/components/dashboard/ActionBar.tsx @@ -13,49 +13,53 @@ export const ActionBar: React.FC = ({ onReceive, onHistory }) => const buttons = [ { label: 'Send', - icon: , + icon: , onClick: () => navigate('/send'), - active: true, - bg: 'bg-primary' + active: true }, { label: 'Receive', - icon: , + icon: , onClick: onReceive, - active: true, - bg: 'bg-lumen' + active: true }, { label: 'Stake', - icon: , + icon: , onClick: () => navigate('/stake'), - active: true, - bg: 'bg-green-500' + active: true }, { label: 'Vote', - icon: , + icon: , onClick: () => navigate('/governance'), - active: true, - bg: 'bg-purple-500' + active: true }, { label: 'History', - icon: , + icon: , onClick: onHistory, - active: true, - bg: 'bg-gray-600' + active: true }, ]; return ( -
+
{buttons.map((btn, idx) => ( -
-
- {btn.icon} +
+
+ {/* Inner Glow Effect */} +
+ +
+ {btn.icon} +
- + {btn.label}
diff --git a/src/components/onboarding/Welcome.tsx b/src/components/onboarding/Welcome.tsx index 0a88c6b..d1785fe 100644 --- a/src/components/onboarding/Welcome.tsx +++ b/src/components/onboarding/Welcome.tsx @@ -3,102 +3,117 @@ import React from 'react'; interface WelcomeProps { onCreateNew: () => void; onImportExisting: () => void; + onBack?: () => void; } -export const Welcome: React.FC = ({ onCreateNew, onImportExisting }) => { +export const Welcome: React.FC = ({ onCreateNew, onImportExisting, onBack }) => { return ( -
- {/* Hero Section */} -
-
- {/* Logo with glow effect */} -
-
- Lumen Wallet +
+ {/* Header */} +
+ {/* Logo with enhanced glow */} +
+
+ Lumen Wallet
- {/* Title */} -

+ {/* Title and Subtitle */} +

Welcome to Lumen

-

- Your gateway to the Lumen blockchain with quantum-resistant security +

+ The most secure and user-friendly wallet for the Lumen blockchain

- {/* Features Grid */} -
-
-
+ {/* Features - 2 Column Grid */} +
+
+
-
-

Quantum-Resistant

-

- Protected with post-quantum cryptography (Dilithium3) -

-
+

Quantum Safe

+

+ Post-quantum cryptography +

-
-
+
+
-
-

Fast & Secure

-

- Lightning-fast transactions with enterprise-grade security -

-
+

Fast & Secure

+

+ Enterprise-grade security +

-
-
+
+
-

Your Keys, Your Crypto

-

- Non-custodial wallet with full control over your assets +

Non-Custodial

+

+ Your keys, your crypto. Full control over your assets.

-
- {/* Action Buttons */} -
- + {/* Action Buttons */} +
+ + + - + {onBack && ( + + )} - {/* Terms */} -

- By continuing, you agree to our{' '} - Terms of Service - {' '}and{' '} - Privacy Policy -

+ {/* Footer Links */} +

+ By continuing, you agree to our{' '} + Terms + {' '}and{' '} + Privacy Policy +

+
); diff --git a/src/components/staking/Staking.tsx b/src/components/staking/Staking.tsx index 35ef5e8..45cfcaf 100644 --- a/src/components/staking/Staking.tsx +++ b/src/components/staking/Staking.tsx @@ -2,9 +2,9 @@ import React, { useState, useEffect } from 'react'; import { ArrowLeft, TrendingUp, Award, Users, ChevronRight, RefreshCw } from 'lucide-react'; import type { LumenWallet } from '../../modules/sdk/key-manager'; import { Toast } from '../common/Toast'; -import { - fetchDelegations, - fetchRewards, +import { + fetchDelegations, + fetchRewards, fetchValidators, fetchValidator, delegateTokens, @@ -52,36 +52,36 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { // Fetch user's delegations and rewards const fetchUserStakingData = async () => { if (!walletKeys?.address) return; - + setFetching(true); try { // Fetch delegations const delegations = await fetchDelegations(walletKeys.address); - + // Fetch rewards const rewardsData = await fetchRewards(walletKeys.address); - + // Calculate total staked let totalStakedAmount = BigInt(0); const stakes: UserStake[] = []; - + for (const delegation of delegations) { const validatorAddr = delegation.delegation.validator_address; const stakedAmount = delegation.balance.amount; totalStakedAmount += BigInt(stakedAmount); - + // Fetch validator info const validatorInfo = await fetchValidator(validatorAddr); - + // Find rewards for this validator const validatorRewards = rewardsData.rewards.find( (r: any) => r.validator_address === validatorAddr ); const rewardAmount = validatorRewards?.reward?.find((r: any) => r.denom === 'ulmn')?.amount || '0'; - + if (validatorInfo) { const commissionRate = parseFloat(validatorInfo.commission.commission_rates.rate) * 100; - + stakes.push({ validator: { address: validatorAddr, @@ -97,14 +97,14 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { }); } } - + setUserStakes(stakes); setTotalStaked((Number(totalStakedAmount) / 1000000).toFixed(2)); - + // Calculate total rewards const totalRewardsAmount = rewardsData.total.find((r: any) => r.denom === 'ulmn')?.amount || '0'; setTotalRewards((parseFloat(totalRewardsAmount) / 1000000).toFixed(6)); - + } catch (error) { console.error('Error fetching staking data:', error); setToastMessage('Failed to fetch staking data'); @@ -119,10 +119,10 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { const fetchValidatorsList = async () => { try { const validatorsData = await fetchValidators(); - + const formattedValidators: Validator[] = validatorsData.map((v: any) => { const commissionRate = parseFloat(v.commission.commission_rates.rate) * 100; - + return { address: v.operator_address, moniker: v.description.moniker, @@ -132,7 +132,7 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { apr: '12.5%' // Calculate from chain params if available }; }); - + setValidators(formattedValidators); } catch (error) { console.error('Error fetching validators:', error); @@ -147,17 +147,17 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { const handleStake = async () => { if (!selectedValidator || !amount || !walletKeys) return; setLoading(true); - + try { const amountUlmn = (parseFloat(amount) * 1000000).toString(); const txHash = await delegateTokens(walletKeys, selectedValidator.address, amountUlmn); - + setToastMessage(`Staked successfully! TX: ${txHash.slice(0, 8)}...`); setToastType('success'); setShowToast(true); setAmount(''); setSelectedValidator(null); - + // Refresh data await fetchUserStakingData(); } catch (error: any) { @@ -173,15 +173,15 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { const handleUnstake = async (stake: UserStake) => { if (!walletKeys) return; setLoading(true); - + try { const amountUlmn = (parseFloat(stake.amount) * 1000000).toString(); const txHash = await undelegateTokens(walletKeys, stake.validatorAddress, amountUlmn); - + setToastMessage(`Unstaked successfully! TX: ${txHash.slice(0, 8)}...`); setToastType('success'); setShowToast(true); - + // Refresh data await fetchUserStakingData(); } catch (error: any) { @@ -197,14 +197,14 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { const handleClaimRewards = async (validatorAddress: string) => { if (!walletKeys) return; setLoading(true); - + try { const txHash = await claimRewards(walletKeys, validatorAddress); - + setToastMessage(`Rewards claimed! TX: ${txHash.slice(0, 8)}...`); setToastType('success'); setShowToast(true); - + // Refresh data await fetchUserStakingData(); } catch (error: any) { @@ -220,84 +220,91 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { return (
{/* Header */} -
+
-

Staking

+

Staking

{/* Stats Cards */} -
-
-
- - APR +
+
+
+ + APR
-

12.5%

+

12.5%

-
-
- - Staked +
+
+ + Staked +
+
+

8 ? 'text-xs' : 'text-sm'}`}> + {totalStaked} +

+ LMN
-

{totalStaked} LMN

-
-
- - Rewards +
+
+ + Rewards +
+
+

8 ? 'text-xs' : 'text-sm'}`}> + {totalRewards} +

+ LMN
-

{totalRewards} LMN

{/* Tabs */} -
+
{/* Content */} -
+
{activeTab === 'stake' && (
{hasStakes ? ( @@ -308,7 +315,7 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { {userStakes.map((stake, idx) => (
@@ -345,23 +352,25 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { /* Show Validator List for New Staking */ <> {/* Amount Input */} -
- +
+
+ + +
setAmount(e.target.value)} placeholder="0.00" - className="flex-1 bg-background border border-border rounded-lg p-2.5 text-foreground text-base font-semibold focus:border-primary outline-none transition-colors" + className="flex-1 bg-background border border-border rounded-md p-2 text-sm font-semibold focus:border-primary outline-none transition-colors" /> - LMN + LMN
-
{/* Validators List */} @@ -372,11 +381,10 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { @@ -445,7 +453,7 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => {
-
-
} /> console.error(error)); +if (typeof chrome !== 'undefined' && chrome.sidePanel && chrome.sidePanel.setPanelBehavior) { + chrome.sidePanel + .setPanelBehavior({ openPanelOnActionClick: false }) + .catch((error) => console.error(error)); +} // Create context menu on install -chrome.runtime.onInstalled.addListener(() => { - // Check if contextMenus API is available - if (chrome.contextMenus) { - chrome.contextMenus.create({ - id: 'openSidePanel', - title: 'Open Side Panel', - contexts: ['all'] - }); - } -}); +if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onInstalled) { + chrome.runtime.onInstalled.addListener(() => { + if (typeof chrome !== 'undefined' && chrome.contextMenus) { + chrome.contextMenus.create({ + id: 'openSidePanel', + title: 'Open Side Panel', + contexts: ['all'] + }); + } + }); +} -// Handle click - check if contextMenus API is available -if (chrome.contextMenus && chrome.contextMenus.onClicked) { +// Handle click +if (typeof chrome !== 'undefined' && chrome.contextMenus && chrome.contextMenus.onClicked) { chrome.contextMenus.onClicked.addListener((info, tab) => { if (info.menuItemId === 'openSidePanel' && tab?.windowId) { - // Requires user interaction (click) which context menu provides - chrome.sidePanel.open({ windowId: tab.windowId }) - .catch(console.error); + if (chrome.sidePanel && chrome.sidePanel.open) { + chrome.sidePanel.open({ windowId: tab.windowId }) + .catch(console.error); + } } }); } diff --git a/src/components/WalletMenu.tsx b/src/components/WalletMenu.tsx index a36eeb0..a014301 100644 --- a/src/components/WalletMenu.tsx +++ b/src/components/WalletMenu.tsx @@ -51,8 +51,8 @@ export const WalletMenu: React.FC = ({ className="flex items-center gap-2 p-2 rounded-lg hover:bg-surfaceHighlight transition-all group cursor-pointer" title="Wallet Settings" > -
- {activeWalletName} +
+ {activeWalletName} Connected
diff --git a/src/components/WalletTab.tsx b/src/components/WalletTab.tsx index f071e80..5d238ae 100644 --- a/src/components/WalletTab.tsx +++ b/src/components/WalletTab.tsx @@ -50,6 +50,7 @@ export const WalletTab: React.FC = ({ onWalletReady, activeKeys, /* UI State */ const [showReceive, setShowReceive] = useState(false); const [showHistory, setShowHistory] = useState(false); + const [copiedAddress, setCopiedAddress] = useState(false); /* Fetch Balance Effect */ React.useEffect(() => { @@ -78,7 +79,7 @@ export const WalletTab: React.FC = ({ onWalletReady, activeKeys, lastBalanceRef.current = newBalRaw; } } catch (e) { - console.error("Balance fetch error:", e); + console.warn("Balance fetch failed (transient):", e); /* Keep previous/default balance on error or show indicator */ } }; @@ -121,7 +122,10 @@ export const WalletTab: React.FC = ({ onWalletReady, activeKeys, onWalletReady(); } catch (e: any) { console.error(e); - setError("Failed to add wallet: " + e.message); + const msg = e.message === 'Session expired.' + ? "Wallet is locked. Please unlock your wallet in the extension popup first before adding a new one." + : "Failed to add wallet: " + e.message; + setError(msg); setIsLoading(false); } }; @@ -130,6 +134,15 @@ export const WalletTab: React.FC = ({ onWalletReady, activeKeys, if (!tempWallet) return; try { setIsLoading(true); + + /* Safety Guard: Check if a vault already exists on disk */ + const exists = await VaultManager.hasWallet(); + if (exists) { + setError("A wallet already exists on this device. Please unlock it and use 'Add Wallet' instead of creating a new vault."); + setIsLoading(false); + return; + } + /* Initial setup -> Just array of one */ await VaultManager.lock([tempWallet], password); onWalletReady(); @@ -249,15 +262,32 @@ export const WalletTab: React.FC = ({ onWalletReady, activeKeys,
Address
-
{activeKeys.address}
+
+ {activeKeys.address.substring(0, 24)} + + {activeKeys.address.substring(24)} +
@@ -315,6 +345,8 @@ export const WalletTab: React.FC = ({ onWalletReady, activeKeys, return ( setView('mnemonic-verify')} onBack={() => setView('create-method')} /> @@ -347,6 +379,17 @@ export const WalletTab: React.FC = ({ onWalletReady, activeKeys, setIsImporting(true); setError(null); + // If adding, ensure session is still valid + if (isAdding) { + try { + await VaultManager.getWallets(); + } catch (e: any) { + setError("Session expired or vault locked. Please unlock and try again."); + setIsImporting(false); + return; + } + } + const keys = await KeyManager.importWallet(mnemonic, pqcKey); setTempWallet(keys); @@ -371,6 +414,7 @@ export const WalletTab: React.FC = ({ onWalletReady, activeKeys, }} onBack={() => setView(isAdding ? 'create-method' : 'welcome')} isLoading={isImporting} + error={_error} /> ); } diff --git a/src/components/dashboard/BackupModal.tsx b/src/components/dashboard/BackupModal.tsx index 22fd157..61422d0 100644 --- a/src/components/dashboard/BackupModal.tsx +++ b/src/components/dashboard/BackupModal.tsx @@ -122,7 +122,7 @@ export const BackupModal: React.FC = ({ wallet, onClose }) =>
) : ( -
+
e.stopPropagation()}>

Secure Backup

@@ -211,12 +211,12 @@ export const ImportWalletAdvanced: React.FC = ({ onIm
{/* Error Message */} - {error && ( + {(error || externalError) && (
-

{error}

+

{error || externalError}

)} diff --git a/src/components/onboarding/MnemonicDisplay.tsx b/src/components/onboarding/MnemonicDisplay.tsx index eeca4c4..6155d13 100644 --- a/src/components/onboarding/MnemonicDisplay.tsx +++ b/src/components/onboarding/MnemonicDisplay.tsx @@ -1,12 +1,15 @@ import React, { useState } from 'react'; +import type { PqcKeyData } from '../../types/wallet'; interface MnemonicDisplayProps { mnemonic: string; + pqcKey: PqcKeyData; + address: string; onConfirm: () => void; onBack: () => void; } -export const MnemonicDisplay: React.FC = ({ mnemonic, onConfirm, onBack }) => { +export const MnemonicDisplay: React.FC = ({ mnemonic, pqcKey, address, onConfirm, onBack }) => { const [copied, setCopied] = useState(false); const [confirmed, setConfirmed] = useState(false); const words = mnemonic.split(' '); @@ -17,6 +20,19 @@ export const MnemonicDisplay: React.FC = ({ mnemonic, onCo setTimeout(() => setCopied(false), 2000); }; + const downloadPqcBackup = () => { + const data = JSON.stringify({ pqcKey: pqcKey }, null, 2); + const blob = new Blob([data], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `lumen_pqc_${address.substring(0, 8)}.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + return (
@@ -46,8 +62,8 @@ export const MnemonicDisplay: React.FC = ({ mnemonic, onCo

Never share your recovery phrase!

  • Anyone with this phrase can access your funds
  • -
  • Lumen will never ask for your recovery phrase
  • -
  • Store it offline in a secure location
  • +
  • Store both mnemonic and PQC keys offline securely
  • +
  • PQC keys cannot be recovered from mnemonic alone
@@ -84,12 +100,34 @@ export const MnemonicDisplay: React.FC = ({ mnemonic, onCo - Copy to Clipboard + Copy Mnemonic )}
+ {/* PQC Section */} +
+
+ + Recommended Backup +
+
+
+ {JSON.stringify({ pqcKey: pqcKey }, null, 2)} +
+ +
+
+ {/* Confirmation Checkbox */}
diff --git a/src/modules/sdk/key-manager.ts b/src/modules/sdk/key-manager.ts index 84a321a..858f8e3 100644 --- a/src/modules/sdk/key-manager.ts +++ b/src/modules/sdk/key-manager.ts @@ -185,4 +185,13 @@ export class KeyManager { pqcKey: pqcKey }; } + + /** + * Helper to derive an address from a mnemonic + */ + static async deriveAddress(mnemonic: string): Promise { + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: 'lmn' }); + const accounts = await wallet.getAccounts(); + return accounts[0].address; + } } diff --git a/vite.config.ts b/vite.config.ts index 738bfb2..727e8a8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -37,6 +37,7 @@ export default defineConfig({ }, }, build: { + chunkSizeWarningLimit: 1000, rollupOptions: { input: { main: path.resolve(__dirname, 'index.html'), @@ -44,6 +45,23 @@ export default defineConfig({ }, output: { entryFileNames: '[name].js', + manualChunks(id) { + if (id.includes('node_modules')) { + if (id.includes('react') || id.includes('react-dom') || id.includes('react-router-dom')) { + return 'vendor-react'; + } + if (id.includes('@cosmjs') || id.includes('cosmjs-types')) { + return 'vendor-cosmjs'; + } + if (id.includes('@lumen-chain/sdk')) { + return 'vendor-lumen'; + } + if (id.includes('lucide-react')) { + return 'vendor-ui'; + } + return 'vendor'; + } + } }, }, }, From 8054b1105648dadc42997e607e4fb26938e7f277 Mon Sep 17 00:00:00 2001 From: Riki J Iskandar Date: Sat, 24 Jan 2026 22:43:57 +0700 Subject: [PATCH 4/4] feat(staking): overhaul UX with multi-step flow and unstaking safety - Redesigned staking UI into a multi-step flow (Dashboard, Detail, Confirm). - Added validator search filter and sorted list by voting power. - Implemented a 21-day unbonding warning modal for unstaking. - Added real-time unbonding progress tracking with completion dates. - Improved UI responsiveness and fixed element overlaps (Navbar/Header). - Integrated transaction history for all staking actions. - Enhanced SDK with unbonding delegation fetching. --- src/components/staking/Staking.tsx | 753 +++++++++++++++++++---------- src/modules/history/history.ts | 18 +- src/modules/sdk/staking.ts | 85 +++- 3 files changed, 578 insertions(+), 278 deletions(-) diff --git a/src/components/staking/Staking.tsx b/src/components/staking/Staking.tsx index 45cfcaf..0e8070a 100644 --- a/src/components/staking/Staking.tsx +++ b/src/components/staking/Staking.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; -import { ArrowLeft, TrendingUp, Award, Users, ChevronRight, RefreshCw } from 'lucide-react'; +import { ArrowLeft, TrendingUp, Award, Users, ChevronRight, RefreshCw, Info, CheckCircle2, ShieldCheck, PieChart, AlertCircle, Search } from 'lucide-react'; +import { HistoryManager } from '../../modules/history/history'; import type { LumenWallet } from '../../modules/sdk/key-manager'; import { Toast } from '../common/Toast'; import { @@ -9,7 +10,8 @@ import { fetchValidator, delegateTokens, undelegateTokens, - claimRewards + claimRewards, + fetchUnbondingDelegations } from '../../modules/sdk/staking'; interface StakingProps { @@ -33,6 +35,12 @@ interface UserStake { validatorAddress: string; } +interface UnbondingEntry { + validatorMoniker: string; + amount: string; + completionTime: string; +} + export const Staking: React.FC = ({ walletKeys, onBack }) => { const [activeTab, setActiveTab] = useState<'stake' | 'unstake' | 'rewards'>('stake'); const [validators, setValidators] = useState([]); @@ -46,6 +54,12 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { const [toastType, setToastType] = useState<'success' | 'error'>('success'); const [totalStaked, setTotalStaked] = useState('0'); const [totalRewards, setTotalRewards] = useState('0'); + const [step, setStep] = useState<'dashboard' | 'detail' | 'confirm'>('dashboard'); + const [balanceUlmn, setBalanceUlmn] = useState('0'); + const [searchTerm, setSearchTerm] = useState(''); + const [unbondingEntries, setUnbondingEntries] = useState([]); + const [showUnstakeConfirm, setShowUnstakeConfirm] = useState(false); + const [stakeToUnstake, setStakeToUnstake] = useState(null); const hasStakes = userStakes.length > 0; @@ -105,6 +119,21 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { const totalRewardsAmount = rewardsData.total.find((r: any) => r.denom === 'ulmn')?.amount || '0'; setTotalRewards((parseFloat(totalRewardsAmount) / 1000000).toFixed(6)); + // Fetch Unbonding + const unbondingData = await fetchUnbondingDelegations(walletKeys.address); + const processedUnbonding: UnbondingEntry[] = []; + for (const unbond of unbondingData) { + const valInfo = await fetchValidator(unbond.validator_address); + for (const entry of unbond.entries) { + processedUnbonding.push({ + validatorMoniker: valInfo?.description?.moniker || 'Unknown', + amount: (Number(entry.balance) / 1000000).toFixed(2), + completionTime: entry.completion_time + }); + } + } + setUnbondingEntries(processedUnbonding); + } catch (error) { console.error('Error fetching staking data:', error); setToastMessage('Failed to fetch staking data'); @@ -134,6 +163,14 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { }); setValidators(formattedValidators); + + // Also fetch wallet balance for validation + const res = await fetch(`https://api-lumen.winnode.xyz/cosmos/bank/v1beta1/balances/${walletKeys.address}`); + if (res.ok) { + const data = await res.json(); + const bal = data.balances.find((b: any) => b.denom === 'ulmn')?.amount || '0'; + setBalanceUlmn(bal); + } } catch (error) { console.error('Error fetching validators:', error); } @@ -150,13 +187,30 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { try { const amountUlmn = (parseFloat(amount) * 1000000).toString(); + if (BigInt(amountUlmn) > BigInt(balanceUlmn)) { + throw new Error("Insufficient balance"); + } + const txHash = await delegateTokens(walletKeys, selectedValidator.address, amountUlmn); - setToastMessage(`Staked successfully! TX: ${txHash.slice(0, 8)}...`); + // Save to History + HistoryManager.saveTransaction(walletKeys.address, { + hash: txHash, + height: "...", + timestamp: new Date().toISOString(), + type: 'stake', + amount: amount, + denom: 'LMN', + counterparty: selectedValidator.moniker, + status: 'success' + }); + + setToastMessage(`Staked successfully!`); setToastType('success'); setShowToast(true); setAmount(''); setSelectedValidator(null); + setStep('dashboard'); // Refresh data await fetchUserStakingData(); @@ -171,14 +225,20 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { }; const handleUnstake = async (stake: UserStake) => { - if (!walletKeys) return; + setStakeToUnstake(stake); + setShowUnstakeConfirm(true); + }; + + const confirmUnstake = async () => { + if (!walletKeys || !stakeToUnstake) return; setLoading(true); + setShowUnstakeConfirm(false); try { - const amountUlmn = (parseFloat(stake.amount) * 1000000).toString(); - const txHash = await undelegateTokens(walletKeys, stake.validatorAddress, amountUlmn); + const amountUlmn = (parseFloat(stakeToUnstake.amount) * 1000000).toString(); + const txHash = await undelegateTokens(walletKeys, stakeToUnstake.validatorAddress, amountUlmn); - setToastMessage(`Unstaked successfully! TX: ${txHash.slice(0, 8)}...`); + setToastMessage(`Unstaked successfully! Asset is now unbonding. TX: ${txHash.slice(0, 8)}...`); setToastType('success'); setShowToast(true); @@ -191,6 +251,7 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { setShowToast(true); } finally { setLoading(false); + setStakeToUnstake(null); } }; @@ -201,7 +262,19 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { try { const txHash = await claimRewards(walletKeys, validatorAddress); - setToastMessage(`Rewards claimed! TX: ${txHash.slice(0, 8)}...`); + // Save to History + HistoryManager.saveTransaction(walletKeys.address, { + hash: txHash, + height: "...", + timestamp: new Date().toISOString(), + type: 'claim', + amount: 'Checking...', + denom: 'LMN', + counterparty: 'Rewards', + status: 'success' + }); + + setToastMessage(`Rewards claimed!`); setToastType('success'); setShowToast(true); @@ -210,24 +283,52 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { } catch (error: any) { console.error('Claim rewards error:', error); setToastMessage(error.message || 'Failed to claim rewards'); - setToastType('error'); - setShowToast(true); } finally { setLoading(false); } }; + const filteredValidators = validators + .filter(v => + v.moniker.toLowerCase().includes(searchTerm.toLowerCase()) || + v.address.toLowerCase().includes(searchTerm.toLowerCase()) + ) + .sort((a, b) => Number(b.votingPower) - Number(a.votingPower)); + return ( -
+
+ {/* Header */} -
+
-

Staking

+

+ {step === 'dashboard' ? 'Staking' : step === 'detail' ? 'Manage Stake' : 'Confirm Staking'} +

- {/* Stats Cards */} -
-
-
- - APR -
-

12.5%

-
-
-
- - Staked -
-
-

8 ? 'text-xs' : 'text-sm'}`}> - {totalStaked} -

- LMN -
-
-
-
- - Rewards -
-
-

8 ? 'text-xs' : 'text-sm'}`}> - {totalRewards} -

- LMN + {/* Dashboard View */} + {step === 'dashboard' && ( +
+ {/* Stats Cards */} +
+
+
+ + APR +
+

12.5%

+
+
+
+ + Staked +
+
+

8 ? 'text-xs' : 'text-sm'}`}> + {totalStaked} +

+ LMN +
+
+
+
+ + Rewards +
+
+

8 ? 'text-xs' : 'text-sm'}`}> + {totalRewards} +

+ LMN +
+
-
-
- {/* Tabs */} -
- - - -
+ {/* Tabs */} +
+ {['stake', 'unstake', 'rewards'].map((tab) => ( + + ))} +
- {/* Content */} -
- {activeTab === 'stake' && ( -
- {hasStakes ? ( - /* Show User's Stakes */ -
-

Your Stakes

-
- {userStakes.map((stake, idx) => ( -
-
-
-
- -
-
-

{stake.validator.moniker}

-

- {stake.validator.address.slice(0, 18)}... -

+ {/* Content Area */} +
+ {activeTab === 'stake' && ( +
+ {hasStakes && ( +
+

Your Active Stakes

+
+ {userStakes.map((stake, idx) => ( +
+
+
+
+ +
+
+

{stake.validator.moniker}

+

{stake.validator.address.slice(0, 16)}...

+
+
+
+

{stake.amount} LMN

+

+{stake.rewards} LMN

+
-
-
-
-

Staked

-

{stake.amount} LMN

-
-
-

Rewards

-

{stake.rewards} LMN

-
-
-

APR

-

{stake.validator.apr}

-
-
+ ))}
- ))} -
-
- ) : ( - /* Show Validator List for New Staking */ - <> - {/* Amount Input */} -
-
- -
-
+ )} + +
+
+

Select Validator

+ {filteredValidators.length} active +
+ + {/* Search Bar */} +
+ setAmount(e.target.value)} - placeholder="0.00" - className="flex-1 bg-background border border-border rounded-md p-2 text-sm font-semibold focus:border-primary outline-none transition-colors" + type="text" + placeholder="Search moniker or address..." + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + className="w-full bg-surface border border-border rounded-xl py-2 px-9 text-[11px] focus:border-primary focus:bg-surfaceHighlight outline-none transition-all shadow-inner" /> - LMN
-
- {/* Validators List */} -
-

Select Validator

-
- {validators.map((validator) => ( +
+ {filteredValidators.map((validator) => ( + ))} + {filteredValidators.length === 0 && ( +
No validators found matching "{searchTerm}"
+ )} +
+
+
+ )} + + {activeTab === 'unstake' && ( +
+ {unbondingEntries.length > 0 && ( +
+

Unbonding Assets

+ {unbondingEntries.map((entry, idx) => ( +
+
-

APR

-

{validator.apr}

+

{entry.validatorMoniker}

+

Completes: {new Date(entry.completionTime).toLocaleDateString()}

-
-

Voting Power

-

{validator.votingPower}

+
+

{entry.amount} LMN

+

Unbonding...

- +
+
+
+
))}
+ )} + +
+

Staked Assets

+ {fetching ? ( +
+ ) : hasStakes ? ( +
+ {userStakes.map((stake, idx) => ( +
+
+
+
+
+

{stake.validator.moniker}

+

Staked: {stake.amount} LMN

+
+
+
+ +
+ ))} +
+ ) : ( +
No active stakes found.
+ )}
+
+ )} - {/* Stake Button */} - - + {activeTab === 'rewards' && ( +
+ {fetching ? ( +
+ ) : hasStakes ? ( + userStakes.map((stake, idx) => ( +
+
+
+
+
+

{stake.validator.moniker}

+

+{stake.rewards} LMN Accumulated

+
+
+
+ +
+ )) + ) : ( +
No rewards to claim.
+ )} +
)}
- )} +
+ )} - {activeTab === 'unstake' && ( -
- {fetching ? ( -
- -

Loading stakes...

+ {/* Detail View */} + {step === 'detail' && selectedValidator && ( +
+
+ {/* Validator Card */} +
+
+
- ) : hasStakes ? ( - userStakes.map((stake, idx) => ( -
-
-
-
- -
-
-

{stake.validator.moniker}

-

Staked: {stake.amount} LMN

-
-
-
+
+
+ +
+
+

{selectedValidator.moniker}

+

{selectedValidator.address}

+
+
+ +
+
+ APR Reward +

{selectedValidator.apr}

+
+
+ Commission +

{selectedValidator.commission}

+
+
+
+ + {/* Amount Input Section */} +
+
+ +
+ Max: {(Number(balanceUlmn) / 1000000).toFixed(2)} LMN +
+
+
+ setAmount(e.target.value)} + placeholder="0.00" + className="w-full bg-surface border-2 border-border rounded-xl p-3 text-lg font-black focus:border-primary outline-none transition-all pr-16 shadow-inner" + /> +
+ onClick={() => setAmount((Number(balanceUlmn) / 1000000).toString())} + className="text-[9px] font-black text-primary uppercase hover:bg-primary/10 px-1.5 py-1 rounded transition-colors" + >MAX
- )) - ) : ( -
-

No active stakes

- )} + {amount && parseFloat(amount) <= 0 && ( +
+ + Amount must be greater than 0 +
+ )} +
+ + {/* Info Alert */} +
+ +

+ Tokens bonded to validator. Unbonding takes 21 days on Cosmos networks. +

+
- )} - {activeTab === 'rewards' && ( -
- {fetching ? ( -
- -

Loading rewards...

-
- ) : hasStakes ? ( - userStakes.map((stake, idx) => ( -
-
-
-
- -
-
-

{stake.validator.moniker}

-

+{stake.rewards} LMN

-
-
+
+ +
+
+ )} + + {/* Confirmation Modal */} + {step === 'confirm' && selectedValidator && ( +
+
+
+

Confirm Staking

+

Review your transaction details

+
+ +
+
+
+ Action + Stake Assets +
+
+
+ Validator + {selectedValidator.moniker} +
+
+ Commission + {selectedValidator.commission} +
+
+
+ Total Amount +
+

{amount} LMN

+

≈ $0 USD

-
- )) - ) : ( -
-

No rewards available

- )} + +
+ + Secured by Post-Quantum Dilithium3 +
+
+ +
+ + +
- )} -
+
+ )} - {/* Toast Notification */} - {showToast && ( - setShowToast(false)} - /> + {/* Unstake Warning Modal */} + {showUnstakeConfirm && stakeToUnstake && ( +
+
+
+ +
+
+

Confirm Unstake

+

+ Unstaking will start a 21-day unbonding period. During this time, you will not earn rewards and cannot move your tokens. +

+
+ +
+
+ Amount + {stakeToUnstake.amount} LMN +
+
+ Validator + {stakeToUnstake.validator.moniker} +
+
+ +
+ + +
+
+
)} + + {/* Toast Notification */} +
+ {showToast && ( + setShowToast(false)} + /> + )} +
); }; diff --git a/src/modules/history/history.ts b/src/modules/history/history.ts index dca6443..9144000 100644 --- a/src/modules/history/history.ts +++ b/src/modules/history/history.ts @@ -20,7 +20,7 @@ export interface Transaction { hash: string; height: string; timestamp: string; - type: 'send' | 'receive'; + type: 'send' | 'receive' | 'stake' | 'unstake' | 'claim'; amount: string; denom: string; counterparty: string; @@ -343,7 +343,7 @@ export class HistoryManager { const amount = decoded.amount[0]; const amt = amount ? (parseFloat(amount.amount) / 1000000).toFixed(6) : "0"; const h = toHex(sha256(fromBase64(txBase64))).toUpperCase(); - + // Check if receiving if (decoded.toAddress === address) { this.saveTransaction(address, { @@ -357,7 +357,7 @@ export class HistoryManager { status: 'success' }); } - + // Check if sending if (decoded.fromAddress === address) { this.saveTransaction(address, { @@ -371,6 +371,18 @@ export class HistoryManager { status: 'success' }); } + } else if (msg.typeUrl === '/cosmos.staking.v1beta1.MsgDelegate') { + const h = toHex(sha256(fromBase64(txBase64))).toUpperCase(); + this.saveTransaction(address, { + hash: h, + height: height.toString(), + timestamp: blk.header.time, + type: 'stake', + amount: 'Checking...', + denom: 'LMN', + counterparty: 'Lumen Staking', + status: 'success' + }); } }); } catch { } diff --git a/src/modules/sdk/staking.ts b/src/modules/sdk/staking.ts index f302ffe..64a7230 100644 --- a/src/modules/sdk/staking.ts +++ b/src/modules/sdk/staking.ts @@ -74,6 +74,22 @@ export async function fetchDelegations(delegatorAddress: string) { } } +/* Fetch Unbonding Delegations */ +export async function fetchUnbondingDelegations(delegatorAddress: string) { + try { + const res = await fetch(`${API_ENDPOINT}/cosmos/staking/v1beta1/delegators/${delegatorAddress}/unbonding_delegations`); + if (!res.ok) { + if (res.status === 404) return []; + throw new Error(`Failed to fetch unbonding delegations: ${res.status}`); + } + const data = await res.json(); + return data.unbonding_responses || []; + } catch (error) { + console.error('Error fetching unbonding delegations:', error); + return []; + } +} + /* Fetch Rewards */ export async function fetchRewards(delegatorAddress: string) { try { @@ -139,18 +155,27 @@ export async function delegateTokens( const { accountNumber, sequence } = await fetchAccountInfo(walletData.address); /* Prepare PQC Keys */ - const pqcData = walletData.pqcKey || walletData.pqc; - if (!pqcData) throw new Error("Missing PQC key data"); + const pqcData = ((walletData.pqcKey as any)?.publicKey || (walletData.pqcKey as any)?.public_key) + ? walletData.pqcKey + : ((walletData.pqc as any)?.publicKey || (walletData.pqc as any)?.public_key) + ? walletData.pqc + : (walletData.pqcKey || walletData.pqc); - const rawPriv = pqcData.privateKey; - const rawPub = pqcData.publicKey; - if (!rawPriv || !rawPub) throw new Error("Missing PQC keys"); + if (!pqcData) throw new Error("Missing PQC key data. Please re-import your wallet."); + + const rawPriv = pqcData.privateKey || pqcData.private_key || (pqcData as any).encryptedPrivateKey; + const rawPub = pqcData.publicKey || pqcData.public_key; + + if (!rawPriv || !rawPub) throw new Error("PQC keys missing sub-properties. Please re-import your wallet."); const pqcPrivKey = ensureUint8Array(rawPriv); const pqcPubKey = ensureUint8Array(rawPub); - if (pqcPubKey.length !== 1952 || pqcPrivKey.length !== 4000) { - throw new Error("Invalid PQC key length"); + if (pqcPubKey.length !== 1952) { + throw new Error(`Invalid PQC Public Key. Expected 1952 bytes, got ${pqcPubKey.length}.`); + } + if (pqcPrivKey.length !== 4000) { + throw new Error(`Invalid PQC Private Key. Expected 4000 bytes, got ${pqcPrivKey.length}.`); } /* Create Delegate Message */ @@ -248,18 +273,27 @@ export async function undelegateTokens( const { accountNumber, sequence } = await fetchAccountInfo(walletData.address); /* Prepare PQC Keys */ - const pqcData = walletData.pqcKey || walletData.pqc; - if (!pqcData) throw new Error("Missing PQC key data"); + const pqcData = ((walletData.pqcKey as any)?.publicKey || (walletData.pqcKey as any)?.public_key) + ? walletData.pqcKey + : ((walletData.pqc as any)?.publicKey || (walletData.pqc as any)?.public_key) + ? walletData.pqc + : (walletData.pqcKey || walletData.pqc); + + if (!pqcData) throw new Error("Missing PQC key data. Please re-import your wallet."); + + const rawPriv = pqcData.privateKey || pqcData.private_key || (pqcData as any).encryptedPrivateKey; + const rawPub = pqcData.publicKey || pqcData.public_key; - const rawPriv = pqcData.privateKey; - const rawPub = pqcData.publicKey; - if (!rawPriv || !rawPub) throw new Error("Missing PQC keys"); + if (!rawPriv || !rawPub) throw new Error("PQC keys missing sub-properties. Please re-import your wallet."); const pqcPrivKey = ensureUint8Array(rawPriv); const pqcPubKey = ensureUint8Array(rawPub); - if (pqcPubKey.length !== 1952 || pqcPrivKey.length !== 4000) { - throw new Error("Invalid PQC key length"); + if (pqcPubKey.length !== 1952) { + throw new Error(`Invalid PQC Public Key. Expected 1952 bytes, got ${pqcPubKey.length}.`); + } + if (pqcPrivKey.length !== 4000) { + throw new Error(`Invalid PQC Private Key. Expected 4000 bytes, got ${pqcPrivKey.length}.`); } /* Create Undelegate Message */ @@ -356,18 +390,27 @@ export async function claimRewards( const { accountNumber, sequence } = await fetchAccountInfo(walletData.address); /* Prepare PQC Keys */ - const pqcData = walletData.pqcKey || walletData.pqc; - if (!pqcData) throw new Error("Missing PQC key data"); + const pqcData = ((walletData.pqcKey as any)?.publicKey || (walletData.pqcKey as any)?.public_key) + ? walletData.pqcKey + : ((walletData.pqc as any)?.publicKey || (walletData.pqc as any)?.public_key) + ? walletData.pqc + : (walletData.pqcKey || walletData.pqc); + + if (!pqcData) throw new Error("Missing PQC key data. Please re-import your wallet."); - const rawPriv = pqcData.privateKey; - const rawPub = pqcData.publicKey; - if (!rawPriv || !rawPub) throw new Error("Missing PQC keys"); + const rawPriv = pqcData.privateKey || pqcData.private_key || (pqcData as any).encryptedPrivateKey; + const rawPub = pqcData.publicKey || pqcData.public_key; + + if (!rawPriv || !rawPub) throw new Error("PQC keys missing sub-properties. Please re-import your wallet."); const pqcPrivKey = ensureUint8Array(rawPriv); const pqcPubKey = ensureUint8Array(rawPub); - if (pqcPubKey.length !== 1952 || pqcPrivKey.length !== 4000) { - throw new Error("Invalid PQC key length"); + if (pqcPubKey.length !== 1952) { + throw new Error(`Invalid PQC Public Key. Expected 1952 bytes, got ${pqcPubKey.length}.`); + } + if (pqcPrivKey.length !== 4000) { + throw new Error(`Invalid PQC Private Key. Expected 4000 bytes, got ${pqcPrivKey.length}.`); } /* Create Withdraw Reward Message */