This is an automated email from the ASF dual-hosted git repository. apratim pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/incubator-resilientdb-resvault.git
The following commit(s) were added to refs/heads/main by this push: new 0c5d1fc Added smart contract deployment support 0c5d1fc is described below commit 0c5d1fcb0b7fc968ab94755382c650b10561a3f0 Author: Apratim Shukla <apratimshuk...@gmail.com> AuthorDate: Fri Nov 22 00:14:37 2024 -0800 Added smart contract deployment support - ResVault now allows you to upload a solidity contract, with JSON configuration and deploy it. - In-built owner's address generation through public/private keys stored within ResVault. - Integrated with add address method and then execution of corresponding mutations. - Multiple nets support similar to KV service. - Consistent UI. --- .gitignore | 2 + package-lock.json | 11 + package.json | 1 + public/background.js | 170 +++++++++ src/App.js | 10 +- src/context/GlobalContext.js | 90 +++-- src/css/App.css | 8 + src/pages/{Dashboard.jsx => Contract.jsx} | 569 +++++++++++++++++++----------- src/pages/Dashboard.jsx | 21 +- 9 files changed, 650 insertions(+), 232 deletions(-) diff --git a/.gitignore b/.gitignore index 4d29575..5ce422f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +build.zip diff --git a/package-lock.json b/package-lock.json index 3f9db0a..65376ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "crypto": "^1.0.1", "crypto-browserify": "^3.12.0", "crypto-js": "^4.1.1", + "js-sha3": "^0.9.3", "jssha": "^3.3.0", "mnemonic-seed-js": "^0.3.1", "moment": "^2.29.4", @@ -15164,6 +15165,11 @@ "node": ">=0.10.0" } }, + "node_modules/js-sha3": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz", + "integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -37558,6 +37564,11 @@ "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==" }, + "js-sha3": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz", + "integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index 27359d2..822b2c4 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "crypto": "^1.0.1", "crypto-browserify": "^3.12.0", "crypto-js": "^4.1.1", + "js-sha3": "^0.9.3", "jssha": "^3.3.0", "mnemonic-seed-js": "^0.3.1", "moment": "^2.29.4", diff --git a/public/background.js b/public/background.js index 0f09cf3..7f724cd 100644 --- a/public/background.js +++ b/public/background.js @@ -554,6 +554,176 @@ chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { }); })(); + return true; // Keep the message channel open for async sendResponse + } else if (request.action === 'deployContractChain') { + // Handler for deploying contract chain + (async function() { + const domain = request.domain; + const net = request.net; + const ownerAddress = request.ownerAddress; + const soliditySource = request.soliditySource; + const deployConfig = request.deployConfig; // Contains arguments and contract_name + + // Retrieve the signer's keys and URL from storage + chrome.storage.local.get(['keys', 'connectedNets'], async function (result) { + const keys = result.keys || {}; + const connectedNets = result.connectedNets || {}; + + if (!connectedNets[domain] || connectedNets[domain] !== net) { + sendResponse({ success: false, error: 'Not connected to the specified net for this domain.' }); + return; + } + + if (!keys[domain] || !keys[domain][net]) { + sendResponse({ success: false, error: 'Keys not found for the specified domain and net.' }); + return; + } + + const { url, exportedKey } = keys[domain][net]; + + try { + // Import the key material from JWK format + const keyMaterial = await crypto.subtle.importKey( + 'jwk', + exportedKey, + { name: 'AES-GCM', + }, + true, + ['encrypt', 'decrypt'] + ); + + const decryptedUrl = await decryptData(url.ciphertext, url.iv, keyMaterial); + + // 1. Perform addAddress mutation + const addAddressMutation = ` + mutation { + addAddress( + config: "5 127.0.0.1 10005", + address: "${escapeGraphQLString(ownerAddress)}", + type: "data" + ) + } + `; + + const addAddressResponse = await fetch(decryptedUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query: addAddressMutation }), + }); + + if (!addAddressResponse.ok) { + throw new Error(`Network response was not ok: ${addAddressResponse.statusText}`); + } + + const addAddressResult = await addAddressResponse.json(); + if (addAddressResult.errors) { + console.error('GraphQL errors in addAddress:', addAddressResult.errors); + sendResponse({ success: false, error: 'Error in addAddress mutation.', errors: addAddressResult.errors }); + return; + } + + // Check if addAddress was successful + if (addAddressResult.data && addAddressResult.data.addAddress === "Address added successfully") { + // 2. Perform compileContract mutation + const escapedSoliditySource = escapeGraphQLString(soliditySource); + + const compileContractMutation = ` + mutation { + compileContract( + source: """${escapedSoliditySource}""", + type: "data" + ) + } + `; + + const compileContractResponse = await fetch(decryptedUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query: compileContractMutation }), + }); + + if (!compileContractResponse.ok) { + throw new Error(`Network response was not ok: ${compileContractResponse.statusText}`); + } + + const compileContractResult = await compileContractResponse.json(); + if (compileContractResult.errors) { + console.error('GraphQL errors in compileContract:', compileContractResult.errors); + sendResponse({ success: false, error: 'Error in compileContract mutation.', errors: compileContractResult.errors }); + return; + } + + // Extract the contract filename + const contractFilename = compileContractResult.data.compileContract; + if (!contractFilename) { + sendResponse({ success: false, error: 'Failed to compile contract.' }); + return; + } + + // 3. Perform deployContract mutation + const { arguments: args, contract_name } = deployConfig; + const deployContractMutation = ` + mutation { + deployContract( + config: "5 127.0.0.1 10005", + contract: "${escapeGraphQLString(contractFilename)}", + name: "/tmp/${escapeGraphQLString(contractFilename.replace('.json', '.sol'))}:${escapeGraphQLString(contract_name)}", + arguments: "${escapeGraphQLString(args)}", + owner: "${escapeGraphQLString(ownerAddress)}", + type: "data" + ){ + ownerAddress + contractAddress + contractName + } + } + `; + + const deployContractResponse = await fetch(decryptedUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query: deployContractMutation }), + }); + + if (!deployContractResponse.ok) { + throw new Error(`Network response was not ok: ${deployContractResponse.statusText}`); + } + + const deployContractResult = await deployContractResponse.json(); + if (deployContractResult.errors) { + console.error('GraphQL errors in deployContract:', deployContractResult.errors); + sendResponse({ success: false, error: 'Error in deployContract mutation.', errors: deployContractResult.errors }); + return; + } + + // Extract the contract address and return success + if (deployContractResult.data && deployContractResult.data.deployContract && deployContractResult.data.deployContract.contractAddress) { + const contractAddress = deployContractResult.data.deployContract.contractAddress; + sendResponse({ success: true, contractAddress: contractAddress }); + return; + } else { + sendResponse({ success: false, error: 'Failed to deploy contract.' }); + return; + } + + } else { + sendResponse({ success: false, error: 'Failed to add address.' }); + return; + } + + } catch (error) { + console.error('Error deploying contract chain:', error); + sendResponse({ success: false, error: error.message }); + } + }); + })(); + return true; // Keep the message channel open for async sendResponse } }); \ No newline at end of file diff --git a/src/App.js b/src/App.js index 1223c1f..4fc7b1f 100644 --- a/src/App.js +++ b/src/App.js @@ -6,11 +6,12 @@ import SignUp from "./pages/SignUp"; import Login from "./pages/Login"; import Dashboard from "./pages/Dashboard"; import Logs from "./pages/Logs"; +import Contract from "./pages/Contract"; import { Routes, Route, Navigate } from 'react-router-dom'; import { GlobalContext } from './context/GlobalContext'; function App() { - const { isAuthenticated } = useContext(GlobalContext); + const { serviceMode, isAuthenticated } = useContext(GlobalContext); return ( <Routes> @@ -21,11 +22,16 @@ function App() { <Route path="/login" element={<Login />} /> <Route path="*" element={<Navigate to="/" replace />} /> </> - ) : ( + ) : serviceMode === 'KV' ? ( <> <Route path="/dashboard" element={<Dashboard />} /> <Route path="*" element={<Navigate to="/dashboard" replace />} /> </> + ) : ( + <> + <Route path="/contract" element={<Contract />} /> + <Route path="*" element={<Navigate to="/contract" replace />} /> + </> )} </Routes> ); diff --git a/src/context/GlobalContext.js b/src/context/GlobalContext.js index 98512cd..04e068c 100644 --- a/src/context/GlobalContext.js +++ b/src/context/GlobalContext.js @@ -1,25 +1,39 @@ /*global chrome*/ import React, { createContext, useState, useEffect } from 'react'; import CryptoJS from 'crypto-js'; +import { keccak256 } from 'js-sha3'; import nacl from 'tweetnacl'; import Base58 from 'bs58'; +import { useNavigate } from 'react-router-dom'; export const GlobalContext = createContext(); export const GlobalProvider = ({ children }) => { + const [serviceMode, setServiceMode] = useState('KV'); // KV or Contract const [values, setValues] = useState({ password: '', showPassword: false }); const [confirmValues, setConfirmValues] = useState({ password: '', showPassword: false }); const [loginValues, setLoginValues] = useState({ password: '', showPassword: false }); const [publicKey, setPublicKey] = useState(''); const [privateKey, setPrivateKey] = useState(''); + const [ownerAddress, setOwnerAddress] = useState(''); // To store the generated owner's address const [keyPairs, setKeyPairs] = useState([]); const [selectedKeyPairIndex, setSelectedKeyPairIndex] = useState(0); const [isAuthenticated, setIsAuthenticated] = useState(false); const [storedPassword, setStoredPassword] = useState(''); + + const navigate = useNavigate(); // Alert state for modals const [alert, setAlert] = useState({ isOpen: false, message: '' }); + // Generate owner's address + const generateOwnerAddress = (publicKey) => { + const decodedPublicKey = Base58.decode(publicKey); + const addressHash = keccak256(Buffer.from(decodedPublicKey)); + const address = `0x${addressHash.slice(-40)}`; + return address; + }; + // Function to encrypt and store key pairs const saveKeyPairsToStorage = (keyPairs, password) => { const encryptedKeyPairs = CryptoJS.AES.encrypt( @@ -122,6 +136,10 @@ export const GlobalProvider = ({ children }) => { setPrivateKey(updatedKeyPairs[newIndex].privateKey); saveSelectedKeyPairIndex(newIndex); + // Generate and set the ownerAddress + const ownerAddress = generateOwnerAddress(updatedKeyPairs[newIndex].publicKey); + setOwnerAddress(ownerAddress); + // 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); @@ -162,6 +180,7 @@ export const GlobalProvider = ({ children }) => { const keyPair = nacl.sign.keyPair(); const newPublicKey = Base58.encode(keyPair.publicKey); const newPrivateKey = Base58.encode(keyPair.secretKey.slice(0, 32)); // Using the first 32 bytes as seed + const ownerAddress = generateOwnerAddress(newPublicKey); const newKeyPair = { publicKey: newPublicKey, privateKey: newPrivateKey }; // Load existing key pairs @@ -184,6 +203,7 @@ export const GlobalProvider = ({ children }) => { setKeyPairs(updatedKeyPairs); setPublicKey(newPublicKey); setPrivateKey(newPrivateKey); + setOwnerAddress(ownerAddress); const newIndex = updatedKeyPairs.length - 1; setSelectedKeyPairIndex(newIndex); saveSelectedKeyPairIndex(newIndex); @@ -237,6 +257,10 @@ export const GlobalProvider = ({ children }) => { setPublicKey(updatedKeyPairs[0].publicKey); setPrivateKey(updatedKeyPairs[0].privateKey); saveSelectedKeyPairIndex(0); + + // Generate and set the ownerAddress + const ownerAddress = generateOwnerAddress(updatedKeyPairs[0].publicKey); + setOwnerAddress(ownerAddress); } // Update 'store' with the new selected key pair @@ -275,10 +299,19 @@ export const GlobalProvider = ({ children }) => { setPublicKey(loadedKeyPairs[index].publicKey); setPrivateKey(loadedKeyPairs[index].privateKey); setSelectedKeyPairIndex(index); + + // Generate and set the ownerAddress + const ownerAddress = generateOwnerAddress(loadedKeyPairs[index].publicKey); + setOwnerAddress(ownerAddress); + } else if (loadedKeyPairs.length > 0) { setPublicKey(loadedKeyPairs[0].publicKey); setPrivateKey(loadedKeyPairs[0].privateKey); setSelectedKeyPairIndex(0); + + // Generate and set the ownerAddress + const ownerAddress = generateOwnerAddress(loadedKeyPairs[0].publicKey); + setOwnerAddress(ownerAddress); } setIsAuthenticated(true); }); @@ -300,6 +333,10 @@ export const GlobalProvider = ({ children }) => { setSelectedKeyPairIndex(index); saveSelectedKeyPairIndex(index); + // Generate and set the ownerAddress + const ownerAddress = generateOwnerAddress(keyPairs[index].publicKey); + setOwnerAddress(ownerAddress); + const password = storedPassword; if (!password) { console.error('Password is not available'); @@ -320,33 +357,46 @@ export const GlobalProvider = ({ children }) => { } }; + useEffect(() => { + // Retrieve the mode from storage when the app starts + chrome.storage.local.get(['serviceMode'], (result) => { + if (result.serviceMode) { + setServiceMode(result.serviceMode); + navigate(result.serviceMode === 'KV' ? '/dashboard' : '/contract'); + } + }); + }, []); + + const toggleServiceMode = () => { + const newMode = serviceMode === 'KV' ? 'Contract' : 'KV'; + setServiceMode(newMode); + chrome.storage.local.set({ serviceMode: newMode }, () => { + // Clear connections when mode changes + chrome.storage.local.remove(['connections'], () => { + navigate(newMode === 'KV' ? '/dashboard' : '/contract'); + }); + }); + }; + return ( <GlobalContext.Provider value={{ - values, - setValues, - confirmValues, - setConfirmValues, - loginValues, - setLoginValues, - publicKey, - setPublicKey, - privateKey, - setPrivateKey, - keyPairs, - setKeyPairs, + values, setValues, + confirmValues, setConfirmValues, + loginValues, setLoginValues, + publicKey, setPublicKey, + privateKey, setPrivateKey, + keyPairs, setKeyPairs, generateKeyPair, - selectedKeyPairIndex, - setSelectedKeyPairIndex, + selectedKeyPairIndex, setSelectedKeyPairIndex, setSelectedKeyPair: setSelectedKeyPairFn, - isAuthenticated, - setIsAuthenticated, - storedPassword, - setStoredPassword, + isAuthenticated, setIsAuthenticated, + storedPassword, setStoredPassword, deleteKeyPair, appendKeyPairs, - alert, // For alert modal - setAlert, // For alert modal + alert, setAlert, + serviceMode, setServiceMode, toggleServiceMode, + ownerAddress, setOwnerAddress, }} > {children} diff --git a/src/css/App.css b/src/css/App.css index fd9fcd1..3007d8b 100644 --- a/src/css/App.css +++ b/src/css/App.css @@ -3179,9 +3179,17 @@ body.panel-closing .page, body.panel-closing .bottom-toolbar { width: 100%; } +.file-upload-row { + display: flex; + flex-direction: row; + justify-content: space-between; +} + .file-upload { user-select: none; cursor: pointer; + flex: 1; + margin: 0 5px; } .drag_box { diff --git a/src/pages/Dashboard.jsx b/src/pages/Contract.jsx similarity index 63% copy from src/pages/Dashboard.jsx copy to src/pages/Contract.jsx index bb28209..fb40d06 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Contract.jsx @@ -34,8 +34,10 @@ import { faLock, faUnlock } from '@fortawesome/free-solid-svg-icons'; import graphql from "../images/logos/graphql.png"; import { GlobalContext } from '../context/GlobalContext'; import { useNavigate } from 'react-router-dom'; +import { keccak256 } from 'js-sha3'; +import Base58 from 'bs58'; -function Dashboard() { +function Contract() { const { publicKey, privateKey, @@ -51,6 +53,10 @@ function Dashboard() { appendKeyPairs, alert, // For alert modal setAlert, // For alert modal + serviceMode, + toggleServiceMode, + ownerAddress, + setOwnerAddress, } = useContext(GlobalContext); const [tabId, setTabId] = useState(null); @@ -71,15 +77,21 @@ function Dashboard() { const keyPairFileInputRef = useRef(null); const navigate = useNavigate(); - // 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); + // New State Variables for Deployment + const [solidityFileName, setSolidityFileName] = useState(''); + const [solidityContent, setSolidityContent] = useState(''); + const [deployJsonFileName, setDeployJsonFileName] = useState(''); + const [deployJsonContent, setDeployJsonContent] = useState(''); + const [deployError, setDeployError] = useState(''); + const [showContractModal, setShowContractModal] = useState(false); + const [contractAddress, setContractAddress] = useState(''); + const [copyMessage, setCopyMessage] = useState(''); + + // Add the missing state declaration const [showDeleteModal, setShowDeleteModal] = useState(false); - // State for copying transaction ID - const [isIdCopied, setIsIdCopied] = useState(false); + // State for copying Contract Address + const [isAddressCopied, setIsAddressCopied] = useState(false); const defaultOptions = { loop: true, @@ -101,6 +113,14 @@ function Dashboard() { } }; + // Function to generate owner's address + const generateOwnerAddress = (publicKey) => { + const decodedPublicKey = Base58.decode(publicKey); + const addressHash = keccak256(Buffer.from(decodedPublicKey)); + const address = `0x${addressHash.slice(-40)}`; + return address; + }; + // Function to get full hostname from URL function getBaseDomain(url) { try { @@ -181,6 +201,7 @@ function Dashboard() { setNets(storedNets); }, []); + // Update useEffect to set default URLs based on serviceMode useEffect(() => { // Fetch active net URL from storage chrome.storage.local.get(['activeNetUrl'], (result) => { @@ -188,9 +209,9 @@ function Dashboard() { setCompleteUrl(result.activeNetUrl); // Use the full URL with protocol // Check if it's one of the known networks - if (result.activeNetUrl === 'https://cloud.resilientdb.com/graphql') { + if (result.activeNetUrl === 'https://cloud.resilientdb.com/graphql' || result.activeNetUrl === 'https://contract.resilientdb.com/graphql') { setSelectedNet('ResilientDB Mainnet'); - } else if (result.activeNetUrl === 'http://localhost:8000/graphql') { + } else if (result.activeNetUrl === 'http://localhost:8000/graphql' || result.activeNetUrl === 'http://localhost:8400/graphql') { setSelectedNet('ResilientDB Localnet'); } else { // Custom URL case @@ -205,11 +226,12 @@ function Dashboard() { } } else { // No active net URL, default to ResilientDB Mainnet - setCompleteUrl('https://cloud.resilientdb.com/graphql'); + const defaultUrl = serviceMode === 'KV' ? 'https://cloud.resilientdb.com/graphql' : 'https://contract.resilientdb.com/graphql'; + setCompleteUrl(defaultUrl); setSelectedNet('ResilientDB Mainnet'); // Ensure default network is selected } }); - }, [nets]); + }, [nets, serviceMode]); useEffect(() => { if (domain && selectedNet) { @@ -289,6 +311,7 @@ function Dashboard() { } }; + // Update the switchNetwork function to use URLs based on serviceMode const switchNetwork = (value) => { if (value === 'Manage Nets') { setShowModal(true); @@ -296,10 +319,10 @@ function Dashboard() { let newCompleteUrl = ''; switch (value) { case 'ResilientDB Mainnet': - newCompleteUrl = 'https://cloud.resilientdb.com/graphql'; + newCompleteUrl = serviceMode === 'KV' ? 'https://cloud.resilientdb.com/graphql' : 'https://contract.resilientdb.com/graphql'; break; case 'ResilientDB Localnet': - newCompleteUrl = 'http://localhost:8000/graphql'; + newCompleteUrl = serviceMode === 'KV' ? 'http://localhost:8000/graphql' : 'http://localhost:8400/graphql'; break; case 'Custom URL': if (customUrl) { @@ -376,11 +399,11 @@ function Dashboard() { } }; - // Function to copy public key - const handleCopyPublicKey = () => { + // Function to copy owner address + const handleCopyOwnerAddress = () => { try { const tempInput = document.createElement('input'); - tempInput.value = publicKey; + tempInput.value = ownerAddress; document.body.appendChild(tempInput); tempInput.select(); document.execCommand('copy'); @@ -395,9 +418,10 @@ function Dashboard() { } }; - // Function to download key pair as JSON + // Function to download key pair including owner address const handleDownloadKeyPair = () => { const keyPair = { + ownerAddress: ownerAddress, publicKey: publicKey, privateKey: privateKey, }; @@ -410,12 +434,16 @@ function Dashboard() { downloadAnchorNode.remove(); }; - // Function to download all key pairs as JSON + // Function to download all key pairs including owner addresses const handleDownloadAllKeyPairs = () => { - const allKeyPairs = keyPairs.map(({ publicKey, privateKey }) => ({ - publicKey, - privateKey, - })); + const allKeyPairs = keyPairs.map(({ publicKey, privateKey }) => { + const ownerAddress = generateOwnerAddress(publicKey); + return { + ownerAddress, + publicKey, + privateKey, + }; + }); const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(allKeyPairs)); const downloadAnchorNode = document.createElement('a'); downloadAnchorNode.setAttribute("href", dataStr); @@ -425,115 +453,148 @@ function Dashboard() { downloadAnchorNode.remove(); }; - const handleFileUpload = (e) => { + // New Handlers for Deployment + const handleSolidityFileUpload = (e) => { const file = e.target.files[0]; - if (file && file.type === 'application/json') { - setJsonFileName(file.name); // Show file name once uploaded - - const reader = new FileReader(); - reader.onload = (event) => { - try { - const json = JSON.parse(event.target.result); - // Validate JSON data - if (json.asset && json.recipientAddress && json.amount) { - setTransactionData(json); - setTransactionError(''); // Clear any previous error - } 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); + if (file && file.name.endsWith('.sol')) { + const reader = new FileReader(); + reader.onload = (event) => { + setSolidityContent(event.target.result); + setSolidityFileName(file.name); // Set after reading + setDeployError(''); // Reset error message + }; + reader.readAsText(file); } else { - 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.' }); + setSolidityFileName(''); + setSolidityContent(''); + setDeployError('Please upload a valid Solidity (.sol) file.'); + if (solidityFileInputRef.current) { + solidityFileInputRef.current.value = ''; + } } }; - // Function to handle file upload for key pairs - const handleKeyPairFileUpload = (e) => { + const handleDeployJsonFileUpload = (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 + const json = JSON.parse(event.target.result); + // Validate JSON data + if (typeof json.arguments === 'string' && typeof json.contract_name === 'string') { + setDeployJsonContent(json); + setDeployError(''); + setDeployJsonFileName(file.name); // Set file name after validation } else { - console.error('Invalid JSON format for key pairs.'); - setAlert({ isOpen: true, message: 'Invalid JSON format for key pairs.' }); + setDeployJsonContent(null); + setDeployError('Invalid JSON: Missing "arguments" and "contract_name"'); + if (deployJsonFileInputRef.current) { + deployJsonFileInputRef.current.value = ''; + } } } catch (err) { console.error('Error parsing JSON:', err); - setAlert({ isOpen: true, message: 'Error parsing JSON file.' }); + setDeployJsonContent(null); + setDeployError('Invalid JSON format.'); + if (deployJsonFileInputRef.current) { + deployJsonFileInputRef.current.value = ''; + } } }; reader.readAsText(file); } else { - console.error('Please upload a valid JSON file.'); - setAlert({ isOpen: true, message: 'Please upload a valid JSON file.' }); + setDeployJsonFileName(''); + setDeployJsonContent(null); + setDeployError('Please upload a valid JSON file.'); + if (deployJsonFileInputRef.current) { + deployJsonFileInputRef.current.value = ''; + } } }; - const handleDragEnter = (e) => { + // New Handlers for Deployment Drag and Drop + const handleDeployDragEnter = (e) => { e.preventDefault(); e.stopPropagation(); }; - const handleDragOver = (e) => { + const handleDeployDragOver = (e) => { e.preventDefault(); e.stopPropagation(); }; - const handleDrop = (e) => { + const handleDeployDrop = (e) => { e.preventDefault(); e.stopPropagation(); const file = e.dataTransfer.files[0]; if (file && file.type === 'application/json') { - setJsonFileName(file.name); - - const reader = new FileReader(); - reader.onload = (event) => { - try { - const json = JSON.parse(event.target.result); - // Validate JSON data - if (json.asset && json.recipientAddress && json.amount) { - setTransactionData(json); - setTransactionError(''); // Clear any previous error - } 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.' }); + const reader = new FileReader(); + reader.onload = (event) => { + try { + const json = JSON.parse(event.target.result); + // Validate JSON data + if (typeof json.arguments === 'string' && typeof json.contract_name === 'string') { + setDeployJsonContent(json); + setDeployError(''); + setDeployJsonFileName(file.name); // Set file name after validation + } else { + setDeployJsonContent(null); + setDeployError('Invalid JSON format: "arguments" and "contract_name" are required.'); + if (deployJsonFileInputRef.current) { + deployJsonFileInputRef.current.value = ''; + } + } + } catch (err) { + console.error('Error parsing JSON:', err); + setDeployJsonContent(null); + setDeployError('Invalid JSON format.'); + if (deployJsonFileInputRef.current) { + deployJsonFileInputRef.current.value = ''; + } + } + }; + reader.readAsText(file); + } else { + setDeployJsonFileName(''); + setDeployJsonContent(null); + setDeployError('Please upload a valid JSON file.'); + if (deployJsonFileInputRef.current) { + deployJsonFileInputRef.current.value = ''; } - }; - reader.readAsText(file); + } + }; + + // Handler functions for Solidity Drag and Drop + const handleSolidityDragEnter = (e) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleSolidityDragOver = (e) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleSolidityDrop = (e) => { + e.preventDefault(); + e.stopPropagation(); + const file = e.dataTransfer.files[0]; + if (file && file.name.endsWith('.sol')) { + const reader = new FileReader(); + reader.onload = (event) => { + setSolidityContent(event.target.result); + setSolidityFileName(file.name); // Set after reading + setDeployError(''); // Reset error message + }; + reader.readAsText(file); } else { - setJsonFileName(''); - setTransactionData(null); - setTransactionError('Please upload a JSON file.'); - setAlert({ isOpen: true, message: 'Please upload a valid JSON file.' }); + setSolidityFileName(''); + setSolidityContent(''); + setDeployError('Please upload a valid Solidity (.sol) file.'); + if (solidityFileInputRef.current) { + solidityFileInputRef.current.value = ''; + } } }; @@ -545,58 +606,73 @@ function Dashboard() { keyPairFileInputRef.current.click(); }; - const handleSubmit = () => { - if (!transactionData) { - setTransactionError('No valid transaction data found.'); - setAlert({ isOpen: true, message: 'No valid transaction data found.' }); - return; + // New Handlers for Deployment + const handleDeployFileClick = () => { + deployJsonFileInputRef.current.click(); + }; + + const deployJsonFileInputRef = useRef(null); + const solidityFileInputRef = useRef(null); + + // New Handler for Deployment + const handleDeploy = () => { + // Validate that both Solidity and JSON files are uploaded + if (!solidityContent || !deployJsonContent) { + setDeployError('Both Solidity contract and JSON configuration files are required.'); + return; + } + + // Ensure JSON has required fields + const { arguments: args, contract_name } = deployJsonContent; + if (!args || !contract_name) { + setDeployError('JSON file must contain "arguments" and "contract_name".'); + 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; + setDeployError('Please connect to a net before deploying a contract.'); + return; } - // Send transaction data to background script + // Send deployment data to background script chrome.runtime.sendMessage({ - action: 'submitTransactionFromDashboard', - transactionData: transactionData, - domain: domain, - net: selectedNet, + action: 'deployContractChain', + soliditySource: solidityContent, + deployConfig: { + arguments: args, + contract_name: contract_name + }, + ownerAddress: ownerAddress, + domain: domain, + net: selectedNet }, (response) => { - if (response.success) { - setSuccessResponse(response.data); - setShowSuccessModal(true); - setTransactionError(''); - setJsonFileName(''); // Clear the file name after successful submission - setTransactionData(null); - } else { - setTransactionError(response.error || 'Transaction submission failed.'); - setAlert({ isOpen: true, message: response.error || 'Transaction submission failed.' }); - } + if (response.success) { + if (response.contractAddress) { + setContractAddress(response.contractAddress); + setShowContractModal(true); + // Clear the uploaded files + setSolidityFileName(''); + setSolidityContent(''); + setDeployJsonFileName(''); + setDeployJsonContent(null); + setDeployError(''); + + // Reset file input values + if (solidityFileInputRef.current) { + solidityFileInputRef.current.value = ''; + } + if (deployJsonFileInputRef.current) { + deployJsonFileInputRef.current.value = ''; + } + } else { + setDeployError('Deployment succeeded but no contract address returned.'); + } + } else { + setDeployError(response.error || 'Contract deployment failed.'); + } }); }; - // Function to handle transaction ID click - const handleIdClick = () => { - try { - const transactionId = (successResponse && successResponse.postTransaction && successResponse.postTransaction.id) || ''; - const tempInput = document.createElement('input'); - tempInput.value = transactionId; - document.body.appendChild(tempInput); - tempInput.select(); - document.execCommand('copy'); - document.body.removeChild(tempInput); - setIsIdCopied(true); - setTimeout(() => { - setIsIdCopied(false); - }, 1500); - } catch (err) { - setAlert({ isOpen: true, message: 'Unable to copy transaction ID.' }); - console.error('Unable to copy text: ', err); - } - }; - // Function to handle favicon load error const handleFaviconError = () => { setFaviconUrl(''); // This will trigger the globe icon to display @@ -634,6 +710,54 @@ function Dashboard() { setAlert({ isOpen: false, message: '' }); }; + // Handler function for uploading 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 json = JSON.parse(event.target.result); + // Assume json is an array of keyPairs or a single keyPair + if (Array.isArray(json)) { + appendKeyPairs(json); + setAlert({ isOpen: true, message: 'Key pairs uploaded successfully.' }); + } else if (json.publicKey && json.privateKey) { + appendKeyPairs([json]); + setAlert({ isOpen: true, message: 'Key pair uploaded successfully.' }); + } else { + setAlert({ isOpen: true, message: 'Invalid JSON format for key pair.' }); + } + } catch (err) { + console.error('Error parsing key pair JSON:', err); + setAlert({ isOpen: true, message: 'Invalid JSON format for key pair.' }); + } + }; + reader.readAsText(file); + } else { + setAlert({ isOpen: true, message: 'Please upload a valid JSON file.' }); + } + }; + + const handleAddressClick = () => { + try { + const address = contractAddress; + const tempInput = document.createElement('input'); + tempInput.value = address; + document.body.appendChild(tempInput); + tempInput.select(); + document.execCommand('copy'); + document.body.removeChild(tempInput); + setIsAddressCopied(true); + setTimeout(() => { + setIsAddressCopied(false); + }, 1500); + } catch (err) { + setDeployError('Unable to copy contract address.'); + console.error('Unable to copy text: ', err); + } + }; + return ( <> <div className="lottie-background"> @@ -646,7 +770,9 @@ function Dashboard() { Res<strong>Vault</strong> </div> <div className="badge-container"> - <span className="badge">KV Service</span> + <span className="badge" onClick={toggleServiceMode}> + {serviceMode === 'Contract' ? 'Smart Contract' : 'Key-Value'} + </span> </div> <div className="header__icon open-panel"> <button @@ -716,40 +842,6 @@ function Dashboard() { </div> )} - {showSuccessModal && ( - <div className="overlay"> - <div className="modal"> - <div className="modal-content"> - <h2>Transaction Submitted Successfully!</h2> - {/* Extract transaction ID */} - {successResponse && successResponse.postTransaction && successResponse.postTransaction.id ? ( - <div className="fieldset"> - <div className="radio-option radio-option--full"> - <input - type="radio" - name="transactionId" - id="txId" - value={successResponse.postTransaction.id} - checked - readOnly - onClick={handleIdClick} - /> - <label htmlFor="txId"> - <span>{isIdCopied ? 'Copied' : `${successResponse.postTransaction.id.slice(0, 5)}...${successResponse.postTransaction.id.slice(-5)}`}</span> - </label> - </div> - </div> - ) : ( - <p>No transaction ID found.</p> - )} - <button onClick={() => setShowSuccessModal(false)} className="button-close" title="Close Modal"> - Close - </button> - </div> - </div> - </div> - )} - {/* Alert Modal */} {alert.isOpen && ( <div className="overlay"> @@ -767,6 +859,37 @@ function Dashboard() { </div> )} + {/* Contract Address Modal */} + {showContractModal && ( + <div className="overlay"> + <div className="modal"> + <div className="modal-content"> + <h2>Contract Deployed Successfully!</h2> + <p>Contract Address:</p> + <div className="fieldset"> + <div className="radio-option radio-option--full"> + <input + type="radio" + name="contractAddress" + id="contractAddress" + value={contractAddress} + checked + readOnly + onClick={handleAddressClick} + /> + <label htmlFor="contractAddress"> + <span>{isAddressCopied ? 'Copied' : `${contractAddress.slice(0, 5)}...${contractAddress.slice(-5)}`}</span> + </label> + </div> + </div> + <button onClick={() => setShowContractModal(false)} className="button-close" title="Close Modal"> + Close + </button> + </div> + </div> + </div> + )} + <div className="page__content page__content--with-header page__content--with-bottom-nav"> <h2 className="page__title">Dashboard</h2> @@ -809,28 +932,58 @@ function Dashboard() { )} </div> - <div className="file-upload"> - <div - className={`drag_box_outline ${jsonFileName ? 'file-uploaded' : ''}`} - onDragEnter={handleDragEnter} - onDragOver={handleDragOver} - onDrop={handleDrop} - onClick={handleFileClick} - > - <input - type="file" - ref={fileInputRef} - style={{ display: 'none' }} - accept="application/json" - onChange={handleFileUpload} - /> - {jsonFileName ? ( - <span className="filename">{jsonFileName} uploaded</span> - ) : ( - <span className="filename">Click to Upload JSON File</span> - )} + {/* Deploy Contract Section */} + <div className="deploy-section"> + <div className="file-upload-row"> + {/* Solidity Contract Upload */} + <div className="file-upload"> + <div + className={`drag_box_outline ${solidityFileName ? 'file-uploaded' : ''}`} + onDragEnter={handleSolidityDragEnter} + onDragOver={handleSolidityDragOver} + onDrop={handleSolidityDrop} + onClick={() => solidityFileInputRef.current.click()} + > + <input + type="file" + ref={solidityFileInputRef} + style={{ display: 'none' }} + accept=".sol" + onChange={handleSolidityFileUpload} + /> + {solidityFileName ? ( + <span className="filename">{solidityFileName} uploaded</span> + ) : ( + <span className="filename">Contract (.sol)</span> + )} + </div> + </div> + + {/* JSON Configuration Upload */} + <div className="file-upload"> + <div + className={`drag_box_outline ${deployJsonFileName ? 'file-uploaded' : ''}`} + onDragEnter={handleDeployDragEnter} + onDragOver={handleDeployDragOver} + onDrop={handleDeployDrop} + onClick={() => deployJsonFileInputRef.current.click()} + > + <input + type="file" + ref={deployJsonFileInputRef} + style={{ display: 'none' }} + accept="application/json" + onChange={handleDeployJsonFileUpload} + /> + {deployJsonFileName ? ( + <span className="filename">{deployJsonFileName} uploaded</span> + ) : ( + <span className="filename">Configuration (.json)</span> + )} + </div> + </div> </div> - {transactionError && <p className="error-message">{transactionError}</p>} + {deployError && <p className="error-message">{deployError}</p>} </div> </div> @@ -845,11 +998,14 @@ function Dashboard() { onChange={(e) => switchKeyPair(Number(e.target.value))} className="select" > - {keyPairs.map((keyPair, index) => ( + {keyPairs.map((keyPair, index) => { + const ownerAddr = generateOwnerAddress(keyPair.publicKey); + return ( <option key={index} value={index}> - {`${keyPair.publicKey.slice(0, 4)}...${keyPair.publicKey.slice(-4)}`} + {`${ownerAddr.slice(0, 4)}...${ownerAddr.slice(-4)}`} </option> - ))} + ); + })} </select> <i className="fas fa-chevron-down"></i> </div> @@ -859,7 +1015,7 @@ function Dashboard() { <DeleteIcon style={{ color: 'white' }} /> </button> )} - <button onClick={handleCopyPublicKey} className="icon-button" title="Copy Public Key"> + <button onClick={handleCopyOwnerAddress} className="icon-button" title="Copy Owner Address"> <ContentCopyIcon style={{ color: isCopied ? 'grey' : 'white' }} /> </button> <button onClick={handleDownloadKeyPair} className="icon-button" title="Download Key Pair"> @@ -916,9 +1072,16 @@ function Dashboard() { </div> )} - <button className="button button--full button--main open-popup" onClick={handleSubmit} title="Submit Transaction"> - Submit + {/* Deploy Button */} + <button + className="button button--full button--main open-popup" + onClick={handleDeploy} + title="Deploy Contract" + disabled={!solidityContent || !deployJsonContent || !isConnected} + > + Deploy </button> + <p className="bottom-navigation" style={{ backgroundColor: 'transparent', display: 'flex', justifyContent: 'center', textShadow: '1px 1px 1px rgba(0, 0, 0, 0.3)', color: 'rgb(255, 255, 255, 0.5)', fontSize: '9px' }}> ResVault v{versionData.version} </p> @@ -929,4 +1092,4 @@ function Dashboard() { } -export default Dashboard; \ No newline at end of file +export default Contract; \ No newline at end of file diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index bb28209..adfedcb 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -51,6 +51,8 @@ function Dashboard() { appendKeyPairs, alert, // For alert modal setAlert, // For alert modal + serviceMode, + toggleServiceMode, } = useContext(GlobalContext); const [tabId, setTabId] = useState(null); @@ -181,6 +183,7 @@ function Dashboard() { setNets(storedNets); }, []); + // Update useEffect to set default URLs based on serviceMode useEffect(() => { // Fetch active net URL from storage chrome.storage.local.get(['activeNetUrl'], (result) => { @@ -188,9 +191,9 @@ function Dashboard() { setCompleteUrl(result.activeNetUrl); // Use the full URL with protocol // Check if it's one of the known networks - if (result.activeNetUrl === 'https://cloud.resilientdb.com/graphql') { + if (result.activeNetUrl === 'https://cloud.resilientdb.com/graphql' || result.activeNetUrl === 'https://contract.resilientdb.com/graphql') { setSelectedNet('ResilientDB Mainnet'); - } else if (result.activeNetUrl === 'http://localhost:8000/graphql') { + } else if (result.activeNetUrl === 'http://localhost:8000/graphql' || result.activeNetUrl === 'http://localhost:8400/graphql') { setSelectedNet('ResilientDB Localnet'); } else { // Custom URL case @@ -205,11 +208,12 @@ function Dashboard() { } } else { // No active net URL, default to ResilientDB Mainnet - setCompleteUrl('https://cloud.resilientdb.com/graphql'); + const defaultUrl = serviceMode === 'KV' ? 'https://cloud.resilientdb.com/graphql' : 'https://contract.resilientdb.com/graphql'; + setCompleteUrl(defaultUrl); setSelectedNet('ResilientDB Mainnet'); // Ensure default network is selected } }); - }, [nets]); + }, [nets, serviceMode]); useEffect(() => { if (domain && selectedNet) { @@ -289,6 +293,7 @@ function Dashboard() { } }; + // Update the switchNetwork function to use URLs based on serviceMode const switchNetwork = (value) => { if (value === 'Manage Nets') { setShowModal(true); @@ -296,10 +301,10 @@ function Dashboard() { let newCompleteUrl = ''; switch (value) { case 'ResilientDB Mainnet': - newCompleteUrl = 'https://cloud.resilientdb.com/graphql'; + newCompleteUrl = serviceMode === 'KV' ? 'https://cloud.resilientdb.com/graphql' : 'https://contract.resilientdb.com/graphql'; break; case 'ResilientDB Localnet': - newCompleteUrl = 'http://localhost:8000/graphql'; + newCompleteUrl = serviceMode === 'KV' ? 'http://localhost:8000/graphql' : 'http://localhost:8400/graphql'; break; case 'Custom URL': if (customUrl) { @@ -646,7 +651,9 @@ function Dashboard() { Res<strong>Vault</strong> </div> <div className="badge-container"> - <span className="badge">KV Service</span> + <span className="badge" onClick={toggleServiceMode}> + {serviceMode === 'KV' ? 'Key-Value' : 'Smart Contract'} + </span> </div> <div className="header__icon open-panel"> <button