Skip to content

Commit fa65258

Browse files
authored
feat: fix log file: storage layout & contract-code-size (#291)
1 parent e6efc36 commit fa65258

File tree

7 files changed

+628
-636
lines changed

7 files changed

+628
-636
lines changed

.husky/pre-commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ set -ex
66
yarn lint-staged
77
yarn clean
88
yarn compile
9-
yarn plugin:storage-layout --override true
9+
yarn plugin:storage-layout
1010
git add logs/storage_layout.log
1111
git add logs/contract_code_sizes.log

hardhat.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import '@nomiclabs/hardhat-ethers';
33
import 'hardhat-deploy';
44
import 'hardhat-gas-reporter';
55
import '@nomicfoundation/hardhat-chai-matchers';
6-
import 'hardhat-contract-sizer';
76
import '@solidstate/hardhat-4byte-uploader';
8-
import 'hardhat-storage-layout';
7+
import '@bahuy3103/hardhat-storage-layout';
8+
import '@bahuy3103/hardhat-contract-sizer';
99

1010
import * as dotenv from 'dotenv';
1111
import { HardhatUserConfig, NetworkUserConfig, SolcUserConfig } from 'hardhat/types';
@@ -131,6 +131,7 @@ const config: HardhatUserConfig = {
131131
outDir: 'src/types',
132132
},
133133
paths: {
134+
newStorageLayoutPath: './logs',
134135
deploy: ['src/deploy', 'src/upgrades'],
135136
tests: 'test/hardhat_test',
136137
},

logs/contract_code_sizes.log

Lines changed: 175 additions & 175 deletions
Large diffs are not rendered by default.

logs/storage_layout.log

Lines changed: 280 additions & 280 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818
"plugin:init-foundry": "hardhat init-foundry",
1919
"plugin:size-contracts": "hardhat size-contracts",
2020
"plugin:upload-selectors": "hardhat upload-selectors",
21-
"plugin:storage-layout": "hardhat check > logs/storage.txt && hardhat generate-storage-layout --source logs/storage.txt",
22-
"plugin:storage-layout-table": "hardhat check > logs/storage.txt && hardhat generate-storage-layout-table --source logs/storage.txt",
21+
"plugin:storage-layout": "hardhat generate-storage-layout --inline --table",
2322
"plugin:sourcify": "hardhat sourcify --endpoint https://sourcify.roninchain.com/server"
2423
},
2524
"lint-staged": {
@@ -31,6 +30,8 @@
3130
"@openzeppelin/contracts": "4.7.3"
3231
},
3332
"devDependencies": {
33+
"@bahuy3103/hardhat-contract-sizer": "2.10.3",
34+
"@bahuy3103/hardhat-storage-layout": "0.2.6",
3435
"@nomicfoundation/hardhat-chai-matchers": "^1.0.3",
3536
"@nomicfoundation/hardhat-foundry": "^1.0.1",
3637
"@nomiclabs/hardhat-ethers": "^2.0.3",
@@ -46,12 +47,12 @@
4647
"ethers": "^5.5.2",
4748
"fs-extra": "11.1.1",
4849
"hardhat": "2.14.0",
49-
"hardhat-contract-sizer": "2.8.0",
5050
"hardhat-deploy": "0.11.29",
5151
"hardhat-gas-reporter": "^1.0.8",
52-
"hardhat-storage-layout": "^0.1.7",
5352
"husky": "^7.0.4",
5453
"lint-staged": ">=10",
54+
"lodash.isequal": "^4.5.0",
55+
"lodash.uniqwith": "^4.5.0",
5556
"prettier": "^2.5.1",
5657
"prettier-plugin-solidity": "^1.0.0-beta.19",
5758
"rimraf": "^3.0.2",
Lines changed: 138 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,27 @@
1-
import fs from 'fs-extra';
1+
import fs from 'fs';
22
import { table } from 'table';
33
import { task } from 'hardhat/config';
4-
import { boolean } from 'hardhat/internal/core/params/argumentTypes';
4+
import { HardhatRuntimeEnvironment } from 'hardhat/types';
5+
import isEqual from 'lodash.isequal';
6+
import uniqWith from 'lodash.uniqwith';
57

6-
const TABLE_STYLE = {
7-
/*
8-
Default Style
9-
┌────────────┬─────┬──────┐
10-
│ foo │ bar │ baz │
11-
├────────────┼─────┼──────┤
12-
│ frobnicate │ bar │ quuz │
13-
└────────────┴─────┴──────┘
14-
*/
15-
headerTop: {
16-
left: '┌',
17-
mid: '┬',
18-
right: '┐',
19-
other: '─',
20-
},
21-
headerBottom: {
22-
left: '├',
23-
mid: '┼',
24-
right: '┤',
25-
other: '─',
26-
},
27-
tableBottom: {
28-
left: '└',
29-
mid: '┴',
30-
right: '┘',
31-
other: '─',
32-
},
33-
vertical: '│',
34-
rowSeparator: {
35-
left: '├',
36-
mid: '┼',
37-
right: '┤',
38-
other: '─',
39-
},
40-
};
41-
42-
const preprocessFile = (fileContent: string) => {
43-
const whiteSpaceRegex = /[\s,\|]/g; // white space
44-
// remove all white space
45-
const fileContentWithoutWhiteSpace = fileContent.replace(whiteSpaceRegex, '');
46-
47-
// get only table contents
48-
const startIndex = fileContentWithoutWhiteSpace.indexOf(TABLE_STYLE.headerTop.right);
49-
const endIndex = fileContentWithoutWhiteSpace.indexOf(TABLE_STYLE.tableBottom.left);
50-
if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex) {
51-
const result = fileContentWithoutWhiteSpace.substring(startIndex + 2, endIndex);
52-
return result;
53-
} else {
54-
throw new Error('File does not contain any table');
55-
}
56-
};
57-
58-
const preprocessTable = (tableContent: string) => {
59-
const colorRegex = /\x1B\[\d{1,3}(;\d{1,3})*m/g; // \x1B[30m \x1B[305m \x1B[38;5m
60-
// remove all color code
61-
const contentsWithoutColorCode = tableContent.replace(colorRegex, '');
62-
// get list items by split vertical sperator
63-
const listItemOfTable = contentsWithoutColorCode.split(TABLE_STYLE.vertical);
8+
interface StateVariable {
9+
contractName: string;
10+
name: string;
11+
slot: string;
12+
offset: number;
13+
type: string;
14+
numberOfBytes: string;
15+
}
16+
enum ExportType {
17+
TABLE,
18+
INLINE,
19+
TABLE_AND_INLINE,
20+
UNKNOWN,
21+
}
6422

65-
return listItemOfTable;
66-
};
23+
const TABLE_FILE_NAME = `storage_layout_table.log`;
24+
const INLINE_FILE_NAME = `storage_layout.log`;
6725

6826
const removeIdentifierSuffix = (type: string) => {
6927
const suffixIdRegex = /\d+_(storage|memory|calldata|ptr)/g; // id_memory id_storage
@@ -72,111 +30,133 @@ const removeIdentifierSuffix = (type: string) => {
7230
return type.replace(suffixIdRegex, '_$1').replace(contractRegex, '$1($2)').replace(enumRegex, '$1($2)');
7331
};
7432

75-
const generateStorageLayoutTable = async ({ source, destination }: { source: string; destination: string }) => {
76-
try {
77-
if (fs.existsSync(source)) {
78-
const fileContent = await fs.readFile(source, 'utf-8');
79-
const tableContent = preprocessFile(fileContent);
80-
const listItemOfTable = preprocessTable(tableContent);
81-
const data = [];
82-
for (let i = 0; i < listItemOfTable.length; i += 9) {
83-
// remove two collums: idx (index = 5) and artifacts (index =6)
84-
const row = listItemOfTable.slice(i, i + 8).filter((_, idx) => idx != 5 && idx != 6);
33+
const getAndPreprocessData = async (hre: HardhatRuntimeEnvironment): Promise<StateVariable[]> => {
34+
const data = await hre.storageLayout.getStorageLayout();
35+
const result = data.contracts.reduce(function (filtered: StateVariable[], row) {
36+
const stateVars: StateVariable[] = row.stateVariables.map((variable) => ({
37+
contractName: row.name,
38+
name: variable.name,
39+
slot: variable.slot,
40+
offset: variable.offset,
41+
type: removeIdentifierSuffix(variable.type),
42+
numberOfBytes: variable.numberOfBytes,
43+
}));
44+
if (stateVars.length == 0) {
45+
return filtered;
46+
}
47+
const result = uniqWith(filtered.concat(stateVars), isEqual);
48+
return result;
49+
}, []);
50+
return result;
51+
};
52+
class StorageLayoutFactory {
53+
static async build(env: HardhatRuntimeEnvironment, exportedType: ExportType): Promise<BaseStorageLayout[]> {
54+
const data = await getAndPreprocessData(env);
55+
switch (exportedType) {
56+
case ExportType.TABLE:
57+
return [new TableStorageLayout(env, data)];
58+
case ExportType.INLINE:
59+
return [new InLineStorageLayout(env, data)];
60+
case ExportType.TABLE_AND_INLINE:
61+
return [new TableStorageLayout(env, data), new InLineStorageLayout(env, data)];
62+
default:
63+
throw new Error('Invalid exported type');
64+
}
65+
}
66+
}
8567

86-
// remove the suffix identifier of data type: <id>_(storage|memory|calldata)
87-
const dataType = row[4];
88-
row[4] = removeIdentifierSuffix(dataType);
89-
data.push(row);
68+
abstract class BaseStorageLayout {
69+
env: HardhatRuntimeEnvironment;
70+
data: StateVariable[];
71+
constructor(env: HardhatRuntimeEnvironment, data: StateVariable[]) {
72+
this.env = env;
73+
this.data = data;
74+
}
75+
async prepareData(): Promise<StateVariable[]> {
76+
const data = await this.env.storageLayout.getStorageLayout();
77+
const result = data.contracts.reduce(function (filtered: StateVariable[], row) {
78+
const stateVars: StateVariable[] = row.stateVariables.map((variable) => ({
79+
contractName: row.name,
80+
name: variable.name,
81+
slot: variable.slot,
82+
offset: variable.offset,
83+
type: removeIdentifierSuffix(variable.type),
84+
numberOfBytes: variable.numberOfBytes,
85+
}));
86+
if (stateVars.length == 0) {
87+
return filtered;
9088
}
91-
const output = table(data);
92-
await fs.writeFile(destination, output, 'utf8');
93-
console.log(`Successful generate storage layout table at ${destination}`);
94-
} else {
95-
throw Error(`File storage layout at ${source} not exits`);
89+
const result = uniqWith(filtered.concat(stateVars), isEqual);
90+
return result;
91+
}, []);
92+
return result;
93+
}
94+
abstract getContent(): string;
95+
abstract getFilePath(): string;
96+
async export() {
97+
const filePath = this.getFilePath();
98+
try {
99+
if (fs.existsSync(filePath)) {
100+
fs.unlinkSync(filePath);
101+
}
102+
fs.writeFileSync(filePath, '');
103+
fs.writeFileSync(filePath, this.getContent(), 'utf8');
104+
console.log(`Successful generate storage layout at ${filePath}`);
105+
} catch (err) {
106+
console.error(err);
96107
}
97-
} catch (err) {
98-
console.error(err);
99108
}
100-
};
109+
}
101110

102-
const generateStorageLayoutInline = async ({
103-
source,
104-
destination,
105-
override,
106-
}: {
107-
source: string;
108-
destination: string;
109-
override: boolean;
110-
}) => {
111-
try {
112-
if (fs.existsSync(source)) {
113-
if (!fs.existsSync(destination) || override) {
114-
const logger = fs.createWriteStream(destination, { flags: 'w' });
115-
const fileContent = await fs.readFile(source, 'utf-8');
116-
const tableContent = preprocessFile(fileContent);
117-
const listItemOfTable = preprocessTable(tableContent);
118-
let headers: string[] = [];
119-
const data: string[] = [];
120-
for (let i = 0; i < listItemOfTable.length; i += 9) {
121-
// remove two collums: idx (index = 5) and artifacts (index =6)
122-
const row = listItemOfTable.slice(i, i + 8).filter((_, idx) => idx != 5 && idx != 6);
111+
class InLineStorageLayout extends BaseStorageLayout {
112+
getContent(): string {
113+
const lines: string[] = [];
114+
this.data.forEach((stateVar) => {
115+
const line = `${stateVar.contractName}:${stateVar.name} (storage_slot: ${stateVar.slot}) (offset: ${stateVar.offset}) (type: ${stateVar.type}) (numberOfBytes: ${stateVar.numberOfBytes})`;
116+
lines.push(line);
117+
});
118+
return lines.join('\n');
119+
}
123120

124-
// remove the suffix identifier of data type: <id>_(storage|memory|calldata)
125-
const dataType = row[4];
126-
row[4] = removeIdentifierSuffix(dataType);
127-
if (i == 0) {
128-
headers = row;
129-
} else {
130-
data.push(
131-
`${row[0]}:${row[1]} (${headers[2]}: ${row[2]}) (${headers[3]}: ${row[3]}) (${headers[4]}: ${row[4]}) (${headers[5]}: ${row[5]})`
132-
);
133-
}
134-
}
135-
logger.write(data.join('\n'));
136-
} else {
137-
throw Error(
138-
`Cannot generate storage layout because file ${destination} already exists. Use the "override" flag to overwrite.`
139-
);
140-
}
141-
console.log(`Successful generate storage layout at ${destination}`);
142-
} else {
143-
throw Error(`File storage layout at ${source} not exits`);
144-
}
145-
} catch (err) {
146-
console.error(err);
121+
getFilePath(): string {
122+
return this.env.config.paths.newStorageLayoutPath + '/' + INLINE_FILE_NAME;
147123
}
148-
};
149-
const removeTempStorageLayout = async ({ path }: { path: string }) => {
150-
try {
151-
if (fs.existsSync(path)) {
152-
await fs.unlink(path);
153-
console.log(`Successful delete temporary storage file`);
154-
} else {
155-
throw Error(`File storage layout at ${path} not exits`);
156-
}
157-
} catch (err) {
158-
console.error(err);
124+
}
125+
126+
class TableStorageLayout extends BaseStorageLayout {
127+
getContent(): string {
128+
const rows: string[][] = [['Contract', 'Name', 'Slot', 'Offset', 'Type', 'Number of bytes']];
129+
this.data.forEach((stateVar) => {
130+
rows.push(Object.values(stateVar));
131+
});
132+
return table(rows);
159133
}
160-
};
161-
/// @notice Generate storage layout table from `source` file to `destination` file.
162-
task('generate-storage-layout-table')
163-
.addParam('source', 'The path to storage layout file extracted from hardhat-storage-layout')
164-
.addOptionalParam('destination', 'The path to store storage layout after generating', 'logs/storage_layout_table.log')
165-
.setAction(async ({ source, destination }, _) => {
166-
await generateStorageLayoutTable({ source, destination });
167-
await removeTempStorageLayout({ path: source });
168-
});
169134

170-
/// @notice Generate storage layout in both live from `source` file.
135+
getFilePath(): string {
136+
return this.env.config.paths.newStorageLayoutPath + '/' + TABLE_FILE_NAME;
137+
}
138+
}
139+
140+
/// @notice Generate storage layout.
171141
task('generate-storage-layout')
172-
.addParam('source', 'The path to storage layout file extracted from hardhat-storage-layout')
173-
.addOptionalParam('override', 'Indicates whether override the destination if it already exits', true, boolean)
174-
.setAction(async ({ source, override }, hre) => {
175-
try {
176-
await generateStorageLayoutTable({ source, destination: 'logs/storage_layout_table.log' });
177-
await generateStorageLayoutInline({ source, override, destination: 'logs/storage_layout.log' });
178-
await removeTempStorageLayout({ path: source });
179-
} catch (err) {
180-
console.error(err);
142+
.addFlag('table', 'Export storage layout as table')
143+
.addFlag('inline', 'Export storage layout as inline')
144+
.setAction(async ({ table, inline }, hre) => {
145+
let exportedType: ExportType;
146+
if (table && inline) {
147+
exportedType = ExportType.TABLE_AND_INLINE;
148+
} else if (table) {
149+
exportedType = ExportType.TABLE;
150+
} else if (inline) {
151+
exportedType = ExportType.INLINE;
152+
} else {
153+
exportedType = ExportType.UNKNOWN;
181154
}
155+
156+
const storageLayouts = await StorageLayoutFactory.build(hre, exportedType);
157+
await Promise.all(
158+
storageLayouts.map(async (storageLayout) => {
159+
await storageLayout.export();
160+
})
161+
);
182162
});

0 commit comments

Comments
 (0)