sol2uml 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,17 +2,19 @@
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/sol2uml.svg)](https://badge.fury.io/js/sol2uml)
4
4
 
5
- [Unified Modeling Language (UML)](https://en.wikipedia.org/wiki/Unified_Modeling_Language) [class diagram](https://en.wikipedia.org/wiki/Class_diagram) generator for [Solidity](https://solidity.readthedocs.io/) contracts.
5
+ A visualisation tool for [Solidity](https://solidity.readthedocs.io/) contracts featuring:
6
+ 1. [Unified Modeling Language (UML)](https://en.wikipedia.org/wiki/Unified_Modeling_Language) [class diagram](https://en.wikipedia.org/wiki/Class_diagram) generator for Solidity contracts.
7
+ 2. Contract storage layout diagrams.
6
8
 
7
- Open Zeppelin's ERC20 token contracts generated from [version 2.5.1](https://github.com/OpenZeppelin/openzeppelin-solidity/tree/v2.5.1/contracts/token/ERC20)
9
+ UML class diagram of Open Zeppelin's ERC20 token contracts generated from [version 2.5.1](https://github.com/OpenZeppelin/openzeppelin-solidity/tree/v2.5.1/contracts/token/ERC20)
8
10
  ![Open Zeppelin ERC20](./examples/OpenZeppelinERC20.svg)
9
11
 
10
12
  See more contract diagrams [here](./examples/README.md).
11
13
 
12
- USDC storage slots from the [verified source code](https://etherscan.io/address/0xa2327a938febf5fec13bacfb16ae10ecbc4cbdcf#code) on Etherscan.
13
- ![USDC](./examples/storage/usdc.png)
14
+ Storage layout diagram of USDC's [verified source code](https://etherscan.io/address/0xa2327a938febf5fec13bacfb16ae10ecbc4cbdcf#code) on Etherscan.
15
+ ![USDC](./examples/storage/usdcData.svg)
14
16
 
15
- See more storage slot diagrams [here](./examples/storage/README.md).
17
+ See more contract storage diagram examples [here](./examples/storage/README.md).
16
18
 
17
19
  # Install
18
20
 
@@ -1,2 +1,2 @@
1
1
  import { Association, UmlClass } from './umlClass';
2
- export declare const findAssociatedClass: (association: Association, sourceUmlClass: UmlClass, umlClasses: UmlClass[]) => UmlClass;
2
+ export declare const findAssociatedClass: (association: Association, sourceUmlClass: UmlClass, umlClasses: UmlClass[]) => UmlClass | undefined;
@@ -1,31 +1,75 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.findAssociatedClass = void 0;
4
+ const umlClass_1 = require("./umlClass");
4
5
  // Find the UML class linked to the association
5
6
  const findAssociatedClass = (association, sourceUmlClass, umlClasses) => {
6
- return umlClasses.find((targetUmlClass) => {
7
- return (
8
- // class is in the same source file
7
+ let umlClass = umlClasses.find((targetUmlClass) => {
8
+ // is the source class link via the association to the target class?
9
+ if (isAssociated(association, sourceUmlClass, targetUmlClass))
10
+ return true;
11
+ // Not linked so now try linking to target under the node_modules folder.
12
+ // eg remove node_modules from node_modules/@openzeppelin/contracts-upgradeable/proxy/Initializable.sol
13
+ // is the target class under node_modules?
14
+ if (targetUmlClass.relativePath.match(/^node_modules\//)) {
15
+ // clone the target and updated absolutePath and relativePath so it's no longer under node_modules
16
+ const clonedTargetClass = new umlClass_1.UmlClass(targetUmlClass);
17
+ clonedTargetClass.absolutePath =
18
+ targetUmlClass.absolutePath.replace(/^node_modules\//, '');
19
+ clonedTargetClass.relativePath =
20
+ targetUmlClass.relativePath.replace(/^node_modules\//, '');
21
+ // is the source class link via the association to the target class?
22
+ return isAssociated(association, sourceUmlClass, clonedTargetClass);
23
+ }
24
+ // could not find a link from the source to target via the association
25
+ return false;
26
+ });
27
+ // If a link was found
28
+ if (umlClass)
29
+ return umlClass;
30
+ // Could not find a link so now need to recursively look at imports of imports
31
+ return findImplicitImport(association, sourceUmlClass, umlClasses);
32
+ };
33
+ exports.findAssociatedClass = findAssociatedClass;
34
+ // Tests if source class can be linked to the target class via an association
35
+ const isAssociated = (association, sourceUmlClass, targetUmlClass) => {
36
+ return (
37
+ // class is in the same source file
38
+ (association.targetUmlClassName === targetUmlClass.name &&
39
+ sourceUmlClass.absolutePath === targetUmlClass.absolutePath) ||
40
+ // imported classes with no explicit import names
9
41
  (association.targetUmlClassName === targetUmlClass.name &&
10
- sourceUmlClass.absolutePath === targetUmlClass.absolutePath) ||
11
- // imported classes with no explicit import names
12
- (association.targetUmlClassName === targetUmlClass.name &&
13
- sourceUmlClass.imports.find((i) => i.absolutePath === targetUmlClass.absolutePath &&
14
- i.classNames.length === 0)) ||
15
- // imported classes with explicit import names or import aliases
16
42
  sourceUmlClass.imports.find((i) => i.absolutePath === targetUmlClass.absolutePath &&
17
- i.classNames.find((importedClass) =>
18
- // no import alias
43
+ i.classNames.length === 0)) ||
44
+ // imported classes with explicit import names or import aliases
45
+ sourceUmlClass.imports.find((i) => i.absolutePath === targetUmlClass.absolutePath &&
46
+ i.classNames.find((importedClass) =>
47
+ // no import alias
48
+ (association.targetUmlClassName ===
49
+ importedClass.className &&
50
+ importedClass.className === targetUmlClass.name &&
51
+ importedClass.alias == undefined) ||
52
+ // import alias
19
53
  (association.targetUmlClassName ===
20
- importedClass.className &&
21
- importedClass.className ===
22
- targetUmlClass.name &&
23
- importedClass.alias == undefined) ||
24
- // import alias
25
- (association.targetUmlClassName ===
26
- importedClass.alias &&
27
- importedClass.className === targetUmlClass.name))));
28
- });
54
+ importedClass.alias &&
55
+ importedClass.className === targetUmlClass.name))));
56
+ };
57
+ const findImplicitImport = (association, sourceUmlClass, umlClasses) => {
58
+ // Get all implicit imports. That is, imports that do not explicitly import contracts or interfaces.
59
+ const implicitImports = sourceUmlClass.imports.filter((i) => i.classNames.length === 0);
60
+ // For each implicit import
61
+ for (const importDetail of implicitImports) {
62
+ // Find a class with the same absolute path as the import so we can get the new imports
63
+ const newSourceUmlClass = umlClasses.find((c) => c.absolutePath === importDetail.absolutePath);
64
+ if (!newSourceUmlClass) {
65
+ // Could not find a class in the import file so just move onto the next loop
66
+ continue;
67
+ }
68
+ // TODO need to handle imports that use aliases as the association will not be found
69
+ const umlClass = (0, exports.findAssociatedClass)(association, newSourceUmlClass, umlClasses);
70
+ if (umlClass)
71
+ return umlClass;
72
+ }
73
+ return undefined;
29
74
  };
30
- exports.findAssociatedClass = findAssociatedClass;
31
75
  //# sourceMappingURL=associations.js.map
@@ -176,7 +176,7 @@ function parseContractDefinition(umlClass, node) {
176
176
  });
177
177
  // Is the variable a constant that could be used in declaring fixed sized arrays
178
178
  if (variable.isDeclaredConst) {
179
- if (variable?.expression.type === 'NumberLiteral') {
179
+ if (variable?.expression?.type === 'NumberLiteral') {
180
180
  umlClass.constants.push({
181
181
  name: variable.name,
182
182
  value: parseInt(variable.expression.number),
@@ -1,7 +1,8 @@
1
1
  import { Attribute, UmlClass } from './umlClass';
2
2
  export declare enum StorageType {
3
- Contract = 0,
4
- Struct = 1
3
+ Contract = "Contract",
4
+ Struct = "Struct",
5
+ Array = "Array"
5
6
  }
6
7
  export interface Variable {
7
8
  id: number;
@@ -10,17 +11,22 @@ export interface Variable {
10
11
  byteSize: number;
11
12
  byteOffset: number;
12
13
  type: string;
13
- variable: string;
14
+ dynamic: boolean;
15
+ variable?: string;
14
16
  contractName?: string;
15
- values: string[];
16
- structStorageId?: number;
17
+ noValue: boolean;
18
+ value?: string;
19
+ referenceStorageId?: number;
17
20
  enumId?: number;
18
21
  }
19
22
  export interface Storage {
20
23
  id: number;
21
24
  name: string;
22
25
  address?: string;
26
+ slotKey?: string;
23
27
  type: StorageType;
28
+ arrayLength?: number;
29
+ arrayDynamic?: boolean;
24
30
  variables: Variable[];
25
31
  }
26
32
  /**
@@ -31,6 +37,12 @@ export interface Storage {
31
37
  */
32
38
  export declare const addStorageValues: (url: string, contractAddress: string, storage: Storage, blockTag: string) => Promise<void>;
33
39
  export declare const convertClasses2Storages: (contractName: string, umlClasses: UmlClass[]) => Storage[];
34
- export declare const parseStructStorage: (attribute: Attribute, otherClasses: UmlClass[], storages: Storage[]) => Storage | undefined;
35
- export declare const calcStorageByteSize: (attribute: Attribute, umlClass: UmlClass, otherClasses: UmlClass[]) => number;
40
+ export declare const parseReferenceStorage: (attribute: Attribute, umlClass: UmlClass, otherClasses: UmlClass[], storages: Storage[]) => Storage | undefined;
41
+ export declare const calcStorageByteSize: (attribute: Attribute, umlClass: UmlClass, otherClasses: UmlClass[]) => {
42
+ size: number;
43
+ dynamic: boolean;
44
+ };
36
45
  export declare const isElementary: (type: string) => boolean;
46
+ export declare const calcSlotKey: (variable: Variable) => string | undefined;
47
+ export declare const offsetStorageSlots: (storage: Storage, slots: number, storages: Storage[]) => void;
48
+ export declare const findDimensionLength: (umlClass: UmlClass, dimension: string) => number;
@@ -1,13 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isElementary = exports.calcStorageByteSize = exports.parseStructStorage = exports.convertClasses2Storages = exports.addStorageValues = exports.StorageType = void 0;
3
+ exports.findDimensionLength = exports.offsetStorageSlots = exports.calcSlotKey = exports.isElementary = exports.calcStorageByteSize = exports.parseReferenceStorage = exports.convertClasses2Storages = exports.addStorageValues = exports.StorageType = void 0;
4
4
  const umlClass_1 = require("./umlClass");
5
5
  const associations_1 = require("./associations");
6
6
  const slotValues_1 = require("./slotValues");
7
+ const utils_1 = require("ethers/lib/utils");
8
+ const ethers_1 = require("ethers");
7
9
  var StorageType;
8
10
  (function (StorageType) {
9
- StorageType[StorageType["Contract"] = 0] = "Contract";
10
- StorageType[StorageType["Struct"] = 1] = "Struct";
11
+ StorageType["Contract"] = "Contract";
12
+ StorageType["Struct"] = "Struct";
13
+ StorageType["Array"] = "Array";
11
14
  })(StorageType = exports.StorageType || (exports.StorageType = {}));
12
15
  let storageId = 1;
13
16
  let variableId = 1;
@@ -18,10 +21,11 @@ let variableId = 1;
18
21
  * @param storage is mutated with the storage values
19
22
  */
20
23
  const addStorageValues = async (url, contractAddress, storage, blockTag) => {
21
- const slots = storage.variables.map((s) => s.fromSlot);
24
+ const valueVariables = storage.variables.filter((s) => !s.noValue);
25
+ const slots = valueVariables.map((s) => s.fromSlot);
22
26
  const values = await (0, slotValues_1.getStorageValues)(url, contractAddress, slots, blockTag);
23
- storage.variables.forEach((storage, i) => {
24
- storage.values = [values[i]];
27
+ valueVariables.forEach((storage, i) => {
28
+ storage.value = values[i];
25
29
  });
26
30
  };
27
31
  exports.addStorageValues = addStorageValues;
@@ -62,8 +66,9 @@ const parseVariables = (umlClass, umlClasses, variables, storages, inheritedCont
62
66
  // Recursively parse each new inherited contract
63
67
  newInheritedContracts.forEach((parent) => {
64
68
  const parentClass = (0, associations_1.findAssociatedClass)(parent, umlClass, umlClasses);
65
- if (!parentClass)
66
- throw Error(`Failed to find parent contract ${parent.targetUmlClassName} of ${umlClass.absolutePath}`);
69
+ if (!parentClass) {
70
+ throw Error(`Failed to find inherited contract "${parent.targetUmlClassName}" of "${umlClass.absolutePath}"`);
71
+ }
67
72
  // recursively parse inherited contract
68
73
  parseVariables(parentClass, umlClasses, variables, storages, inheritedContracts);
69
74
  });
@@ -72,10 +77,11 @@ const parseVariables = (umlClass, umlClasses, variables, storages, inheritedCont
72
77
  // Ignore any attributes that are constants or immutable
73
78
  if (attribute.compiled)
74
79
  return;
75
- const byteSize = (0, exports.calcStorageByteSize)(attribute, umlClass, umlClasses);
76
- // find any dependent structs
77
- const linkedStruct = (0, exports.parseStructStorage)(attribute, umlClasses, storages);
78
- const structStorageId = linkedStruct?.id;
80
+ const { size: byteSize, dynamic } = (0, exports.calcStorageByteSize)(attribute, umlClass, umlClasses);
81
+ const noValue = attribute.attributeType === umlClass_1.AttributeType.Mapping ||
82
+ (attribute.attributeType === umlClass_1.AttributeType.Array && !dynamic);
83
+ // find any dependent storage locations
84
+ const referenceStorage = (0, exports.parseReferenceStorage)(attribute, umlClass, umlClasses, storages);
79
85
  // Get the toSlot of the last storage item
80
86
  let lastToSlot = 0;
81
87
  let nextOffset = 0;
@@ -84,45 +90,121 @@ const parseVariables = (umlClass, umlClasses, variables, storages, inheritedCont
84
90
  lastToSlot = lastStorage.toSlot;
85
91
  nextOffset = lastStorage.byteOffset + lastStorage.byteSize;
86
92
  }
93
+ let newVariable;
87
94
  if (nextOffset + byteSize > 32) {
88
95
  const nextFromSlot = variables.length > 0 ? lastToSlot + 1 : 0;
89
- variables.push({
96
+ newVariable = {
90
97
  id: variableId++,
91
98
  fromSlot: nextFromSlot,
92
99
  toSlot: nextFromSlot + Math.floor((byteSize - 1) / 32),
93
100
  byteSize,
94
101
  byteOffset: 0,
95
102
  type: attribute.type,
103
+ dynamic,
104
+ noValue,
96
105
  variable: attribute.name,
97
106
  contractName: umlClass.name,
98
- structStorageId,
99
- values: [],
100
- });
107
+ referenceStorageId: referenceStorage?.id,
108
+ };
101
109
  }
102
110
  else {
103
- variables.push({
111
+ newVariable = {
104
112
  id: variableId++,
105
113
  fromSlot: lastToSlot,
106
114
  toSlot: lastToSlot,
107
115
  byteSize,
108
116
  byteOffset: nextOffset,
109
117
  type: attribute.type,
118
+ dynamic,
119
+ noValue,
110
120
  variable: attribute.name,
111
121
  contractName: umlClass.name,
112
- structStorageId,
113
- values: [],
114
- });
122
+ referenceStorageId: referenceStorage?.id,
123
+ };
124
+ }
125
+ if (referenceStorage) {
126
+ if (!newVariable.dynamic) {
127
+ (0, exports.offsetStorageSlots)(referenceStorage, newVariable.fromSlot, storages);
128
+ }
129
+ else if (attribute.attributeType === umlClass_1.AttributeType.Array) {
130
+ referenceStorage.slotKey = (0, exports.calcSlotKey)(newVariable);
131
+ }
115
132
  }
133
+ variables.push(newVariable);
116
134
  });
117
135
  return variables;
118
136
  };
119
- const parseStructStorage = (attribute, otherClasses, storages) => {
120
- if (attribute.attributeType === umlClass_1.AttributeType.UserDefined) {
121
- // Have we already created the storage?
122
- const existingStorage = storages.find((dep) => dep.name === attribute.type);
123
- if (existingStorage) {
124
- return existingStorage;
137
+ const parseReferenceStorage = (attribute, umlClass, otherClasses, storages) => {
138
+ if (attribute.attributeType === umlClass_1.AttributeType.Array) {
139
+ // storage is dynamic if the attribute type ends in []
140
+ const result = attribute.type.match(/\[(\w*)]$/);
141
+ const dynamic = result[1] === '';
142
+ const arrayLength = !dynamic
143
+ ? (0, exports.findDimensionLength)(umlClass, result[1])
144
+ : undefined;
145
+ // get the type of the array items. eg
146
+ // address[][4][2] will have base type address[][4]
147
+ const baseType = attribute.type.substring(0, attribute.type.lastIndexOf('['));
148
+ let baseAttributeType;
149
+ if ((0, exports.isElementary)(baseType)) {
150
+ baseAttributeType = umlClass_1.AttributeType.Elementary;
125
151
  }
152
+ else if (baseType[baseType.length - 1] === ']') {
153
+ baseAttributeType = umlClass_1.AttributeType.Array;
154
+ }
155
+ else {
156
+ baseAttributeType = umlClass_1.AttributeType.UserDefined;
157
+ }
158
+ const baseAttribute = {
159
+ visibility: attribute.visibility,
160
+ name: baseType,
161
+ type: baseType,
162
+ attributeType: baseAttributeType,
163
+ };
164
+ const { size: arrayItemSize } = (0, exports.calcStorageByteSize)(baseAttribute, umlClass, otherClasses);
165
+ const slotSize = arrayItemSize > 16 ? 32 : arrayItemSize;
166
+ const firstVariable = {
167
+ id: variableId++,
168
+ fromSlot: 0,
169
+ toSlot: Math.floor((slotSize - 1) / 32),
170
+ byteSize: arrayItemSize,
171
+ byteOffset: 0,
172
+ type: baseType,
173
+ dynamic,
174
+ noValue: false,
175
+ };
176
+ const variables = [firstVariable];
177
+ if (arrayLength > 1) {
178
+ for (let i = 1; i < arrayLength; i++) {
179
+ variables.push({
180
+ id: variableId++,
181
+ fromSlot: Math.floor((i * slotSize) / 32),
182
+ toSlot: Math.floor(((i + 1) * slotSize - 1) / 32),
183
+ byteSize: arrayItemSize,
184
+ byteOffset: (i * slotSize) % 32,
185
+ type: baseType,
186
+ dynamic,
187
+ noValue: false,
188
+ });
189
+ }
190
+ }
191
+ // recursively add storage
192
+ if (baseAttributeType !== umlClass_1.AttributeType.Elementary) {
193
+ const referenceStorage = (0, exports.parseReferenceStorage)(baseAttribute, umlClass, otherClasses, storages);
194
+ firstVariable.referenceStorageId = referenceStorage?.id;
195
+ }
196
+ const newStorage = {
197
+ id: storageId++,
198
+ name: `${attribute.type}: ${attribute.name}`,
199
+ type: StorageType.Array,
200
+ arrayDynamic: dynamic,
201
+ arrayLength,
202
+ variables,
203
+ };
204
+ storages.push(newStorage);
205
+ return newStorage;
206
+ }
207
+ if (attribute.attributeType === umlClass_1.AttributeType.UserDefined) {
126
208
  // Is the user defined type linked to another Contract, Struct or Enum?
127
209
  const dependentClass = otherClasses.find(({ name }) => {
128
210
  return (name === attribute.type || name === attribute.type.split('.')[1]);
@@ -143,20 +225,13 @@ const parseStructStorage = (attribute, otherClasses, storages) => {
143
225
  }
144
226
  return undefined;
145
227
  }
146
- if (attribute.attributeType === umlClass_1.AttributeType.Mapping ||
147
- attribute.attributeType === umlClass_1.AttributeType.Array) {
148
- // get the UserDefined type from the mapping or array
228
+ if (attribute.attributeType === umlClass_1.AttributeType.Mapping) {
229
+ // get the UserDefined type from the mapping
149
230
  // note the mapping could be an array of Structs
150
231
  // Could also be a mapping of a mapping
151
- const result = attribute.attributeType === umlClass_1.AttributeType.Mapping
152
- ? attribute.type.match(/=\\>((?!mapping)\w*)[\\[]/)
153
- : attribute.type.match(/(\w+)\[/);
232
+ const result = attribute.type.match(/=\\>((?!mapping)\w*)[\\[]/);
233
+ // If mapping of user defined type
154
234
  if (result !== null && result[1] && !(0, exports.isElementary)(result[1])) {
155
- // Have we already created the storage?
156
- const existingStorage = storages.find(({ name }) => name === result[1] || name === result[1].split('.')[1]);
157
- if (existingStorage) {
158
- return existingStorage;
159
- }
160
235
  // Find UserDefined type
161
236
  const typeClass = otherClasses.find(({ name }) => name === result[1] || name === result[1].split('.')[1]);
162
237
  if (!typeClass) {
@@ -178,66 +253,78 @@ const parseStructStorage = (attribute, otherClasses, storages) => {
178
253
  }
179
254
  return undefined;
180
255
  };
181
- exports.parseStructStorage = parseStructStorage;
256
+ exports.parseReferenceStorage = parseReferenceStorage;
182
257
  // Calculates the storage size of an attribute in bytes
183
258
  const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
184
259
  if (attribute.attributeType === umlClass_1.AttributeType.Mapping ||
185
260
  attribute.attributeType === umlClass_1.AttributeType.Function) {
186
- return 32;
261
+ return { size: 32, dynamic: true };
187
262
  }
188
263
  if (attribute.attributeType === umlClass_1.AttributeType.Array) {
189
- // All array dimensions must be fixed. eg [2][3][8].
190
- const result = attribute.type.match(/(\w+)(\[([\w][\w]*)\])+$/);
191
- // The above will not match any dynamic array dimensions, eg [],
192
- // as there needs to be one or more [0-9]+ in the square brackets
193
- if (result === null) {
194
- // Any dynamic array dimension means the whole array is dynamic
195
- // so only takes 32 bytes (1 slot)
196
- return 32;
264
+ // Fixed sized arrays are read from right to left until there is a dynamic dimension
265
+ // eg address[][3][2] is a fixed size array that uses 6 slots.
266
+ // while address [2][] is a dynamic sized array.
267
+ const arrayDimensions = attribute.type.match(/\[\w*]/g);
268
+ // Remove first [ and last ] from each arrayDimensions
269
+ const dimensionsStr = arrayDimensions.map((a) => a.slice(1, -1));
270
+ // fixed-sized arrays are read from right to left so reverse the dimensions
271
+ const dimensionsStrReversed = dimensionsStr.reverse();
272
+ // read fixed-size dimensions until we get a dynamic array with no dimension
273
+ let dimension = dimensionsStrReversed.shift();
274
+ const fixedDimensions = [];
275
+ while (dimension && dimension !== '') {
276
+ const dimensionNum = (0, exports.findDimensionLength)(umlClass, dimension);
277
+ fixedDimensions.push(dimensionNum);
278
+ // read the next dimension for the next loop
279
+ dimension = dimensionsStrReversed.shift();
280
+ }
281
+ // If the first dimension is dynamic, ie []
282
+ if (fixedDimensions.length === 0) {
283
+ // dynamic arrays start at the keccak256 of the slot number
284
+ // the array length is stored in the 32 byte slot
285
+ return { size: 32, dynamic: true };
197
286
  }
198
- // All array dimensions are fixes so we now need to multiply all the dimensions
199
- // to get a total number of array elements
200
- const arrayDimensions = attribute.type.match(/\[\w+/g);
201
- const dimensionsStr = arrayDimensions.map((d) => d.slice(1));
202
- const dimensions = dimensionsStr.map((dimension) => {
203
- const dimensionNum = parseInt(dimension);
204
- if (!isNaN(dimensionNum))
205
- return dimensionNum;
206
- // Try and size array dimension from declared constants
207
- const constant = umlClass.constants.find((constant) => constant.name === dimension);
208
- if (constant) {
209
- return constant.value;
210
- }
211
- throw Error(`Could not size fixed sized array with dimension "${dimension}"`);
212
- });
213
287
  let elementSize;
288
+ const type = attribute.type.substring(0, attribute.type.indexOf('['));
214
289
  // If a fixed sized array
215
- if ((0, exports.isElementary)(result[1])) {
290
+ if ((0, exports.isElementary)(type)) {
216
291
  const elementAttribute = {
217
292
  attributeType: umlClass_1.AttributeType.Elementary,
218
- type: result[1],
293
+ type,
219
294
  name: 'element',
220
295
  };
221
- elementSize = (0, exports.calcStorageByteSize)(elementAttribute, umlClass, otherClasses);
296
+ ({ size: elementSize } = (0, exports.calcStorageByteSize)(elementAttribute, umlClass, otherClasses));
222
297
  }
223
298
  else {
224
299
  const elementAttribute = {
225
300
  attributeType: umlClass_1.AttributeType.UserDefined,
226
- type: result[1],
301
+ type,
227
302
  name: 'userDefined',
228
303
  };
229
- elementSize = (0, exports.calcStorageByteSize)(elementAttribute, umlClass, otherClasses);
304
+ ({ size: elementSize } = (0, exports.calcStorageByteSize)(elementAttribute, umlClass, otherClasses));
230
305
  }
231
306
  // Anything over 16 bytes, like an address, will take a whole 32 byte slot
232
307
  if (elementSize > 16 && elementSize < 32) {
233
308
  elementSize = 32;
234
309
  }
235
- const firstDimensionBytes = elementSize * dimensions[0];
236
- const firstDimensionSlotBytes = Math.ceil(firstDimensionBytes / 32) * 32;
237
- const remainingElements = dimensions
238
- .slice(1)
310
+ // If multi dimension, then the first element is 32 bytes
311
+ if (fixedDimensions.length < arrayDimensions.length) {
312
+ const totalDimensions = fixedDimensions.reduce((total, dimension) => total * dimension, 1);
313
+ return {
314
+ size: 32 * totalDimensions,
315
+ dynamic: false,
316
+ };
317
+ }
318
+ const lastItem = fixedDimensions.length - 1;
319
+ const lastDimensionBytes = elementSize * fixedDimensions[lastItem];
320
+ const lastDimensionSlotBytes = Math.ceil(lastDimensionBytes / 32) * 32;
321
+ const remainingDimensions = fixedDimensions
322
+ .slice(0, lastItem)
239
323
  .reduce((total, dimension) => total * dimension, 1);
240
- return firstDimensionSlotBytes * remainingElements;
324
+ return {
325
+ size: lastDimensionSlotBytes * remainingDimensions,
326
+ dynamic: false,
327
+ };
241
328
  }
242
329
  // If a Struct or Enum
243
330
  if (attribute.attributeType === umlClass_1.AttributeType.UserDefined) {
@@ -250,12 +337,12 @@ const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
250
337
  }
251
338
  switch (attributeClass.stereotype) {
252
339
  case umlClass_1.ClassStereotype.Enum:
253
- return 1;
340
+ return { size: 1, dynamic: false };
254
341
  case umlClass_1.ClassStereotype.Contract:
255
342
  case umlClass_1.ClassStereotype.Abstract:
256
343
  case umlClass_1.ClassStereotype.Interface:
257
344
  case umlClass_1.ClassStereotype.Library:
258
- return 20;
345
+ return { size: 20, dynamic: false };
259
346
  case umlClass_1.ClassStereotype.Struct:
260
347
  let structByteSize = 0;
261
348
  attributeClass.attributes.forEach((structAttribute) => {
@@ -280,7 +367,7 @@ const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
280
367
  structByteSize = Math.ceil(structByteSize / 32) * 32;
281
368
  }
282
369
  }
283
- const attributeSize = (0, exports.calcStorageByteSize)(structAttribute, umlClass, otherClasses);
370
+ const { size: attributeSize } = (0, exports.calcStorageByteSize)(structAttribute, umlClass, otherClasses);
284
371
  // check if attribute will fit into the remaining slot
285
372
  const endCurrentSlot = Math.ceil(structByteSize / 32) * 32;
286
373
  const spaceLeftInSlot = endCurrentSlot - structByteSize;
@@ -292,24 +379,27 @@ const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
292
379
  }
293
380
  });
294
381
  // structs take whole 32 byte slots so round up to the nearest 32 sized slots
295
- return Math.ceil(structByteSize / 32) * 32;
382
+ return {
383
+ size: Math.ceil(structByteSize / 32) * 32,
384
+ dynamic: false,
385
+ };
296
386
  default:
297
- return 32;
387
+ return { size: 32, dynamic: false };
298
388
  }
299
389
  }
300
390
  if (attribute.attributeType === umlClass_1.AttributeType.Elementary) {
301
391
  switch (attribute.type) {
302
392
  case 'bool':
303
- return 1;
393
+ return { size: 1, dynamic: false };
304
394
  case 'address':
305
- return 20;
395
+ return { size: 20, dynamic: false };
306
396
  case 'string':
307
397
  case 'bytes':
308
398
  case 'uint':
309
399
  case 'int':
310
400
  case 'ufixed':
311
401
  case 'fixed':
312
- return 32;
402
+ return { size: 32, dynamic: false };
313
403
  default:
314
404
  const result = attribute.type.match(/[u]*(int|fixed|bytes)([0-9]+)/);
315
405
  if (result === null || !result[2]) {
@@ -317,12 +407,12 @@ const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
317
407
  }
318
408
  // If bytes
319
409
  if (result[1] === 'bytes') {
320
- return parseInt(result[2]);
410
+ return { size: parseInt(result[2]), dynamic: false };
321
411
  }
322
412
  // TODO need to handle fixed types when they are supported
323
413
  // If an int
324
414
  const bitSize = parseInt(result[2]);
325
- return bitSize / 8;
415
+ return { size: bitSize / 8, dynamic: false };
326
416
  }
327
417
  }
328
418
  throw new Error(`Failed to calc bytes size of attribute with name "${attribute.name}" and type ${attribute.type}`);
@@ -345,4 +435,44 @@ const isElementary = (type) => {
345
435
  }
346
436
  };
347
437
  exports.isElementary = isElementary;
438
+ const calcSlotKey = (variable) => {
439
+ if (variable.dynamic) {
440
+ return (0, utils_1.keccak256)((0, utils_1.toUtf8Bytes)(ethers_1.BigNumber.from(variable.fromSlot).toHexString()));
441
+ }
442
+ return ethers_1.BigNumber.from(variable.fromSlot).toHexString();
443
+ };
444
+ exports.calcSlotKey = calcSlotKey;
445
+ // recursively offset the slots numbers of a storage item
446
+ const offsetStorageSlots = (storage, slots, storages) => {
447
+ storage.variables.forEach((variable) => {
448
+ variable.fromSlot += slots;
449
+ variable.toSlot += slots;
450
+ if (variable.referenceStorageId) {
451
+ // recursively offset the referenced storage
452
+ const referenceStorage = storages.find((s) => s.id === variable.referenceStorageId);
453
+ if (!referenceStorage.arrayDynamic) {
454
+ (0, exports.offsetStorageSlots)(referenceStorage, slots, storages);
455
+ }
456
+ else {
457
+ referenceStorage.slotKey = (0, exports.calcSlotKey)(variable);
458
+ }
459
+ }
460
+ });
461
+ };
462
+ exports.offsetStorageSlots = offsetStorageSlots;
463
+ const findDimensionLength = (umlClass, dimension) => {
464
+ const dimensionNum = parseInt(dimension);
465
+ if (Number.isInteger(dimensionNum)) {
466
+ return dimensionNum;
467
+ }
468
+ else {
469
+ // Try and size array dimension from declared constants
470
+ const constant = umlClass.constants.find((constant) => constant.name === dimension);
471
+ if (!constant) {
472
+ throw Error(`Could not size fixed sized array with dimension "${dimension}"`);
473
+ }
474
+ return constant.value;
475
+ }
476
+ };
477
+ exports.findDimensionLength = findDimensionLength;
348
478
  //# sourceMappingURL=converterClasses2Storage.js.map
@@ -1,3 +1,7 @@
1
1
  import { Storage } from './converterClasses2Storage';
2
- export declare const convertStorages2Dot: (storages: Storage[]) => string;
3
- export declare function convertStorage2Dot(storage: Storage, dotString: string): string;
2
+ export declare const convertStorages2Dot: (storages: Storage[], options: {
3
+ data: boolean;
4
+ }) => string;
5
+ export declare function convertStorage2Dot(storage: Storage, dotString: string, options: {
6
+ data: boolean;
7
+ }): string;
@@ -3,22 +3,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.convertStorage2Dot = exports.convertStorages2Dot = void 0;
4
4
  const converterClasses2Storage_1 = require("./converterClasses2Storage");
5
5
  const debug = require('debug')('sol2uml');
6
- const convertStorages2Dot = (storages) => {
6
+ const convertStorages2Dot = (storages, options) => {
7
7
  let dotString = `
8
8
  digraph StorageDiagram {
9
9
  rankdir=LR
10
10
  color=black
11
11
  arrowhead=open
12
- node [shape=record, style=filled, fillcolor=gray95]`;
12
+ node [shape=record, style=filled, fillcolor=gray95 fontname="Courier New"]`;
13
13
  // process contract and the struct storages
14
14
  storages.forEach((storage) => {
15
- dotString = convertStorage2Dot(storage, dotString);
15
+ dotString = convertStorage2Dot(storage, dotString, options);
16
16
  });
17
17
  // link contract and structs to structs
18
18
  storages.forEach((slot) => {
19
19
  slot.variables.forEach((storage) => {
20
- if (storage.structStorageId) {
21
- dotString += `\n ${slot.id}:${storage.id} -> ${storage.structStorageId}`;
20
+ if (storage.referenceStorageId) {
21
+ dotString += `\n ${slot.id}:${storage.id} -> ${storage.referenceStorageId}`;
22
22
  }
23
23
  });
24
24
  });
@@ -28,10 +28,10 @@ node [shape=record, style=filled, fillcolor=gray95]`;
28
28
  return dotString;
29
29
  };
30
30
  exports.convertStorages2Dot = convertStorages2Dot;
31
- function convertStorage2Dot(storage, dotString) {
32
- const steorotype = storage.type === converterClasses2Storage_1.StorageType.Struct ? 'Struct' : 'Contract';
31
+ function convertStorage2Dot(storage, dotString, options) {
33
32
  // write storage header with name and optional address
34
- dotString += `\n${storage.id} [label="${storage.name} \\<\\<${steorotype}\\>\\>\\n${storage.address || ''} | {`;
33
+ dotString += `\n${storage.id} [label="${storage.name} \\<\\<${storage.type}\\>\\>\\n${storage.address || storage.slotKey || ''}`;
34
+ dotString += ' | {';
35
35
  const startingVariables = storage.variables.filter((s) => s.byteOffset === 0);
36
36
  // write slot numbers
37
37
  dotString += '{ slot';
@@ -44,10 +44,10 @@ function convertStorage2Dot(storage, dotString) {
44
44
  }
45
45
  });
46
46
  // write slot values if available
47
- if (startingVariables[0]?.values[0]) {
47
+ if (options.data) {
48
48
  dotString += '} | {value';
49
49
  startingVariables.forEach((variable, i) => {
50
- dotString += ` | ${variable.values[0]}`;
50
+ dotString += ` | ${variable.value || ''}`;
51
51
  });
52
52
  }
53
53
  const contractVariablePrefix = storage.type === converterClasses2Storage_1.StorageType.Contract ? '\\<inherited contract\\>.' : '';
@@ -58,6 +58,7 @@ function convertStorage2Dot(storage, dotString) {
58
58
  const slotVariables = storage.variables.filter((s) => s.fromSlot === variable.fromSlot);
59
59
  const usedBytes = slotVariables.reduce((acc, s) => acc + s.byteSize, 0);
60
60
  if (usedBytes < 32) {
61
+ // Create an unallocated variable for display purposes
61
62
  slotVariables.push({
62
63
  id: 0,
63
64
  fromSlot: variable.fromSlot,
@@ -65,9 +66,10 @@ function convertStorage2Dot(storage, dotString) {
65
66
  byteSize: 32 - usedBytes,
66
67
  byteOffset: usedBytes,
67
68
  type: 'unallocated',
69
+ dynamic: false,
70
+ noValue: true,
68
71
  contractName: variable.contractName,
69
72
  variable: '',
70
- values: [],
71
73
  });
72
74
  }
73
75
  const slotVariablesReversed = slotVariables.reverse();
@@ -88,7 +90,7 @@ function convertStorage2Dot(storage, dotString) {
88
90
  }
89
91
  exports.convertStorage2Dot = convertStorage2Dot;
90
92
  const dotVariable = (storage, contractName) => {
91
- const port = storage.structStorageId !== undefined ? `<${storage.id}>` : '';
93
+ const port = storage.referenceStorageId !== undefined ? `<${storage.id}>` : '';
92
94
  const contractNamePrefix = storage.contractName !== contractName ? `${storage.contractName}.` : '';
93
95
  const variable = storage.variable
94
96
  ? `: ${contractNamePrefix}${storage.variable}`
@@ -71,7 +71,14 @@ class EtherscanParser {
71
71
  const { files, contractName } = await this.getSourceCode(contractAddress);
72
72
  let solidityCode = '';
73
73
  files.forEach((file) => {
74
- solidityCode += file.code;
74
+ // comment out any import statements
75
+ // match whitespace before import
76
+ // and characters after import up to ;
77
+ // replace all in file and match across multiple lines
78
+ const removedImports = file.code.replace(/(\s)(import.*;)/gm, '$1/* $2 */');
79
+ // Rename SPDX-License-Identifier to SPDX--License-Identifier so the merged file will compile
80
+ const removedSPDX = removedImports.replace(/SPDX-/, 'SPDX--');
81
+ solidityCode += removedSPDX;
75
82
  });
76
83
  return {
77
84
  solidityCode,
package/lib/slotValues.js CHANGED
@@ -41,7 +41,7 @@ const getStorageValues = async (url, contractAddress, slots, blockTag = 'latest'
41
41
  }
42
42
  const responseData = response.data;
43
43
  const sortedResponses = responseData.sort((a, b) => bignumber_1.BigNumber.from(a.id).gt(b.id) ? 1 : -1);
44
- return sortedResponses.map((data) => data.result);
44
+ return sortedResponses.map((data) => '0x' + data.result.toUpperCase().slice(2));
45
45
  }
46
46
  catch (err) {
47
47
  throw new Error(`Failed to get ${slots.length} storage values for ${contractAddress} from ${url}`, { cause: err });
package/lib/sol2uml.js CHANGED
@@ -39,8 +39,9 @@ The Solidity code can be pulled from verified source code on Blockchain explorer
39
39
  'goerli',
40
40
  'sepolia',
41
41
  ])
42
- .default('mainnet'))
43
- .option('-k, --apiKey <key>', 'Etherscan, Polygonscan, BscScan or Arbiscan API key')
42
+ .default('mainnet')
43
+ .env('ETH_NETWORK'))
44
+ .addOption(new commander_1.Option('-k, --apiKey <key>', 'Etherscan, Polygonscan, BscScan or Arbiscan API key').env('SCAN_API_KEY'))
44
45
  .option('-v, --verbose', 'run with debugging statements', false);
45
46
  program
46
47
  .command('class', { isDefault: true })
@@ -75,18 +76,19 @@ If an Ethereum address with a 0x prefix is passed, the verified source code from
75
76
  ...command.parent._optionValues,
76
77
  ...options,
77
78
  };
78
- const { umlClasses, contractName } = await (0, parserGeneral_1.parserUmlClasses)(fileFolderAddress, combinedOptions);
79
+ let { umlClasses, contractName } = await (0, parserGeneral_1.parserUmlClasses)(fileFolderAddress, combinedOptions);
79
80
  let filteredUmlClasses = umlClasses;
80
81
  if (options.baseContractNames) {
81
82
  const baseContractNames = options.baseContractNames.split(',');
82
83
  filteredUmlClasses = (0, filterClasses_1.classesConnectedToBaseContracts)(umlClasses, baseContractNames, options.depth);
84
+ contractName = baseContractNames[0];
83
85
  }
84
86
  const dotString = (0, converterClasses2Dot_1.convertUmlClasses2Dot)(filteredUmlClasses, combinedOptions.clusterFolders, combinedOptions);
85
- await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, contractName, combinedOptions.outputFormat, combinedOptions.outputFileName);
87
+ await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, contractName || 'classDiagram', combinedOptions.outputFormat, combinedOptions.outputFileName);
86
88
  debug(`Finished generating UML`);
87
89
  }
88
90
  catch (err) {
89
- console.error(`Failed to generate UML diagram ${err}`);
91
+ console.error(`Failed to generate UML diagram\n${err.stack}`);
90
92
  }
91
93
  });
92
94
  program
@@ -132,11 +134,11 @@ program
132
134
  throw Error(`Could not find the "${contractName}" contract in list of parsed storages`);
133
135
  await (0, converterClasses2Storage_1.addStorageValues)(combinedOptions.url, storageAddress, storage, combinedOptions.blockNumber);
134
136
  }
135
- const dotString = (0, converterStorage2Dot_1.convertStorages2Dot)(storages);
136
- await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, contractName, combinedOptions.outputFormat, combinedOptions.outputFileName);
137
+ const dotString = (0, converterStorage2Dot_1.convertStorages2Dot)(storages, combinedOptions);
138
+ await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, contractName || 'storageDiagram', combinedOptions.outputFormat, combinedOptions.outputFileName);
137
139
  }
138
140
  catch (err) {
139
- console.error(`Failed to generate storage diagram ${err}`);
141
+ console.error(`Failed to generate storage diagram.\n${err.stack}`);
140
142
  }
141
143
  });
142
144
  program
@@ -144,16 +146,21 @@ program
144
146
  .description('get all verified source code for a contract from the Blockchain explorer into one local file')
145
147
  .argument('<contractAddress>', 'Contract address')
146
148
  .action(async (contractAddress, options, command) => {
147
- debug(`About to flatten ${contractAddress}`);
148
- const combinedOptions = {
149
- ...command.parent._optionValues,
150
- ...options,
151
- };
152
- const etherscanParser = new parserEtherscan_1.EtherscanParser(combinedOptions.apiKey, combinedOptions.network);
153
- const { solidityCode, contractName } = await etherscanParser.getSolidityCode(contractAddress);
154
- // Write Solidity to the contract address
155
- const outputFilename = combinedOptions.outputFileName || contractName;
156
- await (0, writerFiles_1.writeSolidity)(solidityCode, outputFilename);
149
+ try {
150
+ debug(`About to flatten ${contractAddress}`);
151
+ const combinedOptions = {
152
+ ...command.parent._optionValues,
153
+ ...options,
154
+ };
155
+ const etherscanParser = new parserEtherscan_1.EtherscanParser(combinedOptions.apiKey, combinedOptions.network);
156
+ const { solidityCode, contractName } = await etherscanParser.getSolidityCode(contractAddress);
157
+ // Write Solidity to the contract address
158
+ const outputFilename = combinedOptions.outputFileName || contractName;
159
+ await (0, writerFiles_1.writeSolidity)(solidityCode, outputFilename);
160
+ }
161
+ catch (err) {
162
+ console.error(`Failed to flatten files.\n${err.stack}`);
163
+ }
157
164
  });
158
165
  program.on('option:verbose', () => {
159
166
  debugControl.enable('sol2uml');
package/lib/umlClass.d.ts CHANGED
@@ -88,7 +88,7 @@ export declare class UmlClass implements ClassProperties {
88
88
  name: string;
89
89
  absolutePath: string;
90
90
  relativePath: string;
91
- imports?: Import[];
91
+ imports: Import[];
92
92
  stereotype?: ClassStereotype;
93
93
  constants: Constants[];
94
94
  attributes: Attribute[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sol2uml",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Unified Modeling Language (UML) class diagram generator for Solidity contracts",
5
5
  "main": "./lib/index.js",
6
6
  "types": "./lib/index.d.ts",