@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.
376 lines (375 loc) • 14 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.createStorageRoutes = createStorageRoutes;
const express_1 = require("express");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
/**
* Format storage value based on type information
*/
/**
* Generate fallback storage layout for known contracts
*/
function generateFallbackStorageLayout(contractName) {
const layouts = {
SimpleStorage: {
storage: [
{
slot: "0",
label: "value",
type: "t_uint256",
astId: 1,
contract: "contracts/SimpleStorage.sol:SimpleStorage",
offset: 0
},
{
slot: "1",
label: "owner",
type: "t_address",
astId: 2,
contract: "contracts/SimpleStorage.sol:SimpleStorage",
offset: 0
}
],
types: {
"t_uint256": {
label: "uint256",
numberOfBytes: "32"
},
"t_address": {
label: "address",
numberOfBytes: "20"
}
}
},
SimpleToken: {
storage: [
{
slot: "0",
label: "_balances",
type: "t_mapping(t_address,t_uint256)",
astId: 3,
contract: "contracts/SimpleToken.sol:SimpleToken",
offset: 0
},
{
slot: "1",
label: "_allowances",
type: "t_mapping(t_address,t_mapping(t_address,t_uint256))",
astId: 4,
contract: "contracts/SimpleToken.sol:SimpleToken",
offset: 0
},
{
slot: "2",
label: "_totalSupply",
type: "t_uint256",
astId: 5,
contract: "contracts/SimpleToken.sol:SimpleToken",
offset: 0
},
{
slot: "3",
label: "_name",
type: "t_string_storage",
astId: 6,
contract: "contracts/SimpleToken.sol:SimpleToken",
offset: 0
},
{
slot: "4",
label: "_symbol",
type: "t_string_storage",
astId: 7,
contract: "contracts/SimpleToken.sol:SimpleToken",
offset: 0
}
],
types: {
"t_uint256": {
label: "uint256",
numberOfBytes: "32"
},
"t_address": {
label: "address",
numberOfBytes: "20"
},
"t_mapping(t_address,t_uint256)": {
label: "mapping(address => uint256)",
numberOfBytes: "32"
},
"t_mapping(t_address,t_mapping(t_address,t_uint256))": {
label: "mapping(address => mapping(address => uint256))",
numberOfBytes: "32"
},
"t_string_storage": {
label: "string",
numberOfBytes: "32"
}
}
},
Marketplace: {
storage: [
{
slot: "0",
label: "nextItemId",
type: "t_uint256",
astId: 8,
contract: "contracts/Marketplace.sol:Marketplace",
offset: 0
},
{
slot: "1",
label: "items",
type: "t_mapping(t_uint256,t_struct(Item)storage)",
astId: 9,
contract: "contracts/Marketplace.sol:Marketplace",
offset: 0
}
],
types: {
"t_uint256": {
label: "uint256",
numberOfBytes: "32"
},
"t_mapping(t_uint256,t_struct(Item)storage)": {
label: "mapping(uint256 => struct Marketplace.Item)",
numberOfBytes: "32"
}
}
}
};
return layouts[contractName] || null;
}
function formatStorageValue(value, type) {
if (!value || value === "0x0000000000000000000000000000000000000000000000000000000000000000") {
return "0";
}
// Convert hex to decimal for common types
if (type.includes("uint") || type.includes("int")) {
try {
const decimal = BigInt(value).toString();
return decimal;
}
catch {
return value;
}
}
// Handle address types
if (type === "address") {
return value.length === 66 ? value : value.slice(0, 42);
}
// Handle bool types
if (type === "bool") {
return value === "0x0000000000000000000000000000000000000000000000000000000000000000" ? "false" : "true";
}
// Handle bytes types
if (type.startsWith("bytes")) {
return value;
}
return value;
}
function createStorageRoutes(hre) {
const router = (0, express_1.Router)();
/**
* GET /api/storage/:network/:contractName/layout
* Returns storage layout information for a specific contract
*/
router.get("/api/storage/:network/:contractName/layout", 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: `Deployment not found for ${contractName} on network ${network}`
});
}
// Get storage layout from deployment or try to read from build artifacts
let storageLayout = deployment.storageLayout;
if (!storageLayout) {
try {
// Try to read from build artifacts directory
const artifactPath = path.join(process.cwd(), "artifacts", "contracts", `${contractName}.sol`, `${contractName}.json`);
if (fs.existsSync(artifactPath)) {
const artifactContent = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
storageLayout = artifactContent.storageLayout;
}
}
catch (error) {
console.warn(`Could not read storage layout from artifacts: ${error}`);
}
}
// Fallback: Generate manual storage layout for known contracts
if (!storageLayout) {
storageLayout = generateFallbackStorageLayout(contractName);
}
if (!storageLayout) {
return res.json({
contractAddress: deployment.address,
contractName,
network,
message: "No storage layout available. Ensure contract was compiled with storage layout enabled.",
layout: null
});
}
// Get current values for storage slots
const populatedStorage = await Promise.all(storageLayout.storage.map(async (item) => {
try {
const value = await hre.adminUI.getStorageAt(deployment.address, item.slot);
return {
...item,
currentValue: value,
hexValue: value,
formattedValue: formatStorageValue(value, item.type)
};
}
catch (error) {
return {
...item,
currentValue: null,
error: `Failed to read slot ${item.slot}`
};
}
}));
res.json({
contractAddress: deployment.address,
contractName,
network,
layout: {
...storageLayout,
storage: populatedStorage
}
});
}
catch (error) {
console.error(`Error getting storage layout for ${contractName} on ${network}:`, error);
res.status(500).json({
error: `Failed to get storage layout for ${contractName} on network ${network}`
});
}
});
/**
* GET /api/storage/:network/:contractAddress
* Returns basic storage information for a specific contract
* Note: For detailed storage layout, use the query endpoint with specific slots
*/
router.get("/api/storage/:network/:contractAddress", async (req, res) => {
const { network, contractAddress } = req.params;
try {
hre.network.name = network;
// Return basic contract info and suggest using specific slot queries
res.json({
contractAddress,
network,
message: "Use /api/storage/:network/:contractAddress/:slot for specific slot queries",
endpoints: {
singleSlot: `/api/storage/${network}/${contractAddress}/:slot`,
multipleSlots: `/api/storage/${network}/${contractAddress}/query`,
layout: `/api/storage/${network}/contractName/layout`
}
});
}
catch (error) {
console.error(`Error processing storage request for ${contractAddress} on ${network}:`, error);
res.status(500).json({
error: `Failed to process storage request for ${contractAddress} on network ${network}`
});
}
});
/**
* GET /api/storage/:network/:contractAddress/:slot
* Returns storage value at a specific slot
*/
router.get("/api/storage/:network/:contractAddress/:slot", async (req, res) => {
const { network, contractAddress, slot } = req.params;
try {
hre.network.name = network;
const value = await hre.adminUI.getStorageAt(contractAddress, slot);
res.json({
slot,
value,
hexValue: value,
contractAddress
});
}
catch (error) {
console.error(`Error getting storage at slot ${slot} for ${contractAddress}:`, error);
res.status(500).json({
error: `Failed to get storage at slot ${slot} for ${contractAddress}`
});
}
});
/**
* POST /api/storage/:network/:contractAddress/query
* Queries multiple storage slots at once
*/
router.post("/api/storage/:network/:contractAddress/query", async (req, res) => {
const { network, contractAddress } = req.params;
const { slots } = req.body;
if (!Array.isArray(slots)) {
return res.status(400).json({ error: "Slots must be an array" });
}
try {
hre.network.name = network;
const results = await Promise.all(slots.map(async (slot) => {
try {
const value = await hre.adminUI.getStorageAt(contractAddress, slot);
return { slot, value, hexValue: value, success: true };
}
catch (error) {
return {
slot,
error: error instanceof Error ? error.message : 'Unknown error',
success: false
};
}
}));
res.json({
contractAddress,
network,
results
});
}
catch (error) {
console.error(`Error querying storage for ${contractAddress} on ${network}:`, error);
res.status(500).json({
error: `Failed to query storage for ${contractAddress} on network ${network}`
});
}
});
return router;
}