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/background.js b/background.js index fbe31f8..9d6064c 100644 --- a/background.js +++ b/background.js @@ -1,24 +1,28 @@ // Set side panel behavior -if (chrome.sidePanel && chrome.sidePanel.setPanelBehavior) { +if (typeof chrome !== 'undefined' && chrome.sidePanel && chrome.sidePanel.setPanelBehavior) { chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }).catch((e) => console.error('Side panel error:', e)); } // Create context menu on installation -chrome.runtime.onInstalled.addListener(() => { - 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 context menu clicks -if (chrome.contextMenus) { +if (typeof chrome !== 'undefined' && chrome.contextMenus && chrome.contextMenus.onClicked) { chrome.contextMenus.onClicked.addListener((info, tab) => { if (info.menuItemId === "openSidePanel" && tab?.windowId) { - 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/manifest.json b/manifest.json index 18f1eed..9859a03 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Lumen Wallet", - "version": "1.0.0", + "version": "1.0.1", "description": "Non-custodial Lumen Chain wallet with Swap v1 support.", "action": { "default_popup": "index.html", @@ -9,9 +9,7 @@ }, "permissions": [ "storage", - "activeTab", "sidePanel", - "scripting", "contextMenus" ], "host_permissions": [ diff --git a/package.json b/package.json index 2640f4d..9607a75 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lumen-wallet-extension", "private": true, - "version": "0.0.0", + "version": "1.0.1", "type": "module", "scripts": { "dev": "vite", diff --git a/public/manifest.json b/public/manifest.json index d6479a2..4693a21 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Lumen Wallet", - "version": "1.0.0", + "version": "1.0.1", "description": "Non-custodial Lumen Chain wallet with Swap v1 support.", "action": { "default_popup": "index.html", @@ -11,7 +11,8 @@ "storage", "activeTab", "sidePanel", - "scripting" + "scripting", + "contextMenus" ], "side_panel": { "default_path": "index.html" diff --git a/src/App.tsx b/src/App.tsx index 7ae4b8a..5029cb3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,6 +25,7 @@ function App() { const activeWallet = wallets[activeWalletIndex] || null; const [isLocked, setIsLocked] = useState(false); + const [hasVault, setHasVault] = useState(false); const [loading, setLoading] = useState(true); const [unlockError, setUnlockError] = useState(null); const isLockedRef = useRef(isLocked); @@ -62,14 +63,41 @@ function App() { /* Initial Load & Session Check */ useEffect(() => { const checkSession = async () => { - const hasWallet = await VaultManager.hasWallet(); - if (hasWallet) { - setIsLocked(true); - navigate('/'); + const exists = await VaultManager.hasWallet(); + setHasVault(exists); + if (exists) { + // 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(); @@ -85,8 +113,9 @@ function App() { checkSession(); const interval = setInterval(async () => { - const hasWallet = await VaultManager.hasWallet(); - if (!hasWallet || isLockedRef.current) return; + const exists = await VaultManager.hasWallet(); + setHasVault(exists); + if (!exists || isLockedRef.current) return; const expired = await VaultManager.isSessionExpired(); if (expired) { handleLock(); @@ -100,6 +129,7 @@ function App() { try { const unlockedWallets = await VaultManager.unlock(password); setWallets(unlockedWallets); + setHasVault(true); /* Restore active wallet */ const lastActive = localStorage.getItem('lastActiveWalletAddress'); @@ -119,6 +149,7 @@ function App() { const handleWalletReady = async () => { try { const unlockedWallets = await VaultManager.getWallets(); + setHasVault(true); /* Only jump to last wallet if we just added one */ if (unlockedWallets.length > wallets.length && wallets.length > 0) { @@ -135,7 +166,8 @@ function App() { setWallets(unlockedWallets); setIsLocked(false); navigate('/dashboard'); - } catch { + } catch (e: any) { + console.error("Wallet ready failed:", e); setIsLocked(true); } }; @@ -193,16 +225,40 @@ function App() { return
Loading...
; } + const isLandingPage = (location.pathname.includes('/wallet/create') || location.pathname === '/onboarding') && wallets.length === 0 && !hasVault; + + if (isLandingPage) { + return ( +
+
+ + 0 || hasVault} + onCancel={() => navigate('/dashboard')} + showLinkModal={false} + onCloseLinkModal={() => { }} + /> + } /> + } /> + +
+
+ ); + } + return (
{/* Header */} {!isLocked && activeWallet && ( -
+
Lumen Lumen @@ -258,7 +314,7 @@ function App() { 0 || hasVault} onCancel={() => { }} showLinkModal={isLinkModalOpen} onCloseLinkModal={() => setIsLinkModalOpen(false)} @@ -266,15 +322,20 @@ function App() { ) : } /> 0} - onCancel={() => navigate('/dashboard')} - /* No modal for create flow */ - showLinkModal={false} - onCloseLinkModal={() => { }} - /> +
+ {isLocked && hasVault ? ( + + ) : ( + 0 || hasVault} + onCancel={() => navigate('/dashboard')} + showLinkModal={false} + onCloseLinkModal={() => { }} + /> + )} +
} /> : diff --git a/src/background.ts b/src/background.ts index c1f9095..9c5618d 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,27 +1,31 @@ // Set panel behavior -chrome.sidePanel - .setPanelBehavior({ openPanelOnActionClick: false }) - .catch((error) => 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 72ee80f..5d238ae 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'); @@ -56,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(() => { @@ -84,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 */ } }; @@ -127,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); } }; @@ -136,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(); @@ -203,58 +210,84 @@ 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.substring(0, 24)} + + {activeKeys.address.substring(24)} +
-
@@ -291,6 +324,7 @@ export const WalletTab: React.FC = ({ onWalletReady, activeKeys, setView('create-method')} onImportExisting={() => setView('import')} + onBack={onCancel} /> ); } @@ -311,6 +345,8 @@ export const WalletTab: React.FC = ({ onWalletReady, activeKeys, return ( setView('mnemonic-verify')} onBack={() => setView('create-method')} /> @@ -343,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); @@ -367,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/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/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/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..0e8070a 100644 --- a/src/components/staking/Staking.tsx +++ b/src/components/staking/Staking.tsx @@ -1,15 +1,17 @@ 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 { - fetchDelegations, - fetchRewards, +import { + fetchDelegations, + fetchRewards, fetchValidators, 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,42 +54,48 @@ 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; // 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 +111,29 @@ 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)); - + + // 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'); @@ -119,10 +148,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,8 +161,16 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { apr: '12.5%' // Calculate from chain params if available }; }); - + 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); } @@ -147,17 +184,34 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { const handleStake = async () => { if (!selectedValidator || !amount || !walletKeys) return; setLoading(true); - + 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(); } catch (error: any) { @@ -171,17 +225,23 @@ 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); - - setToastMessage(`Unstaked successfully! TX: ${txHash.slice(0, 8)}...`); + const amountUlmn = (parseFloat(stakeToUnstake.amount) * 1000000).toString(); + const txHash = await undelegateTokens(walletKeys, stakeToUnstake.validatorAddress, amountUlmn); + + setToastMessage(`Unstaked successfully! Asset is now unbonding. TX: ${txHash.slice(0, 8)}...`); setToastType('success'); setShowToast(true); - + // Refresh data await fetchUserStakingData(); } catch (error: any) { @@ -191,324 +251,517 @@ export const Staking: React.FC = ({ walletKeys, onBack }) => { setShowToast(true); } finally { setLoading(false); + setStakeToUnstake(null); } }; 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)}...`); + + // 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); - + // Refresh data await fetchUserStakingData(); } 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 -
-

{totalStaked} LMN

-
-
-
- - Rewards + {/* 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 +
+
-

{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-lg p-2.5 text-foreground text-base 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...

- +
+
+
+
))}
-
+ )} - {/* Stake Button */} - - +
+

Staked Assets

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

{stake.validator.moniker}

+

Staked: {stake.amount} LMN

+
+
+
+ +
+ ))} +
+ ) : ( +
No active stakes found.
+ )} +
+
)} -
- )} - {activeTab === 'unstake' && ( -
- {fetching ? ( -
- -

Loading stakes...

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

{stake.validator.moniker}

-

Staked: {stake.amount} LMN

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

{stake.validator.moniker}

+

+{stake.rewards} LMN Accumulated

+
+
+
-
- -
- )) - ) : ( -
-

No active stakes

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

Loading rewards...

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

{stake.validator.moniker}

-

+{stake.rewards} 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" + /> +
+ +
+
+ {amount && parseFloat(amount) <= 0 && ( +
+ + Amount must be greater than 0 +
+ )} +
+ + {/* Info Alert */} +
+ +

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

+
+
+ +
+ +
+
+ )} + + {/* 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/index.css b/src/index.css index 97a1578..ec21fec 100644 --- a/src/index.css +++ b/src/index.css @@ -51,13 +51,13 @@ [data-theme='light'] { color-scheme: light; - --bg: #ffffff; - --surface: #f9fafb; /* gray-50 */ - --surface-highlight: #e5e7eb; /* gray-200 */ - --border: #e5e7eb; - --foreground: #111827; /* gray-900 */ - --text-muted: #4b5563; /* gray-600 */ - --text-dim: #9ca3af; /* gray-400 */ + --bg: #f8fafc; /* slate-50 - very clean airy background */ + --surface: #ffffff; /* pure white for cards to float on */ + --surface-highlight: #f1f5f9; /* slate-100 */ + --border: #e2e8f0; /* slate-200 */ + --foreground: #020617; /* slate-950 */ + --text-muted: #475569; /* slate-600 */ + --text-dim: #64748b; /* slate-500 */ } html, body { @@ -141,3 +141,105 @@ button:not(:disabled):active { .animate-pulse-slow { animation: pulseSlow 3s ease-in-out infinite; } + +/* Premium UI Utilities */ +.premium-card { + position: relative; + background: var(--surface); + border: none; + overflow: hidden; + transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); /* Bouncy premium feel */ + box-shadow: 0 10px 50px -12px rgba(0, 0, 0, 0.1), 0 4px 12px -4px rgba(0, 0, 0, 0.05); +} + +.premium-card:hover { + transform: translateY(-4px) scale(1.01); + box-shadow: 0 30px 60px -12px rgba(0, 0, 0, 0.15), 0 18px 36px -18px rgba(0, 0, 0, 0.1); +} + +[data-theme='dark'] .premium-card { + box-shadow: 0 10px 50px -12px rgba(0, 0, 0, 0.5), 0 4px 12px -4px rgba(0, 0, 0, 0.3); +} + +[data-theme='dark'] .premium-card:hover { + box-shadow: 0 30px 60px -12px rgba(0, 0, 0, 0.6), 0 18px 36px -18px rgba(0, 0, 0, 0.4); +} + +.premium-card::before { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at 0% 0%, rgba(99, 102, 241, 0.15) 0%, transparent 50%), + radial-gradient(circle at 100% 100%, rgba(0, 209, 255, 0.1) 0%, transparent 50%); + opacity: 0.6; + transition: opacity 0.5s ease; +} + +[data-theme='light'] .premium-card::before { + background: radial-gradient(circle at 0% 0%, rgba(99, 102, 241, 0.04) 0%, transparent 70%), + radial-gradient(circle at 100% 100%, rgba(0, 209, 255, 0.03) 0%, transparent 70%); +} + +.premium-card:hover::before { + opacity: 1; +} + +.premium-header { + position: relative; + background: transparent; /* Seamless blend */ + overflow: visible; /* Allow menu overflow */ +} + +/* Removed ::after border line */ + +.premium-header::before { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at 10% 0%, rgba(99, 102, 241, 0.1) 0%, transparent 40%), + radial-gradient(circle at 90% 100%, rgba(0, 209, 255, 0.05) 0%, transparent 40%); + opacity: 0.6; + pointer-events: none; +} + +[data-theme='light'] .premium-header::before { + background: radial-gradient(circle at 10% 0%, rgba(99, 102, 241, 0.03) 0%, transparent 50%), + radial-gradient(circle at 90% 100%, rgba(0, 209, 255, 0.02) 0%, transparent 50%); +} + +.mesh-gradient { + background-image: + radial-gradient(at 0% 0%, hsla(253,16%,7%,1) 0, transparent 50%), + radial-gradient(at 50% 0%, hsla(225,39%,30%,1) 0, transparent 50%), + radial-gradient(at 100% 0%, hsla(339,49%,30%,1) 0, transparent 50%); +} + +[data-theme='light'] .mesh-gradient { + background-image: + radial-gradient(at 0% 0%, hsla(210, 40%, 98%, 1) 0, transparent 50%), + radial-gradient(at 50% 0%, hsla(210, 40%, 94%, 1) 0, transparent 50%), + radial-gradient(at 100% 0%, hsla(210, 40%, 96%, 1) 0, transparent 50%); +} + +.glass-squircle { + @apply relative overflow-hidden transition-all duration-300; + background: rgba(255, 255, 255, 0.03); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.05); +} + +[data-theme='light'] .glass-squircle { + background: rgba(15, 23, 42, 0.03); + border: 1px solid rgba(15, 23, 42, 0.05); +} + +.glass-squircle:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.1); +} + +[data-theme='light'] .glass-squircle:hover { + background: rgba(15, 23, 42, 0.06); + border-color: rgba(15, 23, 42, 0.1); +} 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/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/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 */ 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'; + } + } }, }, },