sol2uml 2.1.0 → 2.1.3

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
 
@@ -42,16 +44,16 @@ npm ls sol2uml -g
42
44
  ## Command Line Interface (CLI)
43
45
 
44
46
  ```
45
- $ sol2uml --help
46
47
  Usage: sol2uml [subcommand] <options>
47
48
  The three subcommands:
48
49
  * class: Generates a UML class diagram from Solidity source code. default
49
50
  * storage: Generates a diagram of a contract's storage slots.
50
- * flatten: Pulls verified source files from a Blockchain explorer into one, flat, local Solidity file.
51
+ * flatten: Merges verified source files from a Blockchain explorer into one local file.
51
52
 
52
53
  The Solidity code can be pulled from verified source code on Blockchain explorers like Etherscan or from local Solidity files.
53
54
 
54
55
  Options:
56
+ -V, --version output the version number
55
57
  -sf, --subfolders <value> number of subfolders that will be recursively searched for Solidity files. (default: all)
56
58
  -f, --outputFormat <value> output file format. (choices: "svg", "png", "dot", "all", default: "svg")
57
59
  -o, --outputFileName <value> output file name
@@ -71,7 +73,6 @@ Commands:
71
73
  ### Class usage
72
74
 
73
75
  ```
74
- $sol2uml class --help
75
76
  Usage: sol2uml class <fileFolderAddress> [options]
76
77
 
77
78
  Generates UML diagrams from Solidity source code.
@@ -108,20 +109,19 @@ Options:
108
109
  ### Storage usage
109
110
 
110
111
  ```
111
- Usage: sol2uml storage [options] <fileFolderAddress>
112
-
113
- Visually display a contract's storage slots.
112
+ Usage: sol2uml storage <fileFolderAddress> [options]
114
113
 
115
114
  WARNING: sol2uml does not use the Solidity compiler so may differ with solc. A known example is fixed-sized arrays declared with an expression will fail to be sized.
116
115
 
116
+ Visually display a contract's storage slots.
117
+
117
118
  Arguments:
118
119
  fileFolderAddress file name, base folder or contract address
119
120
 
120
121
  Options:
121
122
  -c, --contract <name> Contract name in local Solidity files. Not needed when using an address as the first argument as the contract name can be derived from Etherscan.
122
123
  -d, --data Gets the values in the storage slots from an Ethereum node. (default: false)
123
- -s, --storage <address> The address of the contract with the storage values. This will be different from the contract with the code if a proxy contract is used. This is not needed if `fileFolderAddress` is an address and
124
- the contract is not proxied.
124
+ -s, --storage <address> The address of the contract with the storage values. This will be different from the contract with the code if a proxy contract is used. This is not needed if `fileFolderAddress` is an address and the contract is not proxied.
125
125
  -u, --url <url> URL of the Ethereum node to get storage values if the `data` option is used. (default: "http://localhost:8545", env: NODE_URL)
126
126
  -bn, --block <number> Block number to get the contract storage values from. (default: "latest")
127
127
  -h, --help display help for command
@@ -130,17 +130,20 @@ Options:
130
130
  ### Flatten usage
131
131
 
132
132
  ```
133
- $sol2uml flatten --help
134
- Usage: sol2uml flatten [options] <contractAddress>
133
+ Usage: sol2uml flatten <contractAddress> [options]
134
+
135
+ In order for the merged code to compile, the following is done:
136
+ 1. File imports are commented out.
137
+ 2. "SPDX-License-Identifier" is renamed to "SPDX--License-Identifier".
138
+ 3. Contract dependencies are analysed so the files are merged in an order that will compile.
135
139
 
136
- get all verified source code for a contract from the Blockchain explorer into one local file
140
+ Merges verified source files for a contract from a Blockchain explorer into one local file.
137
141
 
138
142
  Arguments:
139
- contractAddress Contract address
143
+ contractAddress Contract address in hexadecimal format with a 0x prefix.
140
144
 
141
145
  Options:
142
146
  -h, --help display help for command
143
-
144
147
  ```
145
148
 
146
149
  ## UML Class diagram examples
@@ -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,120 @@ 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 firstVariable = {
166
+ id: variableId++,
167
+ fromSlot: 0,
168
+ toSlot: Math.floor((arrayItemSize - 1) / 32),
169
+ byteSize: arrayItemSize,
170
+ byteOffset: 0,
171
+ type: baseType,
172
+ dynamic,
173
+ noValue: false,
174
+ };
175
+ const variables = [firstVariable];
176
+ if (arrayLength > 1) {
177
+ for (let i = 1; i < arrayLength; i++) {
178
+ variables.push({
179
+ id: variableId++,
180
+ fromSlot: Math.floor((i * arrayItemSize) / 32),
181
+ toSlot: Math.floor(((i + 1) * arrayItemSize - 1) / 32),
182
+ byteSize: arrayItemSize,
183
+ byteOffset: (i * arrayItemSize) % 32,
184
+ type: baseType,
185
+ dynamic,
186
+ noValue: false,
187
+ });
188
+ }
189
+ }
190
+ // recursively add storage
191
+ if (baseAttributeType !== umlClass_1.AttributeType.Elementary) {
192
+ const referenceStorage = (0, exports.parseReferenceStorage)(baseAttribute, umlClass, otherClasses, storages);
193
+ firstVariable.referenceStorageId = referenceStorage?.id;
194
+ }
195
+ const newStorage = {
196
+ id: storageId++,
197
+ name: `${attribute.type}: ${attribute.name}`,
198
+ type: StorageType.Array,
199
+ arrayDynamic: dynamic,
200
+ arrayLength,
201
+ variables,
202
+ };
203
+ storages.push(newStorage);
204
+ return newStorage;
205
+ }
206
+ if (attribute.attributeType === umlClass_1.AttributeType.UserDefined) {
126
207
  // Is the user defined type linked to another Contract, Struct or Enum?
127
208
  const dependentClass = otherClasses.find(({ name }) => {
128
209
  return (name === attribute.type || name === attribute.type.split('.')[1]);
@@ -143,20 +224,13 @@ const parseStructStorage = (attribute, otherClasses, storages) => {
143
224
  }
144
225
  return undefined;
145
226
  }
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
227
+ if (attribute.attributeType === umlClass_1.AttributeType.Mapping) {
228
+ // get the UserDefined type from the mapping
149
229
  // note the mapping could be an array of Structs
150
230
  // 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+)\[/);
231
+ const result = attribute.type.match(/=\\>((?!mapping)\w*)[\\[]/);
232
+ // If mapping of user defined type
154
233
  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
234
  // Find UserDefined type
161
235
  const typeClass = otherClasses.find(({ name }) => name === result[1] || name === result[1].split('.')[1]);
162
236
  if (!typeClass) {
@@ -178,66 +252,78 @@ const parseStructStorage = (attribute, otherClasses, storages) => {
178
252
  }
179
253
  return undefined;
180
254
  };
181
- exports.parseStructStorage = parseStructStorage;
255
+ exports.parseReferenceStorage = parseReferenceStorage;
182
256
  // Calculates the storage size of an attribute in bytes
183
257
  const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
184
258
  if (attribute.attributeType === umlClass_1.AttributeType.Mapping ||
185
259
  attribute.attributeType === umlClass_1.AttributeType.Function) {
186
- return 32;
260
+ return { size: 32, dynamic: true };
187
261
  }
188
262
  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;
263
+ // Fixed sized arrays are read from right to left until there is a dynamic dimension
264
+ // eg address[][3][2] is a fixed size array that uses 6 slots.
265
+ // while address [2][] is a dynamic sized array.
266
+ const arrayDimensions = attribute.type.match(/\[\w*]/g);
267
+ // Remove first [ and last ] from each arrayDimensions
268
+ const dimensionsStr = arrayDimensions.map((a) => a.slice(1, -1));
269
+ // fixed-sized arrays are read from right to left so reverse the dimensions
270
+ const dimensionsStrReversed = dimensionsStr.reverse();
271
+ // read fixed-size dimensions until we get a dynamic array with no dimension
272
+ let dimension = dimensionsStrReversed.shift();
273
+ const fixedDimensions = [];
274
+ while (dimension && dimension !== '') {
275
+ const dimensionNum = (0, exports.findDimensionLength)(umlClass, dimension);
276
+ fixedDimensions.push(dimensionNum);
277
+ // read the next dimension for the next loop
278
+ dimension = dimensionsStrReversed.shift();
279
+ }
280
+ // If the first dimension is dynamic, ie []
281
+ if (fixedDimensions.length === 0) {
282
+ // dynamic arrays start at the keccak256 of the slot number
283
+ // the array length is stored in the 32 byte slot
284
+ return { size: 32, dynamic: true };
197
285
  }
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
286
  let elementSize;
287
+ const type = attribute.type.substring(0, attribute.type.indexOf('['));
214
288
  // If a fixed sized array
215
- if ((0, exports.isElementary)(result[1])) {
289
+ if ((0, exports.isElementary)(type)) {
216
290
  const elementAttribute = {
217
291
  attributeType: umlClass_1.AttributeType.Elementary,
218
- type: result[1],
292
+ type,
219
293
  name: 'element',
220
294
  };
221
- elementSize = (0, exports.calcStorageByteSize)(elementAttribute, umlClass, otherClasses);
295
+ ({ size: elementSize } = (0, exports.calcStorageByteSize)(elementAttribute, umlClass, otherClasses));
222
296
  }
223
297
  else {
224
298
  const elementAttribute = {
225
299
  attributeType: umlClass_1.AttributeType.UserDefined,
226
- type: result[1],
300
+ type,
227
301
  name: 'userDefined',
228
302
  };
229
- elementSize = (0, exports.calcStorageByteSize)(elementAttribute, umlClass, otherClasses);
303
+ ({ size: elementSize } = (0, exports.calcStorageByteSize)(elementAttribute, umlClass, otherClasses));
230
304
  }
231
305
  // Anything over 16 bytes, like an address, will take a whole 32 byte slot
232
306
  if (elementSize > 16 && elementSize < 32) {
233
307
  elementSize = 32;
234
308
  }
235
- const firstDimensionBytes = elementSize * dimensions[0];
236
- const firstDimensionSlotBytes = Math.ceil(firstDimensionBytes / 32) * 32;
237
- const remainingElements = dimensions
238
- .slice(1)
309
+ // If multi dimension, then the first element is 32 bytes
310
+ if (fixedDimensions.length < arrayDimensions.length) {
311
+ const totalDimensions = fixedDimensions.reduce((total, dimension) => total * dimension, 1);
312
+ return {
313
+ size: 32 * totalDimensions,
314
+ dynamic: false,
315
+ };
316
+ }
317
+ const lastItem = fixedDimensions.length - 1;
318
+ const lastDimensionBytes = elementSize * fixedDimensions[lastItem];
319
+ const lastDimensionSlotBytes = Math.ceil(lastDimensionBytes / 32) * 32;
320
+ const remainingDimensions = fixedDimensions
321
+ .slice(0, lastItem)
239
322
  .reduce((total, dimension) => total * dimension, 1);
240
- return firstDimensionSlotBytes * remainingElements;
323
+ return {
324
+ size: lastDimensionSlotBytes * remainingDimensions,
325
+ dynamic: false,
326
+ };
241
327
  }
242
328
  // If a Struct or Enum
243
329
  if (attribute.attributeType === umlClass_1.AttributeType.UserDefined) {
@@ -250,12 +336,12 @@ const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
250
336
  }
251
337
  switch (attributeClass.stereotype) {
252
338
  case umlClass_1.ClassStereotype.Enum:
253
- return 1;
339
+ return { size: 1, dynamic: false };
254
340
  case umlClass_1.ClassStereotype.Contract:
255
341
  case umlClass_1.ClassStereotype.Abstract:
256
342
  case umlClass_1.ClassStereotype.Interface:
257
343
  case umlClass_1.ClassStereotype.Library:
258
- return 20;
344
+ return { size: 20, dynamic: false };
259
345
  case umlClass_1.ClassStereotype.Struct:
260
346
  let structByteSize = 0;
261
347
  attributeClass.attributes.forEach((structAttribute) => {
@@ -280,7 +366,7 @@ const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
280
366
  structByteSize = Math.ceil(structByteSize / 32) * 32;
281
367
  }
282
368
  }
283
- const attributeSize = (0, exports.calcStorageByteSize)(structAttribute, umlClass, otherClasses);
369
+ const { size: attributeSize } = (0, exports.calcStorageByteSize)(structAttribute, umlClass, otherClasses);
284
370
  // check if attribute will fit into the remaining slot
285
371
  const endCurrentSlot = Math.ceil(structByteSize / 32) * 32;
286
372
  const spaceLeftInSlot = endCurrentSlot - structByteSize;
@@ -292,24 +378,27 @@ const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
292
378
  }
293
379
  });
294
380
  // structs take whole 32 byte slots so round up to the nearest 32 sized slots
295
- return Math.ceil(structByteSize / 32) * 32;
381
+ return {
382
+ size: Math.ceil(structByteSize / 32) * 32,
383
+ dynamic: false,
384
+ };
296
385
  default:
297
- return 32;
386
+ return { size: 32, dynamic: false };
298
387
  }
299
388
  }
300
389
  if (attribute.attributeType === umlClass_1.AttributeType.Elementary) {
301
390
  switch (attribute.type) {
302
391
  case 'bool':
303
- return 1;
392
+ return { size: 1, dynamic: false };
304
393
  case 'address':
305
- return 20;
394
+ return { size: 20, dynamic: false };
306
395
  case 'string':
307
396
  case 'bytes':
308
397
  case 'uint':
309
398
  case 'int':
310
399
  case 'ufixed':
311
400
  case 'fixed':
312
- return 32;
401
+ return { size: 32, dynamic: false };
313
402
  default:
314
403
  const result = attribute.type.match(/[u]*(int|fixed|bytes)([0-9]+)/);
315
404
  if (result === null || !result[2]) {
@@ -317,12 +406,12 @@ const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
317
406
  }
318
407
  // If bytes
319
408
  if (result[1] === 'bytes') {
320
- return parseInt(result[2]);
409
+ return { size: parseInt(result[2]), dynamic: false };
321
410
  }
322
411
  // TODO need to handle fixed types when they are supported
323
412
  // If an int
324
413
  const bitSize = parseInt(result[2]);
325
- return bitSize / 8;
414
+ return { size: bitSize / 8, dynamic: false };
326
415
  }
327
416
  }
328
417
  throw new Error(`Failed to calc bytes size of attribute with name "${attribute.name}" and type ${attribute.type}`);
@@ -345,4 +434,44 @@ const isElementary = (type) => {
345
434
  }
346
435
  };
347
436
  exports.isElementary = isElementary;
437
+ const calcSlotKey = (variable) => {
438
+ if (variable.dynamic) {
439
+ return (0, utils_1.keccak256)((0, utils_1.toUtf8Bytes)(ethers_1.BigNumber.from(variable.fromSlot).toHexString()));
440
+ }
441
+ return ethers_1.BigNumber.from(variable.fromSlot).toHexString();
442
+ };
443
+ exports.calcSlotKey = calcSlotKey;
444
+ // recursively offset the slots numbers of a storage item
445
+ const offsetStorageSlots = (storage, slots, storages) => {
446
+ storage.variables.forEach((variable) => {
447
+ variable.fromSlot += slots;
448
+ variable.toSlot += slots;
449
+ if (variable.referenceStorageId) {
450
+ // recursively offset the referenced storage
451
+ const referenceStorage = storages.find((s) => s.id === variable.referenceStorageId);
452
+ if (!referenceStorage.arrayDynamic) {
453
+ (0, exports.offsetStorageSlots)(referenceStorage, slots, storages);
454
+ }
455
+ else {
456
+ referenceStorage.slotKey = (0, exports.calcSlotKey)(variable);
457
+ }
458
+ }
459
+ });
460
+ };
461
+ exports.offsetStorageSlots = offsetStorageSlots;
462
+ const findDimensionLength = (umlClass, dimension) => {
463
+ const dimensionNum = parseInt(dimension);
464
+ if (Number.isInteger(dimensionNum)) {
465
+ return dimensionNum;
466
+ }
467
+ else {
468
+ // Try and size array dimension from declared constants
469
+ const constant = umlClass.constants.find((constant) => constant.name === dimension);
470
+ if (!constant) {
471
+ throw Error(`Could not size fixed sized array with dimension "${dimension}"`);
472
+ }
473
+ return constant.value;
474
+ }
475
+ };
476
+ exports.findDimensionLength = findDimensionLength;
348
477
  //# 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}`
@@ -1,6 +1,7 @@
1
1
  import { WeightedDiGraph } from 'js-graph-algorithms';
2
2
  import { UmlClass } from './umlClass';
3
3
  export declare const classesConnectedToBaseContracts: (umlClasses: UmlClass[], baseContractNames: string[], depth?: number) => UmlClass[];
4
- export declare const classesConnectedToBaseContract: (umlClasses: UmlClass[], baseContractName: string, graph: WeightedDiGraph, depth?: number) => {
4
+ export declare const classesConnectedToBaseContract: (umlClasses: UmlClass[], baseContractName: string, weightedDirectedGraph: WeightedDiGraph, depth?: number) => {
5
5
  [contractName: string]: UmlClass;
6
6
  };
7
+ export declare const topologicalSortClasses: (umlClasses: UmlClass[]) => UmlClass[];
@@ -1,21 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.classesConnectedToBaseContract = exports.classesConnectedToBaseContracts = void 0;
3
+ exports.topologicalSortClasses = exports.classesConnectedToBaseContract = exports.classesConnectedToBaseContracts = void 0;
4
4
  const js_graph_algorithms_1 = require("js-graph-algorithms");
5
5
  const associations_1 = require("./associations");
6
6
  const classesConnectedToBaseContracts = (umlClasses, baseContractNames, depth) => {
7
7
  let filteredUmlClasses = {};
8
- const graph = loadGraph(umlClasses);
8
+ const weightedDirectedGraph = loadWeightedDirectedGraph(umlClasses);
9
9
  for (const baseContractName of baseContractNames) {
10
10
  filteredUmlClasses = {
11
11
  ...filteredUmlClasses,
12
- ...(0, exports.classesConnectedToBaseContract)(umlClasses, baseContractName, graph, depth),
12
+ ...(0, exports.classesConnectedToBaseContract)(umlClasses, baseContractName, weightedDirectedGraph, depth),
13
13
  };
14
14
  }
15
15
  return Object.values(filteredUmlClasses);
16
16
  };
17
17
  exports.classesConnectedToBaseContracts = classesConnectedToBaseContracts;
18
- const classesConnectedToBaseContract = (umlClasses, baseContractName, graph, depth = 1000) => {
18
+ const classesConnectedToBaseContract = (umlClasses, baseContractName, weightedDirectedGraph, depth = 1000) => {
19
19
  // Find the base UML Class from the base contract name
20
20
  const baseUmlClass = umlClasses.find(({ name }) => {
21
21
  return name === baseContractName;
@@ -23,7 +23,7 @@ const classesConnectedToBaseContract = (umlClasses, baseContractName, graph, dep
23
23
  if (!baseUmlClass) {
24
24
  throw Error(`Failed to find base contract with name "${baseContractName}"`);
25
25
  }
26
- const dfs = new js_graph_algorithms_1.Dijkstra(graph, baseUmlClass.id);
26
+ const dfs = new js_graph_algorithms_1.Dijkstra(weightedDirectedGraph, baseUmlClass.id);
27
27
  // Get all the UML Classes that are connected to the base contract
28
28
  const filteredUmlClasses = {};
29
29
  for (const umlClass of umlClasses) {
@@ -34,8 +34,8 @@ const classesConnectedToBaseContract = (umlClasses, baseContractName, graph, dep
34
34
  return filteredUmlClasses;
35
35
  };
36
36
  exports.classesConnectedToBaseContract = classesConnectedToBaseContract;
37
- function loadGraph(umlClasses) {
38
- const graph = new js_graph_algorithms_1.WeightedDiGraph(umlClasses.length); // 6 is the number vertices in the graph
37
+ function loadWeightedDirectedGraph(umlClasses) {
38
+ const weightedDirectedGraph = new js_graph_algorithms_1.WeightedDiGraph(umlClasses.length); // the number vertices in the graph
39
39
  for (const sourceUmlClass of umlClasses) {
40
40
  for (const association of Object.values(sourceUmlClass.associations)) {
41
41
  // Find the first UML Class that matches the target class name
@@ -43,9 +43,34 @@ function loadGraph(umlClasses) {
43
43
  if (!targetUmlClass) {
44
44
  continue;
45
45
  }
46
- graph.addEdge(new js_graph_algorithms_1.Edge(sourceUmlClass.id, targetUmlClass.id, 1));
46
+ weightedDirectedGraph.addEdge(new js_graph_algorithms_1.Edge(sourceUmlClass.id, targetUmlClass.id, 1));
47
47
  }
48
48
  }
49
- return graph;
49
+ return weightedDirectedGraph;
50
50
  }
51
+ const topologicalSortClasses = (umlClasses) => {
52
+ const directedAcyclicGraph = loadDirectedAcyclicGraph(umlClasses);
53
+ const topologicalSort = new js_graph_algorithms_1.TopologicalSort(directedAcyclicGraph);
54
+ // Topological sort the class ids
55
+ const sortedUmlClassIds = topologicalSort.order().reverse();
56
+ const sortedUmlClasses = sortedUmlClassIds.map((umlClassId) =>
57
+ // Lookup the UmlClass for each class id
58
+ umlClasses.find((umlClass) => umlClass.id === umlClassId));
59
+ return sortedUmlClasses;
60
+ };
61
+ exports.topologicalSortClasses = topologicalSortClasses;
62
+ const loadDirectedAcyclicGraph = (umlClasses) => {
63
+ const directedAcyclicGraph = new js_graph_algorithms_1.DiGraph(umlClasses.length); // the number vertices in the graph
64
+ for (const sourceUmlClass of umlClasses) {
65
+ for (const association of Object.values(sourceUmlClass.associations)) {
66
+ // Find the first UML Class that matches the target class name
67
+ const targetUmlClass = (0, associations_1.findAssociatedClass)(association, sourceUmlClass, umlClasses);
68
+ if (!targetUmlClass) {
69
+ continue;
70
+ }
71
+ directedAcyclicGraph.addEdge(sourceUmlClass.id, targetUmlClass.id);
72
+ }
73
+ }
74
+ return directedAcyclicGraph;
75
+ };
51
76
  //# sourceMappingURL=filterClasses.js.map
@@ -7,6 +7,8 @@ exports.EtherscanParser = void 0;
7
7
  const axios_1 = __importDefault(require("axios"));
8
8
  const parser_1 = require("@solidity-parser/parser");
9
9
  const converterAST2Classes_1 = require("./converterAST2Classes");
10
+ const filterClasses_1 = require("./filterClasses");
11
+ const debug = require('debug')('sol2uml');
10
12
  const networks = [
11
13
  'mainnet',
12
14
  'ropsten',
@@ -69,9 +71,39 @@ class EtherscanParser {
69
71
  */
70
72
  async getSolidityCode(contractAddress) {
71
73
  const { files, contractName } = await this.getSourceCode(contractAddress);
74
+ // Parse the UmlClasses from the Solidity code in each file
75
+ let umlClasses = [];
76
+ for (const file of files) {
77
+ const node = await this.parseSourceCode(file.code);
78
+ const umlClass = (0, converterAST2Classes_1.convertAST2UmlClasses)(node, file.filename);
79
+ umlClasses = umlClasses.concat(umlClass);
80
+ }
81
+ // Sort the classes so dependent code is first
82
+ const topologicalSortedClasses = (0, filterClasses_1.topologicalSortClasses)(umlClasses);
83
+ // Get a list of filenames the classes are in
84
+ const sortedFilenames = topologicalSortedClasses.map((umlClass) => umlClass.relativePath);
85
+ // Remove duplicate filenames from the list
86
+ const dependentFilenames = [...new Set(sortedFilenames)];
87
+ // find any files that didn't have dependencies found
88
+ const nonDependentFiles = files.filter((f) => !dependentFilenames.includes(f.filename));
89
+ const nonDependentFilenames = nonDependentFiles.map((f) => f.filename);
72
90
  let solidityCode = '';
73
- files.forEach((file) => {
74
- solidityCode += file.code;
91
+ // output non dependent code before the dependent files just in case sol2uml missed some dependencies
92
+ const filenames = [...nonDependentFilenames, ...dependentFilenames];
93
+ // For each filename
94
+ filenames.forEach((filename) => {
95
+ // Lookup the file that contains the Solidity code
96
+ const file = files.find((f) => f.filename === filename);
97
+ if (!file)
98
+ throw Error(`Failed to find file with filename "${filename}"`);
99
+ // comment out any import statements
100
+ // match whitespace before import
101
+ // and characters after import up to ;
102
+ // replace all in file and match across multiple lines
103
+ const removedImports = file.code.replace(/(\s)(import.*;)/gm, '$1/* $2 */');
104
+ // Rename SPDX-License-Identifier to SPDX--License-Identifier so the merged file will compile
105
+ const removedSPDX = removedImports.replace(/SPDX-/, 'SPDX--');
106
+ solidityCode += removedSPDX;
75
107
  });
76
108
  return {
77
109
  solidityCode,
@@ -99,6 +131,7 @@ class EtherscanParser {
99
131
  async getSourceCode(contractAddress) {
100
132
  const description = `get verified source code for address ${contractAddress} from Etherscan API.`;
101
133
  try {
134
+ debug(`About to get Solidity source code for ${contractAddress} from ${this.url}`);
102
135
  const response = await axios_1.default.get(this.url, {
103
136
  params: {
104
137
  module: 'contract',
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
@@ -11,14 +11,16 @@ const converterStorage2Dot_1 = require("./converterStorage2Dot");
11
11
  const regEx_1 = require("./utils/regEx");
12
12
  const writerFiles_1 = require("./writerFiles");
13
13
  const program = new commander_1.Command();
14
+ program.version(require('../package.json').version);
14
15
  const debugControl = require('debug');
15
16
  const debug = require('debug')('sol2uml');
16
17
  program
17
18
  .usage(`[subcommand] <options>
18
- The three subcommands:
19
- * class: Generates a UML class diagram from Solidity source code. default
19
+
20
+ sol2uml comes with three subcommands:
21
+ * class: Generates a UML class diagram from Solidity source code. (default)
20
22
  * storage: Generates a diagram of a contract's storage slots.
21
- * flatten: Pulls verified source files from a Blockchain explorer into one, flat, local Solidity file.
23
+ * flatten: Merges verified source files from a Blockchain explorer into one local file.
22
24
 
23
25
  The Solidity code can be pulled from verified source code on Blockchain explorers like Etherscan or from local Solidity files.`)
24
26
  .addOption(new commander_1.Option('-sf, --subfolders <value>', 'number of subfolders that will be recursively searched for Solidity files.').default('-1', 'all'))
@@ -39,8 +41,9 @@ The Solidity code can be pulled from verified source code on Blockchain explorer
39
41
  'goerli',
40
42
  'sepolia',
41
43
  ])
42
- .default('mainnet'))
43
- .option('-k, --apiKey <key>', 'Etherscan, Polygonscan, BscScan or Arbiscan API key')
44
+ .default('mainnet')
45
+ .env('ETH_NETWORK'))
46
+ .addOption(new commander_1.Option('-k, --apiKey <key>', 'Etherscan, Polygonscan, BscScan or Arbiscan API key').env('SCAN_API_KEY'))
44
47
  .option('-v, --verbose', 'run with debugging statements', false);
45
48
  program
46
49
  .command('class', { isDefault: true })
@@ -75,23 +78,27 @@ If an Ethereum address with a 0x prefix is passed, the verified source code from
75
78
  ...command.parent._optionValues,
76
79
  ...options,
77
80
  };
78
- const { umlClasses, contractName } = await (0, parserGeneral_1.parserUmlClasses)(fileFolderAddress, combinedOptions);
81
+ let { umlClasses, contractName } = await (0, parserGeneral_1.parserUmlClasses)(fileFolderAddress, combinedOptions);
79
82
  let filteredUmlClasses = umlClasses;
80
83
  if (options.baseContractNames) {
81
84
  const baseContractNames = options.baseContractNames.split(',');
82
85
  filteredUmlClasses = (0, filterClasses_1.classesConnectedToBaseContracts)(umlClasses, baseContractNames, options.depth);
86
+ contractName = baseContractNames[0];
83
87
  }
84
88
  const dotString = (0, converterClasses2Dot_1.convertUmlClasses2Dot)(filteredUmlClasses, combinedOptions.clusterFolders, combinedOptions);
85
- await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, contractName, combinedOptions.outputFormat, combinedOptions.outputFileName);
89
+ await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, contractName || 'classDiagram', combinedOptions.outputFormat, combinedOptions.outputFileName);
86
90
  debug(`Finished generating UML`);
87
91
  }
88
92
  catch (err) {
89
- console.error(`Failed to generate UML diagram ${err}`);
93
+ console.error(`Failed to generate UML diagram\n${err.stack}`);
90
94
  }
91
95
  });
92
96
  program
93
97
  .command('storage')
94
- .description("Visually display a contract's storage slots.\n\nWARNING: sol2uml does not use the Solidity compiler so may differ with solc. A known example is fixed-sized arrays declared with an expression will fail to be sized.")
98
+ .description("Visually display a contract's storage slots.")
99
+ .usage(`<fileFolderAddress> [options]
100
+
101
+ WARNING: sol2uml does not use the Solidity compiler so may differ with solc. A known example is fixed-sized arrays declared with an expression will fail to be sized.`)
95
102
  .argument('<fileFolderAddress>', 'file name, base folder or contract address')
96
103
  .option('-c, --contract <name>', 'Contract name in local Solidity files. Not needed when using an address as the first argument as the contract name can be derived from Etherscan.')
97
104
  .option('-d, --data', 'Gets the values in the storage slots from an Ethereum node.', false)
@@ -132,28 +139,39 @@ program
132
139
  throw Error(`Could not find the "${contractName}" contract in list of parsed storages`);
133
140
  await (0, converterClasses2Storage_1.addStorageValues)(combinedOptions.url, storageAddress, storage, combinedOptions.blockNumber);
134
141
  }
135
- const dotString = (0, converterStorage2Dot_1.convertStorages2Dot)(storages);
136
- await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, contractName, combinedOptions.outputFormat, combinedOptions.outputFileName);
142
+ const dotString = (0, converterStorage2Dot_1.convertStorages2Dot)(storages, combinedOptions);
143
+ await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, contractName || 'storageDiagram', combinedOptions.outputFormat, combinedOptions.outputFileName);
137
144
  }
138
145
  catch (err) {
139
- console.error(`Failed to generate storage diagram ${err}`);
146
+ console.error(`Failed to generate storage diagram.\n${err.stack}`);
140
147
  }
141
148
  });
142
149
  program
143
150
  .command('flatten')
144
- .description('get all verified source code for a contract from the Blockchain explorer into one local file')
145
- .argument('<contractAddress>', 'Contract address')
151
+ .description('Merges verified source files for a contract from a Blockchain explorer into one local file.')
152
+ .usage(`<contractAddress> [options]
153
+
154
+ In order for the merged code to compile, the following is done:
155
+ 1. File imports are commented out.
156
+ 2. "SPDX-License-Identifier" is renamed to "SPDX--License-Identifier".
157
+ 3. Contract dependencies are analysed so the files are merged in an order that will compile.`)
158
+ .argument('<contractAddress>', 'Contract address in hexadecimal format with a 0x prefix.')
146
159
  .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);
160
+ try {
161
+ debug(`About to flatten ${contractAddress}`);
162
+ const combinedOptions = {
163
+ ...command.parent._optionValues,
164
+ ...options,
165
+ };
166
+ const etherscanParser = new parserEtherscan_1.EtherscanParser(combinedOptions.apiKey, combinedOptions.network);
167
+ const { solidityCode, contractName } = await etherscanParser.getSolidityCode(contractAddress);
168
+ // Write Solidity to the contract address
169
+ const outputFilename = combinedOptions.outputFileName || contractName;
170
+ await (0, writerFiles_1.writeSolidity)(solidityCode, outputFilename);
171
+ }
172
+ catch (err) {
173
+ console.error(`Failed to flatten files.\n${err.stack}`);
174
+ }
157
175
  });
158
176
  program.on('option:verbose', () => {
159
177
  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.3",
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",