UNPKG

@nomyx/hardhat-adminui

Version:

A comprehensive Hardhat plugin providing a web-based admin UI for deployed smart contracts with Diamond proxy support, contract interaction, event monitoring, and deployment dashboard.

379 lines (378 loc) 18.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); require("hardhat/types/runtime"); const config_1 = require("hardhat/config"); const ethers_1 = require("ethers"); const ws_1 = require("ws"); const chokidar_1 = __importDefault(require("chokidar")); const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const functionSignatureDecoder_1 = require("./services/functionSignatureDecoder"); const port_utils_1 = require("./utils/port-utils"); (0, config_1.extendEnvironment)((hre) => { // Initialize WebSocket server asynchronously to avoid blocking environment setup let wss; const initWebSocketServer = async () => { try { const wsPort = await (0, port_utils_1.findAvailablePort)(8080); if (wsPort !== 8080) { console.log(`🔄 WebSocket port 8080 in use, using port ${wsPort} instead`); } wss = new ws_1.WebSocketServer({ port: wsPort }); } catch (error) { console.warn(`⚠️ Could not find available WebSocket port, using default 8080`); wss = new ws_1.WebSocketServer({ port: 8080 }); } setupWebSocketHandlers(); }; const setupWebSocketHandlers = () => { if (!wss) return; wss.on('connection', ws => { console.log('Admin UI client connected'); ws.on('message', async (message) => { try { const { type, data } = JSON.parse(message.toString()); if (type === 'verifyContract') { await hre.adminUI.verifyContract(data.contractName); } else if (type === 'simulateTransaction') { const result = await hre.adminUI.simulateTransaction(data.from, data.to, data.data, data.value); ws.send(JSON.stringify({ type: 'simulationResult', data: result })); } } catch (e) { console.error('Error processing message from client:', e); } }); ws.on('close', () => console.log('Admin UI client disconnected')); }); }; const broadcast = (message) => { if (wss && wss.clients) { wss.clients.forEach((client) => { if (client.readyState === ws_1.WebSocket.OPEN) { client.send(JSON.stringify(message)); } }); } }; // Initialize WebSocket server asynchronously initWebSocketServer().catch(error => { console.error('Failed to initialize WebSocket server:', error); }); const deploymentsPath = hre.config.paths.deployments; let deploymentTimeout; const watcher = chokidar_1.default.watch(deploymentsPath, { persistent: true, ignoreInitial: true, }); console.log(`Watching for deployments in: ${deploymentsPath}`); watcher.on('add', async (filePath) => { clearTimeout(deploymentTimeout); if (path_1.default.extname(filePath) === '.json' && !filePath.includes('.dbg.json')) { try { const content = await promises_1.default.readFile(filePath, 'utf-8'); const artifact = JSON.parse(content); // Extract contract name from file path if not present in artifact const fileName = path_1.default.basename(filePath, '.json'); const contractName = artifact.contractName || fileName; // Ensure artifact has contract name for frontend processing const enrichedArtifact = { ...artifact, contractName, deployedAt: new Date().toISOString() }; console.log(`📦 New deployment detected: ${contractName} at ${enrichedArtifact.address || 'unknown'}`); broadcast({ type: 'artifact', data: enrichedArtifact }); } catch (error) { console.error(`❌ Error processing artifact ${filePath}:`, error); } } deploymentTimeout = setTimeout(async () => { console.log('🎉 Deployment sequence complete. Broadcasting final state...'); try { const deployments = await hre.adminUI.listDeployments(); console.log(`📊 Broadcasting ${deployments.length} deployments to UI`); broadcast({ type: 'deploymentComplete', data: deployments }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error('❌ Error getting final deployments:', error); broadcast({ type: 'deploymentError', data: { error: errorMessage } }); } }, 2000); }); hre.adminUI = { getProjectInfo: () => ({ name: hre.config.solidity.compilers[0].version, hardhatVersion: hre.version, networks: Object.keys(hre.config.networks), }), getDeployment: async (contractName) => { try { const deployment = await hre.deployments.get(contractName); return { ...deployment, contractName }; } catch (error) { return null; } }, listDeployments: async (network) => { const all = await hre.deployments.all(); return Object.entries(all).map(([contractName, deployment]) => ({ ...deployment, contractName, })); }, getContract: async (contractName, signer) => { const deployment = await hre.deployments.get(contractName); if (!deployment) return null; return new ethers_1.ethers.Contract(deployment.address, deployment.abi, signer); }, getStorageAt: async (address, slot) => { return hre.ethers.provider.getStorageAt(address, slot); }, verifyContract: async (contractName) => { const deployment = await hre.deployments.get(contractName); if (!deployment) { throw new Error(`Deployment not found for ${contractName}`); } broadcast({ type: 'verificationStatus', data: { contractName, status: 'Verifying...' } }); try { await hre.run("verify:verify", { address: deployment.address, constructorArguments: deployment.args, }); broadcast({ type: 'verificationStatus', data: { contractName, status: 'Successfully verified' } }); } catch (e) { broadcast({ type: 'verificationStatus', data: { contractName, status: 'Verification failed', error: e.message } }); throw e; } }, simulateTransaction: async (from, to, data, value) => { await hre.network.provider.request({ method: "hardhat_impersonateAccount", params: [from], }); const signer = await hre.ethers.getSigner(from); try { const result = await signer.call({ to: to, data: data, value: value, }); return { success: true, result }; } catch (error) { return { success: false, error: error.message }; } finally { await hre.network.provider.request({ method: "hardhat_stopImpersonatingAccount", params: [from], }); } }, getRecentTransactions: async (contractAddress, limit = 10) => { try { const provider = hre.ethers.provider; const currentBlock = await provider.getBlockNumber(); const fromBlock = Math.max(0, currentBlock - 1000); // Look back 1000 blocks // Get recent transactions by filtering for transactions to this contract const filter = { address: contractAddress, fromBlock, toBlock: currentBlock }; const logs = await provider.getLogs(filter); const transactions = []; // Get unique transaction hashes from logs const txHashes = [...new Set(logs.map(log => log.transactionHash))]; // Fetch transaction details for each unique hash for (const hash of txHashes.slice(0, limit)) { try { const tx = await provider.getTransaction(hash); const receipt = await provider.getTransactionReceipt(hash); if (tx && receipt) { transactions.push({ hash: tx.hash, from: tx.from, to: tx.to, blockNumber: tx.blockNumber, method: 'Unknown', // We could decode this from tx.data if needed status: receipt.status === 1 ? 'Success' : 'Failed', gasUsed: receipt.gasUsed.toString(), timestamp: Date.now() // We could get actual timestamp from block if needed }); } } catch (error) { console.error(`Error fetching transaction ${hash}:`, error); } } return transactions.sort((a, b) => (b.blockNumber || 0) - (a.blockNumber || 0)); } catch (error) { console.error('Error fetching recent transactions:', error); return []; } }, getTransactionDetails: async (txHash) => { try { const provider = hre.ethers.provider; // Get transaction and receipt const [transaction, receipt] = await Promise.all([ provider.getTransaction(txHash), provider.getTransactionReceipt(txHash) ]); if (!transaction) { throw new Error(`Transaction ${txHash} not found`); } // Get block information for timestamp const block = await provider.getBlock(transaction.blockNumber); // Calculate transaction fee const gasPrice = transaction.gasPrice || receipt.effectiveGasPrice || ethers_1.ethers.BigNumber.from(0); const transactionFee = receipt.gasUsed.mul(gasPrice); // Decode function call if it's a contract interaction let decodedFunction = null; if (transaction.data && transaction.data !== '0x' && transaction.to) { try { // Try to find the contract ABI to decode the function call const deployments = await hre.deployments.all(); const contractDeployment = Object.values(deployments).find(dep => dep.address.toLowerCase() === transaction.to.toLowerCase()); let contractInterface = null; if (contractDeployment && contractDeployment.abi) { contractInterface = new ethers_1.ethers.utils.Interface(contractDeployment.abi); try { const functionFragment = contractInterface.parseTransaction({ data: transaction.data }); decodedFunction = { name: functionFragment.name, signature: functionFragment.signature, args: functionFragment.args, functionFragment: functionFragment.functionFragment }; } catch (decodeError) { // Function signature not found in ABI, fall through to signature decoder } } // Fallback to signature decoder if ABI decoding failed if (!decodedFunction) { const decoder = new functionSignatureDecoder_1.FunctionSignatureDecoder(hre.ethers.provider); const decoded = await decoder.decodeFunction(transaction.data, contractInterface || undefined); if (decoded && decoded.name !== 'Unknown Function') { decodedFunction = { name: decoded.name, signature: decoded.signature, args: decoded.inputs?.map((input) => input.value) || [], inputs: decoded.inputs }; } } } catch (error) { console.error('Error decoding function call:', error); } } // Decode events from logs const decodedEvents = []; if (receipt.logs && receipt.logs.length > 0) { const decoder = new functionSignatureDecoder_1.FunctionSignatureDecoder(hre.ethers.provider); for (const log of receipt.logs) { try { // Try to find the contract ABI to decode events const deployments = await hre.deployments.all(); const contractDeployment = Object.values(deployments).find(dep => dep.address.toLowerCase() === log.address.toLowerCase()); let contractInterface = null; let decoded = null; if (contractDeployment && contractDeployment.abi) { contractInterface = new ethers_1.ethers.utils.Interface(contractDeployment.abi); try { const decodedLog = contractInterface.parseLog(log); decoded = { name: decodedLog.name, signature: decodedLog.signature, args: decodedLog.args, eventFragment: decodedLog.eventFragment }; } catch (decodeError) { // Event not found in ABI, fall through to signature decoder } } // Fallback to signature decoder if ABI decoding failed if (!decoded) { const decodedEvent = await decoder.decodeEvent(log, contractInterface || undefined); if (decodedEvent && decodedEvent.name !== 'Unknown Event') { decoded = { name: decodedEvent.name, signature: decodedEvent.signature, args: decodedEvent.inputs?.map((input) => input.value) || [], inputs: decodedEvent.inputs }; } } decodedEvents.push({ ...log, decoded }); } catch (error) { console.error('Error decoding event:', error); decodedEvents.push(log); } } } // Get current block number for confirmations const currentBlock = await provider.getBlockNumber(); const confirmations = transaction.blockNumber ? currentBlock - transaction.blockNumber + 1 : 0; return { // Basic transaction info hash: transaction.hash, blockNumber: transaction.blockNumber, blockHash: transaction.blockHash, transactionIndex: receipt.transactionIndex, confirmations, // Addresses and value from: transaction.from, to: transaction.to, value: transaction.value.toString(), // Gas information gasLimit: transaction.gasLimit.toString(), gasUsed: receipt.gasUsed.toString(), gasPrice: gasPrice.toString(), transactionFee: transactionFee.toString(), // Transaction data data: transaction.data, nonce: transaction.nonce, // Status and result status: receipt.status, // Block and timing info timestamp: block.timestamp, // Decoded information decodedFunction, decodedEvents, // Raw objects for advanced use transaction, receipt, block }; } catch (error) { console.error(`Error fetching transaction details for ${txHash}:`, error); throw error; } } }; });