@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.
233 lines (232 loc) • 10.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createContractsRoutes = createContractsRoutes;
const express_1 = require("express");
function createContractsRoutes(hre) {
const router = (0, express_1.Router)();
/**
* GET /api/contracts
* Returns list of all contract artifacts with their ABIs
*/
router.get("/api/contracts", async (req, res) => {
try {
const contractNames = await hre.artifacts.getAllFullyQualifiedNames();
const contracts = await Promise.all(contractNames.map(async (name) => {
const artifact = await hre.artifacts.readArtifact(name);
if (artifact.sourceName.endsWith('.dbg.json'))
return null;
return {
contractName: artifact.contractName,
sourceName: artifact.sourceName,
abi: artifact.abi,
};
}));
res.json(contracts.filter(c => c !== null));
}
catch (error) {
console.error("Error getting contracts:", error);
res.status(500).json({ error: "Failed to read contract artifacts." });
}
});
/**
* GET /api/contracts/:network/:contractName/functions
* Returns available functions for a specific contract
*/
router.get("/api/contracts/:network/:contractName/functions", async (req, res) => {
const { network, contractName } = req.params;
try {
hre.network.name = network;
const deployment = await hre.adminUI.getDeployment(contractName);
if (!deployment) {
return res.status(404).json({ error: `Contract ${contractName} not found` });
}
const contract = new hre.ethers.Contract(deployment.address, deployment.abi);
const functions = contract.interface.fragments
.filter((f) => f.type === 'function')
.map((f) => ({
name: f.name,
inputs: f.inputs.map((input) => ({
name: input.name,
type: input.type,
internalType: input.internalType || input.type
})),
outputs: f.outputs?.map((output) => ({
name: output.name,
type: output.type,
internalType: output.internalType || output.type
})) || [],
stateMutability: f.stateMutability || 'nonpayable',
payable: f.payable || false
}));
res.json(functions);
}
catch (error) {
console.error("Error getting contract functions:", error);
res.status(500).json({ error: "Failed to get contract functions." });
}
});
/**
* POST /api/contracts/:network/:contractName/call
* Calls a contract method with specified parameters
*/
router.post("/api/contracts/:network/:contractName/call", async (req, res) => {
const { network, contractName } = req.params;
const { method, args = [], value = "0" } = req.body;
try {
hre.network.name = network;
const deployment = await hre.adminUI.getDeployment(contractName);
if (!deployment) {
return res.status(404).json({ error: `Contract ${contractName} not found` });
}
const signers = await hre.ethers.getSigners();
const signer = signers[0]; // Use first available signer
const contract = new hre.ethers.Contract(deployment.address, deployment.abi, signer);
console.log(`📞 Calling ${contractName}.${method}(${args.join(', ')}) with value: ${value}`);
const callOptions = value !== "0" ? { value: hre.ethers.utils.parseEther(value) } : {};
const result = await contract[method](...args, callOptions);
// Handle different return types
let response;
if (result && typeof result.wait === 'function') {
// It's a transaction
const receipt = await result.wait();
response = {
type: 'transaction',
hash: result.hash,
gasUsed: receipt.gasUsed.toString(),
blockNumber: receipt.blockNumber,
status: receipt.status,
logs: receipt.logs.map((log) => ({
address: log.address,
topics: log.topics,
data: log.data
}))
};
}
else {
// It's a view function result
response = {
type: 'view',
result: Array.isArray(result) ? result.map(r => r.toString()) : result.toString()
};
}
res.json(response);
}
catch (error) {
console.error(`❌ Contract call failed:`, error);
const errorMessage = error?.reason || error?.message || 'Unknown error';
res.status(500).json({
error: errorMessage,
code: error?.code || 'UNKNOWN'
});
}
});
/**
* GET /api/contracts/:network/:contractName/events
* Returns event logs for a specific contract
*/
router.get("/api/contracts/:network/:contractName/events", async (req, res) => {
const { network, contractName } = req.params;
const { fromBlock = 0, toBlock = 'latest', limit = 100 } = req.query;
try {
hre.network.name = network;
const deployment = await hre.adminUI.getDeployment(contractName);
if (!deployment) {
return res.status(404).json({ error: `Contract ${contractName} not found` });
}
const provider = hre.ethers.provider;
const contract = new hre.ethers.Contract(deployment.address, deployment.abi, provider);
// Get deployment block to optimize query range
const deploymentBlock = deployment.receipt?.blockNumber || 0;
const actualFromBlock = Math.max(Number(fromBlock), deploymentBlock);
// Query all events for the contract
const filter = {
address: deployment.address,
fromBlock: actualFromBlock,
toBlock: toBlock === 'latest' ? 'latest' : Number(toBlock)
};
const logs = await provider.getLogs(filter);
// Parse and format the logs
const parsedEvents = logs.slice(0, Number(limit)).map((log, index) => {
try {
// Try to parse the log using the contract interface
const parsedLog = contract.interface.parseLog(log);
return {
id: `${log.transactionHash}-${log.logIndex}`,
eventName: parsedLog.name,
signature: parsedLog.signature,
args: Object.keys(parsedLog.args).reduce((acc, key) => {
if (isNaN(Number(key))) { // Skip numeric indices
const value = parsedLog.args[key];
acc[key] = typeof value === 'object' && value._isBigNumber ? value.toString() : value;
}
return acc;
}, {}),
blockNumber: log.blockNumber,
blockHash: log.blockHash,
transactionHash: log.transactionHash,
transactionIndex: log.transactionIndex,
logIndex: log.logIndex,
removed: log.removed || false,
address: log.address,
topics: log.topics,
data: log.data
};
}
catch (parseError) {
// If parsing fails, return raw log data
return {
id: `${log.transactionHash}-${log.logIndex}`,
eventName: 'Unknown',
signature: null,
args: {},
blockNumber: log.blockNumber,
blockHash: log.blockHash,
transactionHash: log.transactionHash,
transactionIndex: log.transactionIndex,
logIndex: log.logIndex,
removed: log.removed || false,
address: log.address,
topics: log.topics,
data: log.data,
parseError: 'Failed to parse event'
};
}
});
// Get timestamps for blocks if needed
const eventLogsWithTimestamps = await Promise.all(parsedEvents.map(async (event) => {
try {
const block = await provider.getBlock(event.blockNumber);
return {
...event,
timestamp: block.timestamp,
blockTimestamp: new Date(block.timestamp * 1000).toISOString()
};
}
catch (error) {
return {
...event,
timestamp: null,
blockTimestamp: null
};
}
}));
res.json({
contractAddress: deployment.address,
contractName,
network,
fromBlock: actualFromBlock,
toBlock,
totalEvents: eventLogsWithTimestamps.length,
events: eventLogsWithTimestamps
});
}
catch (error) {
console.error("Error getting contract events:", error);
res.status(500).json({
error: "Failed to get contract events.",
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
return router;
}