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 <[email protected]>
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