diff --git a/src/context/GlobalContext.js b/src/context/GlobalContext.js index 5615c56ec..98512cd36 100644 --- a/src/context/GlobalContext.js +++ b/src/context/GlobalContext.js @@ -16,6 +16,9 @@ export const GlobalProvider = ({ children }) => { const [selectedKeyPairIndex, setSelectedKeyPairIndex] = useState(0); const [isAuthenticated, setIsAuthenticated] = useState(false); const [storedPassword, setStoredPassword] = useState(''); + + // Alert state for modals + const [alert, setAlert] = useState({ isOpen: false, message: '' }); // Function to encrypt and store key pairs const saveKeyPairsToStorage = (keyPairs, password) => { @@ -39,6 +42,7 @@ export const GlobalProvider = ({ children }) => { callback(decryptedKeyPairs); } catch (err) { console.error('Error decrypting key pairs:', err); + setAlert({ isOpen: true, message: 'Failed to decrypt key pairs. Please check your password.' }); callback([]); } } else { @@ -47,6 +51,92 @@ export const GlobalProvider = ({ children }) => { }); }; + // Function to append new key pairs while preventing duplicates and validating them + const appendKeyPairs = (newKeyPairs) => { + const password = storedPassword; + if (!password) { + console.error('Password is not available'); + setAlert({ isOpen: true, message: 'Password is not available. Please log in again.' }); + return; + } + + // Validate each key pair + for (let i = 0; i < newKeyPairs.length; i++) { + const keyPair = newKeyPairs[i]; + if (!keyPair.publicKey || !keyPair.privateKey) { + setAlert({ isOpen: true, message: `Key pair at index ${i} is missing publicKey or privateKey.` }); + console.error(`Key pair at index ${i} is missing publicKey or privateKey.`); + return; + } + + try { + // Decode private key from Base58 + const decodedPrivateKey = Base58.decode(keyPair.privateKey); + if (decodedPrivateKey.length !== 32) { + setAlert({ isOpen: true, message: `Private key at index ${i} is not 32 bytes.` }); + console.error(`Private key at index ${i} is not 32 bytes.`); + return; + } + + // Derive public key from private key using nacl.sign.keyPair.fromSeed + const derivedKeyPair = nacl.sign.keyPair.fromSeed(decodedPrivateKey); + const derivedPublicKey = Base58.encode(derivedKeyPair.publicKey); + + // Compare derived public key with provided public key + if (derivedPublicKey !== keyPair.publicKey) { + setAlert({ isOpen: true, message: `Public key does not match private key at index ${i}.` }); + console.error(`Public key does not match private key at index ${i}.`); + return; + } + } catch (err) { + console.error('Error validating key pair:', err); + setAlert({ isOpen: true, message: `Error validating key pair at index ${i}.` }); + return; + } + } + + // Load existing key pairs + loadKeyPairsFromStorage(password, (existingKeyPairs) => { + // Filter out duplicates + const uniqueNewKeyPairs = newKeyPairs.filter(newKey => { + return !existingKeyPairs.some(existingKey => + existingKey.publicKey === newKey.publicKey && + existingKey.privateKey === newKey.privateKey + ); + }); + + if (uniqueNewKeyPairs.length === 0) { + console.log('No new unique key pairs to add.'); + setAlert({ isOpen: true, message: 'No new unique key pairs to add.' }); + return; + } + + const updatedKeyPairs = [...existingKeyPairs, ...uniqueNewKeyPairs]; + saveKeyPairsToStorage(updatedKeyPairs, password); + setKeyPairs(updatedKeyPairs); + + // Update selected key pair to the last one added + const newIndex = updatedKeyPairs.length - 1; + setSelectedKeyPairIndex(newIndex); + setPublicKey(updatedKeyPairs[newIndex].publicKey); + setPrivateKey(updatedKeyPairs[newIndex].privateKey); + saveSelectedKeyPairIndex(newIndex); + + // Update 'store' with the new key pair + const encryptedPrivateKey = CryptoJS.AES.encrypt(updatedKeyPairs[newIndex].privateKey, password).toString(); + const hash = CryptoJS.SHA256(password).toString(CryptoJS.enc.Hex); + const store = { + hash, + publicKey: updatedKeyPairs[newIndex].publicKey, + encryptedPrivateKey: encryptedPrivateKey, + history: [], + }; + chrome.storage.sync.set({ store }, () => { + console.log('Store updated with new key pair'); + }); + }); + }; + // Function to save selected key pair index const saveSelectedKeyPairIndex = (index) => { chrome.storage.local.set({ selectedKeyPairIndex: index }, () => {}); @@ -65,16 +155,29 @@ export const GlobalProvider = ({ children }) => { const password = storedPassword; if (!password) { console.error('Password is not available'); + setAlert({ isOpen: true, message: 'Password is not available. Please log in again.' }); return; } const keyPair = nacl.sign.keyPair(); const newPublicKey = Base58.encode(keyPair.publicKey); - const newPrivateKey = Base58.encode(keyPair.secretKey.slice(0, 32)); + const newPrivateKey = Base58.encode(keyPair.secretKey.slice(0, 32)); // Using the first 32 bytes as seed const newKeyPair = { publicKey: newPublicKey, privateKey: newPrivateKey }; // Load existing key pairs loadKeyPairsFromStorage(password, (existingKeyPairs) => { + // Check for duplicates before adding + const isDuplicate = existingKeyPairs.some(existingKey => + existingKey.publicKey === newKeyPair.publicKey && + existingKey.privateKey === newKeyPair.privateKey + ); + + if (isDuplicate) { + console.log('Generated key pair is a duplicate. Skipping.'); + setAlert({ isOpen: true, message: 'Generated key pair is a duplicate. Skipping.' }); + return; + } + const updatedKeyPairs = [...existingKeyPairs, newKeyPair]; // Save updated key pairs saveKeyPairsToStorage(updatedKeyPairs, password); @@ -109,12 +212,14 @@ export const GlobalProvider = ({ children }) => { const password = storedPassword; if (!password) { console.error('Password is not available'); + setAlert({ isOpen: true, message: 'Password is not available. Please log in again.' }); return; } loadKeyPairsFromStorage(password, (existingKeyPairs) => { if (existingKeyPairs.length <= 1) { console.error('Cannot delete the last remaining key pair.'); + setAlert({ isOpen: true, message: 'Cannot delete the last remaining key pair.' }); return; } @@ -122,16 +227,32 @@ export const GlobalProvider = ({ children }) => { const updatedKeyPairs = [...existingKeyPairs]; updatedKeyPairs.splice(index, 1); - // Immediately update the key pairs state - setKeyPairs(updatedKeyPairs); - // Save the updated keyPairs back to storage saveKeyPairsToStorage(updatedKeyPairs, password); + setKeyPairs(updatedKeyPairs); // Reset to the first key pair after deletion setSelectedKeyPairIndex(0); - setPublicKey(updatedKeyPairs.length > 0 ? updatedKeyPairs[0].publicKey : ''); - setPrivateKey(updatedKeyPairs.length > 0 ? updatedKeyPairs[0].privateKey : ''); + if (updatedKeyPairs.length > 0) { + setPublicKey(updatedKeyPairs[0].publicKey); + setPrivateKey(updatedKeyPairs[0].privateKey); + saveSelectedKeyPairIndex(0); + } + + // Update 'store' with the new selected key pair + if (updatedKeyPairs.length > 0) { + const encryptedPrivateKey = CryptoJS.AES.encrypt(updatedKeyPairs[0].privateKey, password).toString(); + const hash = CryptoJS.SHA256(password).toString(CryptoJS.enc.Hex); + const store = { + hash, + publicKey: updatedKeyPairs[0].publicKey, + encryptedPrivateKey: encryptedPrivateKey, + history: [], + }; + chrome.storage.sync.set({ store }, () => { + console.log('Store updated with selected key pair after deletion'); + }); + } // Optionally call the callback if (callback) { @@ -172,7 +293,7 @@ export const GlobalProvider = ({ children }) => { }, []); // Function to set selected key pair - const setSelectedKeyPair = (index) => { + const setSelectedKeyPairFn = (index) => { if (keyPairs[index]) { setPublicKey(keyPairs[index].publicKey); setPrivateKey(keyPairs[index].privateKey); @@ -182,6 +303,7 @@ export const GlobalProvider = ({ children }) => { const password = storedPassword; if (!password) { console.error('Password is not available'); + setAlert({ isOpen: true, message: 'Password is not available. Please log in again.' }); return; } const encryptedPrivateKey = CryptoJS.AES.encrypt(keyPairs[index].privateKey, password).toString(); @@ -216,12 +338,15 @@ export const GlobalProvider = ({ children }) => { generateKeyPair, selectedKeyPairIndex, setSelectedKeyPairIndex, - setSelectedKeyPair, + setSelectedKeyPair: setSelectedKeyPairFn, isAuthenticated, setIsAuthenticated, storedPassword, setStoredPassword, deleteKeyPair, + appendKeyPairs, + alert, // For alert modal + setAlert, // For alert modal }} > {children} diff --git a/src/css/App.css b/src/css/App.css index d95dd29a7..fd9fcd15e 100644 --- a/src/css/App.css +++ b/src/css/App.css @@ -3706,33 +3706,95 @@ tr:hover { opacity: 0.7; } +/* Container for keypair actions */ .keypair-actions { display: flex; - justify-content: space-between; + gap: 10px; + justify-content: center; margin-top: 10px; } +/* Badge Button Styles */ .badge-button { background-color: #3c4e63; /* Matches your theme's primary color */ color: white; border: none; - border-radius: 20px; - padding: 10px 20px; + border-radius: 50%; /* Makes the button circular */ + padding: 10px; /* Uniform padding */ font-size: 14px; cursor: pointer; - width: 48%; /* Ensures equal width for both buttons */ - text-align: center; + width: 40px; /* Fixed width suitable for icons */ + height: 40px; /* Fixed height suitable for icons */ display: flex; align-items: center; justify-content: center; + transition: background-color 0.3s; } .badge-button:hover { background-color: #50647e; /* A slightly lighter shade for hover effect */ } +/* Remove margin-left from SVGs since there's no text */ .badge-button svg { - margin-left: 8px; /* Adds space between the text and icon */ + margin: 0; /* Eliminates any unintended spacing */ + width: 24px; /* Adjust icon size as needed */ + height: 24px; /* Adjust icon size as needed */ } +/* Tooltip container */ +.button-with-tooltip { + position: relative; + display: inline-block; +} + +/* Tooltip text */ +.tooltip-text { + visibility: hidden; + width: max-content; + background-color: black; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px 8px; + position: absolute; + z-index: 1; + bottom: 125%; /* Position above the button */ + left: 50%; + transform: translateX(-50%); + opacity: 0; + transition: opacity 0.3s; +} +/* Tooltip arrow */ +.tooltip-text::after { + content: ""; + position: absolute; + top: 100%; /* At the bottom of the tooltip */ + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: black transparent transparent transparent; +} + +/* Show the tooltip text when hovering over the container */ +.button-with-tooltip:hover .tooltip-text { + visibility: visible; + opacity: 1; +} + +/* Additional styling for .keypair-actions to align buttons */ +.keypair-actions { + display: flex; + gap: 10px; + justify-content: center; + margin-top: 10px; +} + +/* Centered icon class (optional if not needed) */ +.centered-icon { + display: flex; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index df05436e7..bb2820919 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -25,6 +25,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import DownloadIcon from '@mui/icons-material/Download'; import DeleteIcon from '@mui/icons-material/Delete'; import SaveAltIcon from '@mui/icons-material/SaveAlt'; +import UploadIcon from '@mui/icons-material/Upload'; import React, { useRef, useState, useEffect, useContext } from 'react'; import Lottie from 'react-lottie'; import versionData from '../data/version.json'; @@ -47,6 +48,9 @@ function Dashboard() { setSelectedKeyPair, setIsAuthenticated, deleteKeyPair, + appendKeyPairs, + alert, // For alert modal + setAlert, // For alert modal } = useContext(GlobalContext); const [tabId, setTabId] = useState(null); @@ -64,16 +68,17 @@ function Dashboard() { const [error, setError] = useState(''); const [jsonFileName, setJsonFileName] = useState(''); const fileInputRef = useRef(null); + const keyPairFileInputRef = useRef(null); const navigate = useNavigate(); - // New state variables for transaction data and handling + // State variables for transaction data and handling const [transactionData, setTransactionData] = useState(null); const [transactionError, setTransactionError] = useState(''); const [showSuccessModal, setShowSuccessModal] = useState(false); const [successResponse, setSuccessResponse] = useState(null); const [showDeleteModal, setShowDeleteModal] = useState(false); - // New state for copying transaction ID + // State for copying transaction ID const [isIdCopied, setIsIdCopied] = useState(false); const defaultOptions = { @@ -225,6 +230,7 @@ function Dashboard() { const addNet = () => { if (!newNetName.trim() || !customUrl.trim()) { setError('Both fields are required.'); + setAlert({ isOpen: true, message: 'Both Net Name and GraphQL URL are required.' }); return; } setError(''); @@ -245,6 +251,7 @@ function Dashboard() { const toggleConnection = () => { if (!publicKey || !privateKey) { + setAlert({ isOpen: true, message: 'Public or Private key is missing.' }); console.error('Public or Private key is missing'); return; } @@ -363,7 +370,7 @@ function Dashboard() { setSelectedKeyPairIndex(0); setSelectedKeyPair(0); }); - + // Close the delete confirmation modal setShowDeleteModal(false); } @@ -383,6 +390,7 @@ function Dashboard() { setIsCopied(false); }, 1500); } catch (err) { + setAlert({ isOpen: true, message: 'Unable to copy text.' }); console.error('Unable to copy text: ', err); } }; @@ -433,11 +441,13 @@ function Dashboard() { } else { setTransactionData(null); setTransactionError('Invalid JSON format: Missing required fields.'); + setAlert({ isOpen: true, message: 'Invalid JSON format: Missing required fields.' }); } } catch (err) { console.error('Error parsing JSON:', err); setTransactionData(null); setTransactionError('Invalid JSON format.'); + setAlert({ isOpen: true, message: 'Invalid JSON format.' }); } }; reader.readAsText(file); @@ -445,6 +455,39 @@ function Dashboard() { setJsonFileName(''); // Clear if the file is not JSON setTransactionData(null); setTransactionError('Please upload a JSON file.'); + setAlert({ isOpen: true, message: 'Please upload a valid JSON file.' }); + } + }; + + // Function to handle file upload for key pairs + const handleKeyPairFileUpload = (e) => { + const file = e.target.files[0]; + if (file && file.type === 'application/json') { + const reader = new FileReader(); + reader.onload = (event) => { + try { + const uploadedKeyPairs = JSON.parse(event.target.result); + + // Ensure the uploaded data is either an array or an object + if (Array.isArray(uploadedKeyPairs)) { + appendKeyPairs(uploadedKeyPairs); + // After appending, the context sets the selectedKeyPairIndex to the last one + } else if (uploadedKeyPairs.publicKey && uploadedKeyPairs.privateKey) { + appendKeyPairs([uploadedKeyPairs]); // Wrap single key pair into an array + // After appending, the context sets the selectedKeyPairIndex to the last one + } else { + console.error('Invalid JSON format for key pairs.'); + setAlert({ isOpen: true, message: 'Invalid JSON format for key pairs.' }); + } + } catch (err) { + console.error('Error parsing JSON:', err); + setAlert({ isOpen: true, message: 'Error parsing JSON file.' }); + } + }; + reader.readAsText(file); + } else { + console.error('Please upload a valid JSON file.'); + setAlert({ isOpen: true, message: 'Please upload a valid JSON file.' }); } }; @@ -476,11 +519,13 @@ function Dashboard() { } else { setTransactionData(null); setTransactionError('Invalid JSON format: Missing required fields.'); + setAlert({ isOpen: true, message: 'Invalid JSON format: Missing required fields.' }); } } catch (err) { console.error('Error parsing JSON:', err); setTransactionData(null); setTransactionError('Invalid JSON format.'); + setAlert({ isOpen: true, message: 'Invalid JSON format.' }); } }; reader.readAsText(file); @@ -488,20 +533,27 @@ function Dashboard() { setJsonFileName(''); setTransactionData(null); setTransactionError('Please upload a JSON file.'); + setAlert({ isOpen: true, message: 'Please upload a valid JSON file.' }); } }; const handleFileClick = () => { - fileInputRef.current.click(); // Open file explorer when clicking on the field + fileInputRef.current.click(); + }; + + const handleKeyPairFileClick = () => { + keyPairFileInputRef.current.click(); }; const handleSubmit = () => { if (!transactionData) { setTransactionError('No valid transaction data found.'); + setAlert({ isOpen: true, message: 'No valid transaction data found.' }); return; } if (!isConnected) { setTransactionError('Please connect to a net before submitting a transaction.'); + setAlert({ isOpen: true, message: 'Please connect to a net before submitting a transaction.' }); return; } @@ -520,11 +572,12 @@ function Dashboard() { setTransactionData(null); } else { setTransactionError(response.error || 'Transaction submission failed.'); + setAlert({ isOpen: true, message: response.error || 'Transaction submission failed.' }); } }); }; - // New function to handle transaction ID click + // Function to handle transaction ID click const handleIdClick = () => { try { const transactionId = (successResponse && successResponse.postTransaction && successResponse.postTransaction.id) || ''; @@ -539,11 +592,12 @@ function Dashboard() { setIsIdCopied(false); }, 1500); } catch (err) { + setAlert({ isOpen: true, message: 'Unable to copy transaction ID.' }); console.error('Unable to copy text: ', err); } }; - // **New function to handle favicon load error** + // Function to handle favicon load error const handleFaviconError = () => { setFaviconUrl(''); // This will trigger the globe icon to display }; @@ -570,11 +624,15 @@ function Dashboard() { const handleGenerateKeyPair = () => { generateKeyPair(() => { - setSelectedKeyPairIndex(keyPairs.length); // Select the newly generated key pair + setSelectedKeyPairIndex(keyPairs.length - 1); // Select the newly generated key pair disconnectDueToKeysChange(); }); }; - + + // Function to close the alert modal + const closeAlertModal = () => { + setAlert({ isOpen: false, message: '' }); + }; return ( <> @@ -594,6 +652,7 @@ function Dashboard() { @@ -623,7 +682,7 @@ function Dashboard() {
No transaction ID found.
)} -{alert.message}
+This action is irreversible and will delete the selected key pair forever.
@@ -832,6 +926,7 @@ function Dashboard() { > ); + } export default Dashboard; \ No newline at end of file