1
- import fs from 'fs-extra ' ;
1
+ import fs from 'fs' ;
2
2
import { table } from 'table' ;
3
3
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' ;
5
7
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
+ }
64
22
65
- return listItemOfTable ;
66
- } ;
23
+ const TABLE_FILE_NAME = `storage_layout_table.log` ;
24
+ const INLINE_FILE_NAME = `storage_layout.log` ;
67
25
68
26
const removeIdentifierSuffix = ( type : string ) => {
69
27
const suffixIdRegex = / \d + _ ( s t o r a g e | m e m o r y | c a l l d a t a | p t r ) / g; // id_memory id_storage
@@ -72,111 +30,133 @@ const removeIdentifierSuffix = (type: string) => {
72
30
return type . replace ( suffixIdRegex , '_$1' ) . replace ( contractRegex , '$1($2)' ) . replace ( enumRegex , '$1($2)' ) ;
73
31
} ;
74
32
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
+ }
85
67
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 ;
90
88
}
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 ) ;
96
107
}
97
- } catch ( err ) {
98
- console . error ( err ) ;
99
108
}
100
- } ;
109
+ }
101
110
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
+ }
123
120
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 ;
147
123
}
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 ) ;
159
133
}
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
- } ) ;
169
134
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.
171
141
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 ;
181
154
}
155
+
156
+ const storageLayouts = await StorageLayoutFactory . build ( hre , exportedType ) ;
157
+ await Promise . all (
158
+ storageLayouts . map ( async ( storageLayout ) => {
159
+ await storageLayout . export ( ) ;
160
+ } )
161
+ ) ;
182
162
} ) ;
0 commit comments