sol2uml 2.0.3 → 2.1.0

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020 Nick Addison
3
+ Copyright (c) 2022 Nick Addison
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -20,16 +20,19 @@ The following installation assumes [Node.js](https://nodejs.org/en/download/) ha
20
20
  `sol2uml` works with node 14 or above.
21
21
 
22
22
  To install globally so you can run `sol2uml` from anywhere
23
+
23
24
  ```bash
24
25
  npm link sol2uml --only=production
25
26
  ```
26
27
 
27
28
  To upgrade run
29
+
28
30
  ```bash
29
31
  npm upgrade sol2uml -g
30
32
  ```
31
33
 
32
34
  To see which version you are using
35
+
33
36
  ```bash
34
37
  npm ls sol2uml -g
35
38
  ```
@@ -109,15 +112,19 @@ Usage: sol2uml storage [options] <fileFolderAddress>
109
112
 
110
113
  Visually display a contract's storage slots.
111
114
 
112
- WARNING: sol2uml does not use the Solidity compiler so may differ with solc. A known example is storage arrays declared with a constant, immutable or expression will show as only taking one slot but it could be more. Storage arrays declared with an integer work.
115
+ 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.
113
116
 
114
117
  Arguments:
115
- fileFolderAddress file name, base folder or contract address
118
+ fileFolderAddress file name, base folder or contract address
116
119
 
117
120
  Options:
118
- -c, --contractName <value> Contract name in local Solidity files. Not needed when using an address as the first argument.
119
- -h, --help display help for command
120
-
121
+ -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
+ -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.
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
+ -bn, --block <number> Block number to get the contract storage values from. (default: "latest")
127
+ -h, --help display help for command
121
128
  ```
122
129
 
123
130
  ### Flatten usage
@@ -139,36 +146,43 @@ Options:
139
146
  ## UML Class diagram examples
140
147
 
141
148
  To generate a diagram of all contracts under the contracts folder and its sub folders
149
+
142
150
  ```bash
143
151
  sol2uml class ./contracts
144
152
  ```
145
153
 
146
154
  To generate a diagram of EtherDelta's contract from the verified source code on [Etherscan](https://etherscan.io/address/0x8d12A197cB00D4747a1fe03395095ce2A5CC6819#code). The output wil be a svg file `0x8d12A197cB00D4747a1fe03395095ce2A5CC6819.svg` in the working folder.
155
+
147
156
  ```bash
148
157
  sol2uml class 0x8d12A197cB00D4747a1fe03395095ce2A5CC6819
149
158
  ```
150
159
 
151
160
  To generate a diagram of EtherDelta's contract from the verified source code on [Etherscan Ropsten](https://ropsten.etherscan.io/address/0xa19833bd291b66aB0E17b9C6d46D2Ec5fEC15190#code). The output wil be a svg file `0xa19833bd291b66aB0E17b9C6d46D2Ec5fEC15190.svg` in the working folder.
161
+
152
162
  ```bash
153
163
  sol2uml class 0xa19833bd291b66aB0E17b9C6d46D2Ec5fEC15190 -n ropsten
154
164
  ```
155
165
 
156
166
  To generate all Solidity files under some root folder and output the svg file to a specific location
167
+
157
168
  ```bash
158
169
  sol2uml class path/to/contracts/root/folder -o ./outputFile.svg
159
170
  ```
160
171
 
161
172
  To generate a diagram of all contracts in a single Solidity file, the output file in png format to output file `./someFile.png`
173
+
162
174
  ```bash
163
175
  sol2uml class path/to/contracts/root/folder/solidity/file.sol -f png -o ./someFile.png
164
176
  ```
165
177
 
166
- To generate a diagram of all Solidity files under the `contracts` and `node_modules/openzeppelin-solidity` folders. The output will be `contracts.svg` and `contracts.png` files in the working folder.
178
+ To generate a diagram of all Solidity files under the `contracts` and `node_modules/openzeppelin-solidity` folders. The output will be `contracts.svg` and `contracts.png` files in the working folder.
179
+
167
180
  ```bash
168
181
  sol2uml class ./contracts,node_modules/openzeppelin-solidity -f all -v
169
182
  ```
170
183
 
171
184
  To generate a diagram of all Solidity files under the working folder ignoring and files under the `solparse`, `@solidity-parser` and `ethlint` folders, which will be under the `node_modules` folder.
185
+
172
186
  ```bash
173
187
  sol2uml class -i solparse,@solidity-parser,ethlint
174
188
  ```
@@ -176,8 +190,9 @@ sol2uml class -i solparse,@solidity-parser,ethlint
176
190
  # UML Class Diagram Syntax
177
191
 
178
192
  Good online resources for learning UML
179
- * [UML 2 Class Diagramming Guidelines](http://www.agilemodeling.com/style/classDiagram.htm)
180
- * [Creating class diagrams with UML](https://www.ionos.com/digitalguide/websites/web-development/class-diagrams-with-uml/)
193
+
194
+ - [UML 2 Class Diagramming Guidelines](http://www.agilemodeling.com/style/classDiagram.htm)
195
+ - [Creating class diagrams with UML](https://www.ionos.com/digitalguide/websites/web-development/class-diagrams-with-uml/)
181
196
 
182
197
  ## Terminology differences
183
198
 
@@ -187,33 +202,35 @@ A Solidity variable becomes an attribute in UML and a Solidity function becomes
187
202
 
188
203
  ### Class stereotypes
189
204
 
190
- * Interface
191
- * Abstract - if any of the contract's functions are abstract, the class will have an Abstract stereotype. Child contracts of abstract contracts that do not implement all the abstract functions are currently not marked as Abstract.
192
- * Library
205
+ - Interface
206
+ - Abstract - if any of the contract's functions are abstract, the class will have an Abstract stereotype. Child contracts of abstract contracts that do not implement all the abstract functions are currently not marked as Abstract.
207
+ - Library
193
208
 
194
209
  ### Operator stereotypes
195
210
 
196
- * event
197
- * modifier
198
- * abstract - if there is no function body on a contract, the operator is marked as abstract. Operators on an Interface do not have an abstract stereotype as all operators are abstract.
199
- * fallback - abstract fallback functions will just have an abstract stereotype.
200
- * payable - payable fallback functions will just have a fallback stereotype.
211
+ - event
212
+ - modifier
213
+ - abstract - if there is no function body on a contract, the operator is marked as abstract. Operators on an Interface do not have an abstract stereotype as all operators are abstract.
214
+ - fallback - abstract fallback functions will just have an abstract stereotype.
215
+ - payable - payable fallback functions will just have a fallback stereotype.
201
216
 
202
217
  ## UML Associations
203
218
 
204
219
  Lines:
205
- - Solid lines for
206
- - link the contract types of storage (state) variables. This can be linked to contracts, interfaces, libraries or file level structs and enums.
207
- - generalisations of contracts and abstract contracts.
208
- - aggregated contract level structs and enums.
209
- - Dashed lines for
210
- - generalisations of interfaces.
211
- - types of memory variables.
220
+
221
+ - Solid lines for
222
+ - link the contract types of storage (state) variables. This can be linked to contracts, interfaces, libraries or file level structs and enums.
223
+ - generalisations of contracts and abstract contracts.
224
+ - aggregated contract level structs and enums.
225
+ - Dashed lines for
226
+ - generalisations of interfaces.
227
+ - types of memory variables.
212
228
 
213
229
  Heads/Tails:
214
- - An empty triangle head for generalisations of contracts, interfaces and abstract contracts.
215
- - An open arrow head for storage or memory variable dependencies
216
- - A diamond tail for aggregations of contract level structs and enums
230
+
231
+ - An empty triangle head for generalisations of contracts, interfaces and abstract contracts.
232
+ - An open arrow head for storage or memory variable dependencies
233
+ - A diamond tail for aggregations of contract level structs and enums
217
234
 
218
235
  ## Storage diagram
219
236
 
@@ -98,7 +98,9 @@ function convertAST2UmlClasses(node, relativePath, filesystem = false) {
98
98
  }
99
99
  else {
100
100
  // this has come from Etherscan
101
- const importPath = path.join(codeFolder, childNode.path);
101
+ const importPath = childNode.path[0] === '@'
102
+ ? childNode.path
103
+ : path.join(codeFolder, childNode.path);
102
104
  imports.push({
103
105
  absolutePath: importPath,
104
106
  classNames: childNode.symbolAliases
@@ -172,6 +174,16 @@ function parseContractDefinition(umlClass, node) {
172
174
  attributeType,
173
175
  compiled: valueStore,
174
176
  });
177
+ // Is the variable a constant that could be used in declaring fixed sized arrays
178
+ if (variable.isDeclaredConst) {
179
+ if (variable?.expression.type === 'NumberLiteral') {
180
+ umlClass.constants.push({
181
+ name: variable.name,
182
+ value: parseInt(variable.expression.number),
183
+ });
184
+ }
185
+ // TODO handle expressions. eg N_COINS * 2
186
+ }
175
187
  });
176
188
  // Recursively parse variables for associations
177
189
  umlClass = addAssociations(subNode.variables, umlClass);
@@ -3,7 +3,7 @@ export declare enum StorageType {
3
3
  Contract = 0,
4
4
  Struct = 1
5
5
  }
6
- export interface Storage {
6
+ export interface Variable {
7
7
  id: number;
8
8
  fromSlot: number;
9
9
  toSlot: number;
@@ -12,18 +12,25 @@ export interface Storage {
12
12
  type: string;
13
13
  variable: string;
14
14
  contractName?: string;
15
- value?: string;
16
- structObjectId?: number;
15
+ values: string[];
16
+ structStorageId?: number;
17
17
  enumId?: number;
18
18
  }
19
- export interface StorageObject {
19
+ export interface Storage {
20
20
  id: number;
21
21
  name: string;
22
22
  address?: string;
23
23
  type: StorageType;
24
- storages: Storage[];
24
+ variables: Variable[];
25
25
  }
26
- export declare const convertClasses2StorageObjects: (contractName: string, umlClasses: UmlClass[]) => StorageObject[];
27
- export declare const parseStructStorageObject: (attribute: Attribute, otherClasses: UmlClass[], storageObjects: StorageObject[]) => StorageObject | undefined;
26
+ /**
27
+ *
28
+ * @param url
29
+ * @param contractAddress Contract address to get the storage slot values from
30
+ * @param storage is mutated with the storage values
31
+ */
32
+ export declare const addStorageValues: (url: string, contractAddress: string, storage: Storage, blockTag: string) => Promise<void>;
33
+ export declare const convertClasses2Storages: (contractName: string, umlClasses: UmlClass[]) => Storage[];
34
+ export declare const parseStructStorage: (attribute: Attribute, otherClasses: UmlClass[], storages: Storage[]) => Storage | undefined;
28
35
  export declare const calcStorageByteSize: (attribute: Attribute, umlClass: UmlClass, otherClasses: UmlClass[]) => number;
29
36
  export declare const isElementary: (type: string) => boolean;
@@ -1,16 +1,31 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isElementary = exports.calcStorageByteSize = exports.parseStructStorageObject = exports.convertClasses2StorageObjects = exports.StorageType = void 0;
3
+ exports.isElementary = exports.calcStorageByteSize = exports.parseStructStorage = exports.convertClasses2Storages = exports.addStorageValues = exports.StorageType = void 0;
4
4
  const umlClass_1 = require("./umlClass");
5
5
  const associations_1 = require("./associations");
6
+ const slotValues_1 = require("./slotValues");
6
7
  var StorageType;
7
8
  (function (StorageType) {
8
9
  StorageType[StorageType["Contract"] = 0] = "Contract";
9
10
  StorageType[StorageType["Struct"] = 1] = "Struct";
10
11
  })(StorageType = exports.StorageType || (exports.StorageType = {}));
11
- let storageObjectId = 1;
12
12
  let storageId = 1;
13
- const convertClasses2StorageObjects = (contractName, umlClasses) => {
13
+ let variableId = 1;
14
+ /**
15
+ *
16
+ * @param url
17
+ * @param contractAddress Contract address to get the storage slot values from
18
+ * @param storage is mutated with the storage values
19
+ */
20
+ const addStorageValues = async (url, contractAddress, storage, blockTag) => {
21
+ const slots = storage.variables.map((s) => s.fromSlot);
22
+ const values = await (0, slotValues_1.getStorageValues)(url, contractAddress, slots, blockTag);
23
+ storage.variables.forEach((storage, i) => {
24
+ storage.values = [values[i]];
25
+ });
26
+ };
27
+ exports.addStorageValues = addStorageValues;
28
+ const convertClasses2Storages = (contractName, umlClasses) => {
14
29
  // Find the base UML Class from the base contract name
15
30
  const umlClass = umlClasses.find(({ name }) => {
16
31
  return name === contractName;
@@ -18,25 +33,25 @@ const convertClasses2StorageObjects = (contractName, umlClasses) => {
18
33
  if (!umlClass) {
19
34
  throw Error(`Failed to find contract with name "${contractName}"`);
20
35
  }
21
- const storageObjects = [];
22
- const storages = parseStorage(umlClass, umlClasses, [], storageObjects, []);
23
- storageObjects.unshift({
24
- id: storageObjectId++,
36
+ const storages = [];
37
+ const variables = parseVariables(umlClass, umlClasses, [], storages, []);
38
+ storages.unshift({
39
+ id: storageId++,
25
40
  name: contractName,
26
41
  type: StorageType.Contract,
27
- storages,
42
+ variables: variables,
28
43
  });
29
- return storageObjects;
44
+ return storages;
30
45
  };
31
- exports.convertClasses2StorageObjects = convertClasses2StorageObjects;
46
+ exports.convertClasses2Storages = convertClasses2Storages;
32
47
  /**
33
- * Recursively parses the storage for a given contract.
48
+ * Recursively parses the storage variables for a given contract.
34
49
  * @param umlClass contract or file level struct
35
50
  * @param umlClasses other contracts, structs and enums that may be a type of a storage variable.
36
- * @param storages mutable array of storage slots that is appended to
37
- * @param storageObjects mutable array of StorageObjects that is appended with structs
51
+ * @param variables mutable array of storage slots that is appended to
52
+ * @param storages mutable array of storages that is appended with structs
38
53
  */
39
- const parseStorage = (umlClass, umlClasses, storages, storageObjects, inheritedContracts) => {
54
+ const parseVariables = (umlClass, umlClasses, variables, storages, inheritedContracts) => {
40
55
  // Add storage slots from inherited contracts first.
41
56
  // Get immediate parent contracts that the class inherits from
42
57
  const parentContracts = umlClass.getParentContracts();
@@ -50,7 +65,7 @@ const parseStorage = (umlClass, umlClasses, storages, storageObjects, inheritedC
50
65
  if (!parentClass)
51
66
  throw Error(`Failed to find parent contract ${parent.targetUmlClassName} of ${umlClass.absolutePath}`);
52
67
  // recursively parse inherited contract
53
- parseStorage(parentClass, umlClasses, storages, storageObjects, inheritedContracts);
68
+ parseVariables(parentClass, umlClasses, variables, storages, inheritedContracts);
54
69
  });
55
70
  // Parse storage for each attribute
56
71
  umlClass.attributes.forEach((attribute) => {
@@ -59,20 +74,20 @@ const parseStorage = (umlClass, umlClasses, storages, storageObjects, inheritedC
59
74
  return;
60
75
  const byteSize = (0, exports.calcStorageByteSize)(attribute, umlClass, umlClasses);
61
76
  // find any dependent structs
62
- const linkedStruct = (0, exports.parseStructStorageObject)(attribute, umlClasses, storageObjects);
63
- const structObjectId = linkedStruct?.id;
77
+ const linkedStruct = (0, exports.parseStructStorage)(attribute, umlClasses, storages);
78
+ const structStorageId = linkedStruct?.id;
64
79
  // Get the toSlot of the last storage item
65
80
  let lastToSlot = 0;
66
81
  let nextOffset = 0;
67
- if (storages.length > 0) {
68
- const lastStorage = storages[storages.length - 1];
82
+ if (variables.length > 0) {
83
+ const lastStorage = variables[variables.length - 1];
69
84
  lastToSlot = lastStorage.toSlot;
70
85
  nextOffset = lastStorage.byteOffset + lastStorage.byteSize;
71
86
  }
72
87
  if (nextOffset + byteSize > 32) {
73
- const nextFromSlot = storages.length > 0 ? lastToSlot + 1 : 0;
74
- storages.push({
75
- id: storageId++,
88
+ const nextFromSlot = variables.length > 0 ? lastToSlot + 1 : 0;
89
+ variables.push({
90
+ id: variableId++,
76
91
  fromSlot: nextFromSlot,
77
92
  toSlot: nextFromSlot + Math.floor((byteSize - 1) / 32),
78
93
  byteSize,
@@ -80,12 +95,13 @@ const parseStorage = (umlClass, umlClasses, storages, storageObjects, inheritedC
80
95
  type: attribute.type,
81
96
  variable: attribute.name,
82
97
  contractName: umlClass.name,
83
- structObjectId,
98
+ structStorageId,
99
+ values: [],
84
100
  });
85
101
  }
86
102
  else {
87
- storages.push({
88
- id: storageId++,
103
+ variables.push({
104
+ id: variableId++,
89
105
  fromSlot: lastToSlot,
90
106
  toSlot: lastToSlot,
91
107
  byteSize,
@@ -93,18 +109,19 @@ const parseStorage = (umlClass, umlClasses, storages, storageObjects, inheritedC
93
109
  type: attribute.type,
94
110
  variable: attribute.name,
95
111
  contractName: umlClass.name,
96
- structObjectId,
112
+ structStorageId,
113
+ values: [],
97
114
  });
98
115
  }
99
116
  });
100
- return storages;
117
+ return variables;
101
118
  };
102
- const parseStructStorageObject = (attribute, otherClasses, storageObjects) => {
119
+ const parseStructStorage = (attribute, otherClasses, storages) => {
103
120
  if (attribute.attributeType === umlClass_1.AttributeType.UserDefined) {
104
- // Have we already created the storageObject?
105
- const existingStorageObject = storageObjects.find((dep) => dep.name === attribute.type);
106
- if (existingStorageObject) {
107
- return existingStorageObject;
121
+ // Have we already created the storage?
122
+ const existingStorage = storages.find((dep) => dep.name === attribute.type);
123
+ if (existingStorage) {
124
+ return existingStorage;
108
125
  }
109
126
  // Is the user defined type linked to another Contract, Struct or Enum?
110
127
  const dependentClass = otherClasses.find(({ name }) => {
@@ -114,15 +131,15 @@ const parseStructStorageObject = (attribute, otherClasses, storageObjects) => {
114
131
  throw Error(`Failed to find user defined type "${attribute.type}"`);
115
132
  }
116
133
  if (dependentClass.stereotype === umlClass_1.ClassStereotype.Struct) {
117
- const storages = parseStorage(dependentClass, otherClasses, [], storageObjects, []);
118
- const newStorageObject = {
119
- id: storageObjectId++,
134
+ const variables = parseVariables(dependentClass, otherClasses, [], storages, []);
135
+ const newStorage = {
136
+ id: storageId++,
120
137
  name: attribute.type,
121
138
  type: StorageType.Struct,
122
- storages,
139
+ variables,
123
140
  };
124
- storageObjects.push(newStorageObject);
125
- return newStorageObject;
141
+ storages.push(newStorage);
142
+ return newStorage;
126
143
  }
127
144
  return undefined;
128
145
  }
@@ -135,10 +152,10 @@ const parseStructStorageObject = (attribute, otherClasses, storageObjects) => {
135
152
  ? attribute.type.match(/=\\>((?!mapping)\w*)[\\[]/)
136
153
  : attribute.type.match(/(\w+)\[/);
137
154
  if (result !== null && result[1] && !(0, exports.isElementary)(result[1])) {
138
- // Have we already created the storageObject?
139
- const existingStorageObject = storageObjects.find(({ name }) => name === result[1] || name === result[1].split('.')[1]);
140
- if (existingStorageObject) {
141
- return existingStorageObject;
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;
142
159
  }
143
160
  // Find UserDefined type
144
161
  const typeClass = otherClasses.find(({ name }) => name === result[1] || name === result[1].split('.')[1]);
@@ -146,22 +163,22 @@ const parseStructStorageObject = (attribute, otherClasses, storageObjects) => {
146
163
  throw Error(`Failed to find user defined type "${result[1]}" in attribute type "${attribute.type}"`);
147
164
  }
148
165
  if (typeClass.stereotype === umlClass_1.ClassStereotype.Struct) {
149
- const storages = parseStorage(typeClass, otherClasses, [], storageObjects, []);
150
- const newStorageObject = {
151
- id: storageObjectId++,
166
+ const variables = parseVariables(typeClass, otherClasses, [], storages, []);
167
+ const newStorage = {
168
+ id: storageId++,
152
169
  name: typeClass.name,
153
170
  type: StorageType.Struct,
154
- storages,
171
+ variables,
155
172
  };
156
- storageObjects.push(newStorageObject);
157
- return newStorageObject;
173
+ storages.push(newStorage);
174
+ return newStorage;
158
175
  }
159
176
  }
160
177
  return undefined;
161
178
  }
162
179
  return undefined;
163
180
  };
164
- exports.parseStructStorageObject = parseStructStorageObject;
181
+ exports.parseStructStorage = parseStructStorage;
165
182
  // Calculates the storage size of an attribute in bytes
166
183
  const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
167
184
  if (attribute.attributeType === umlClass_1.AttributeType.Mapping ||
@@ -170,7 +187,7 @@ const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
170
187
  }
171
188
  if (attribute.attributeType === umlClass_1.AttributeType.Array) {
172
189
  // All array dimensions must be fixed. eg [2][3][8].
173
- const result = attribute.type.match(/(\w+)(\[([1-9][0-9]*)\])+$/);
190
+ const result = attribute.type.match(/(\w+)(\[([\w][\w]*)\])+$/);
174
191
  // The above will not match any dynamic array dimensions, eg [],
175
192
  // as there needs to be one or more [0-9]+ in the square brackets
176
193
  if (result === null) {
@@ -180,9 +197,19 @@ const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
180
197
  }
181
198
  // All array dimensions are fixes so we now need to multiply all the dimensions
182
199
  // to get a total number of array elements
183
- const arrayDimensions = attribute.type.match(/\[\d+/g);
200
+ const arrayDimensions = attribute.type.match(/\[\w+/g);
184
201
  const dimensionsStr = arrayDimensions.map((d) => d.slice(1));
185
- const dimensions = dimensionsStr.map((d) => parseInt(d));
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
+ });
186
213
  let elementSize;
187
214
  // If a fixed sized array
188
215
  if ((0, exports.isElementary)(result[1])) {
@@ -1,3 +1,3 @@
1
- import { StorageObject } from './converterClasses2Storage';
2
- export declare const convertStorage2Dot: (storageObjects: StorageObject[]) => string;
3
- export declare function convertStorageObject2Dot(storageObject: StorageObject, dotString: string): string;
1
+ import { Storage } from './converterClasses2Storage';
2
+ export declare const convertStorages2Dot: (storages: Storage[]) => string;
3
+ export declare function convertStorage2Dot(storage: Storage, dotString: string): string;
@@ -1,24 +1,24 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.convertStorageObject2Dot = exports.convertStorage2Dot = void 0;
3
+ exports.convertStorage2Dot = exports.convertStorages2Dot = void 0;
4
4
  const converterClasses2Storage_1 = require("./converterClasses2Storage");
5
5
  const debug = require('debug')('sol2uml');
6
- const convertStorage2Dot = (storageObjects) => {
6
+ const convertStorages2Dot = (storages) => {
7
7
  let dotString = `
8
8
  digraph StorageDiagram {
9
9
  rankdir=LR
10
10
  color=black
11
11
  arrowhead=open
12
12
  node [shape=record, style=filled, fillcolor=gray95]`;
13
- // process contract and the struct objects
14
- storageObjects.forEach((storageObject) => {
15
- dotString = convertStorageObject2Dot(storageObject, dotString);
13
+ // process contract and the struct storages
14
+ storages.forEach((storage) => {
15
+ dotString = convertStorage2Dot(storage, dotString);
16
16
  });
17
17
  // link contract and structs to structs
18
- storageObjects.forEach((slot) => {
19
- slot.storages.forEach((storage) => {
20
- if (storage.structObjectId) {
21
- dotString += `\n ${slot.id}:${storage.id} -> ${storage.structObjectId}`;
18
+ storages.forEach((slot) => {
19
+ slot.variables.forEach((storage) => {
20
+ if (storage.structStorageId) {
21
+ dotString += `\n ${slot.id}:${storage.id} -> ${storage.structStorageId}`;
22
22
  }
23
23
  });
24
24
  });
@@ -27,63 +27,72 @@ node [shape=record, style=filled, fillcolor=gray95]`;
27
27
  debug(dotString);
28
28
  return dotString;
29
29
  };
30
- exports.convertStorage2Dot = convertStorage2Dot;
31
- function convertStorageObject2Dot(storageObject, dotString) {
32
- const steorotype = storageObject.type === converterClasses2Storage_1.StorageType.Struct ? 'Struct' : 'Contract';
33
- // write object header with name and optional address
34
- dotString += `\n${storageObject.id} [label="{\\<\\<${steorotype}\\>\\>\\n${storageObject.name}\\n${storageObject.address || ''} | `;
30
+ exports.convertStorages2Dot = convertStorages2Dot;
31
+ function convertStorage2Dot(storage, dotString) {
32
+ const steorotype = storage.type === converterClasses2Storage_1.StorageType.Struct ? 'Struct' : 'Contract';
33
+ // write storage header with name and optional address
34
+ dotString += `\n${storage.id} [label="${storage.name} \\<\\<${steorotype}\\>\\>\\n${storage.address || ''} | {`;
35
+ const startingVariables = storage.variables.filter((s) => s.byteOffset === 0);
35
36
  // write slot numbers
36
- storageObject.storages.forEach((storage, i) => {
37
- if (i === 0) {
38
- dotString += `{slot | 0`;
37
+ dotString += '{ slot';
38
+ startingVariables.forEach((variable, i) => {
39
+ if (variable.fromSlot === variable.toSlot) {
40
+ dotString += `| ${variable.fromSlot} `;
39
41
  }
40
- else if (storage.byteOffset === 0) {
41
- if (storage.fromSlot === storage.toSlot) {
42
- dotString += `| ${storage.fromSlot}`;
43
- }
44
- else {
45
- dotString += `| ${storage.fromSlot}-${storage.toSlot}`;
46
- }
42
+ else {
43
+ dotString += `| ${variable.fromSlot}-${variable.toSlot} `;
47
44
  }
48
45
  });
49
- // write storage types
50
- storageObject.storages.forEach((storage, i) => {
51
- const lastStorage = i > 0 ? storageObject.storages[i - 1] : undefined;
52
- const nextStorage = i + 1 < storageObject.storages.length
53
- ? storageObject.storages[i + 1]
54
- : undefined;
55
- if (i === 0) {
56
- const contractVaraiblePrefix = storageObject.type === converterClasses2Storage_1.StorageType.Contract
57
- ? '\\<inherited contract\\>.'
58
- : '';
59
- dotString += `} | {type: ${contractVaraiblePrefix}variable (bytes) `;
60
- }
61
- // if next storage is in the same slot
62
- // and storage is the first in the slot
63
- if (nextStorage?.fromSlot === storage.fromSlot &&
64
- storage.byteOffset === 0) {
65
- dotString += `| { ${dotVariable(storage, storageObject.name)} `;
66
- return;
67
- }
68
- // if last storage was on the same slot
69
- // and the next storage is on a different slot
70
- if (lastStorage?.fromSlot === storage.fromSlot &&
71
- (nextStorage?.fromSlot > storage.fromSlot ||
72
- nextStorage === undefined)) {
73
- dotString += `| ${dotVariable(storage, storageObject.name)} } `;
74
- return;
46
+ // write slot values if available
47
+ if (startingVariables[0]?.values[0]) {
48
+ dotString += '} | {value';
49
+ startingVariables.forEach((variable, i) => {
50
+ dotString += ` | ${variable.values[0]}`;
51
+ });
52
+ }
53
+ const contractVariablePrefix = storage.type === converterClasses2Storage_1.StorageType.Contract ? '\\<inherited contract\\>.' : '';
54
+ dotString += `} | { type: ${contractVariablePrefix}variable (bytes)`;
55
+ // For each slot
56
+ startingVariables.forEach((variable) => {
57
+ // Get all the storage variables in this slot
58
+ const slotVariables = storage.variables.filter((s) => s.fromSlot === variable.fromSlot);
59
+ const usedBytes = slotVariables.reduce((acc, s) => acc + s.byteSize, 0);
60
+ if (usedBytes < 32) {
61
+ slotVariables.push({
62
+ id: 0,
63
+ fromSlot: variable.fromSlot,
64
+ toSlot: variable.fromSlot,
65
+ byteSize: 32 - usedBytes,
66
+ byteOffset: usedBytes,
67
+ type: 'unallocated',
68
+ contractName: variable.contractName,
69
+ variable: '',
70
+ values: [],
71
+ });
75
72
  }
76
- // If storage covers a whole slot or is not at the start or end of a slot
77
- dotString += `| ${dotVariable(storage, storageObject.name)} `;
73
+ const slotVariablesReversed = slotVariables.reverse();
74
+ // For each variable in the slot
75
+ slotVariablesReversed.forEach((variable, i) => {
76
+ if (i === 0) {
77
+ dotString += ` | { ${dotVariable(variable, storage.name)} `;
78
+ }
79
+ else {
80
+ dotString += ` | ${dotVariable(variable, storage.name)} `;
81
+ }
82
+ });
83
+ dotString += '}';
78
84
  });
79
85
  // Need to close off the last label
80
86
  dotString += '}}"]\n';
81
87
  return dotString;
82
88
  }
83
- exports.convertStorageObject2Dot = convertStorageObject2Dot;
89
+ exports.convertStorage2Dot = convertStorage2Dot;
84
90
  const dotVariable = (storage, contractName) => {
85
- const port = storage.structObjectId !== undefined ? `<${storage.id}>` : '';
91
+ const port = storage.structStorageId !== undefined ? `<${storage.id}>` : '';
86
92
  const contractNamePrefix = storage.contractName !== contractName ? `${storage.contractName}.` : '';
87
- return `${port} ${storage.type}: ${contractNamePrefix}${storage.variable} (${storage.byteSize})`;
93
+ const variable = storage.variable
94
+ ? `: ${contractNamePrefix}${storage.variable}`
95
+ : '';
96
+ return `${port} ${storage.type}${variable} (${storage.byteSize})`;
88
97
  };
89
98
  //# sourceMappingURL=converterStorage2Dot.js.map
@@ -0,0 +1,3 @@
1
+ import { BigNumberish } from '@ethersproject/bignumber';
2
+ export declare const getStorageValue: (url: string, contractAddress: string, slot: BigNumberish, blockTag?: BigNumberish | 'latest') => Promise<string>;
3
+ export declare const getStorageValues: (url: string, contractAddress: string, slots: BigNumberish[], blockTag?: BigNumberish | 'latest') => Promise<string[]>;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getStorageValues = exports.getStorageValue = void 0;
7
+ const bignumber_1 = require("@ethersproject/bignumber");
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const debug = require('debug')('sol2uml');
10
+ const getStorageValue = async (url, contractAddress, slot, blockTag = 'latest') => {
11
+ debug(`About to get storage slot ${slot} value for ${contractAddress}`);
12
+ const values = await (0, exports.getStorageValues)(url, contractAddress, [slot], blockTag);
13
+ debug(`Got slot ${slot} value: ${values[0]}`);
14
+ return values[0];
15
+ };
16
+ exports.getStorageValue = getStorageValue;
17
+ let jsonRpcId = 0;
18
+ const getStorageValues = async (url, contractAddress, slots, blockTag = 'latest') => {
19
+ try {
20
+ debug(`About to get ${slots.length} storage values for ${contractAddress} at block ${blockTag}`);
21
+ const block = blockTag === 'latest'
22
+ ? blockTag
23
+ : bignumber_1.BigNumber.from(blockTag).toHexString();
24
+ const payload = slots.map((slot) => ({
25
+ id: (jsonRpcId++).toString(),
26
+ jsonrpc: '2.0',
27
+ method: 'eth_getStorageAt',
28
+ params: [
29
+ contractAddress,
30
+ bignumber_1.BigNumber.from(slot).toHexString(),
31
+ block,
32
+ ],
33
+ }));
34
+ const response = await axios_1.default.post(url, payload);
35
+ console.log(response.data);
36
+ if (response.data?.error?.message) {
37
+ throw new Error(response.data.error.message);
38
+ }
39
+ if (response.data.length !== slots.length) {
40
+ throw new Error(`Requested ${slots.length} storage slot values but only got ${response.data.length}`);
41
+ }
42
+ const responseData = response.data;
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);
45
+ }
46
+ catch (err) {
47
+ throw new Error(`Failed to get ${slots.length} storage values for ${contractAddress} from ${url}`, { cause: err });
48
+ }
49
+ };
50
+ exports.getStorageValues = getStorageValues;
51
+ //# sourceMappingURL=slotValues.js.map
package/lib/sol2uml.js CHANGED
@@ -75,14 +75,14 @@ If an Ethereum address with a 0x prefix is passed, the verified source code from
75
75
  ...command.parent._optionValues,
76
76
  ...options,
77
77
  };
78
- const { umlClasses } = await (0, parserGeneral_1.parserUmlClasses)(fileFolderAddress, combinedOptions);
78
+ const { umlClasses, contractName } = await (0, parserGeneral_1.parserUmlClasses)(fileFolderAddress, combinedOptions);
79
79
  let filteredUmlClasses = umlClasses;
80
80
  if (options.baseContractNames) {
81
81
  const baseContractNames = options.baseContractNames.split(',');
82
82
  filteredUmlClasses = (0, filterClasses_1.classesConnectedToBaseContracts)(umlClasses, baseContractNames, options.depth);
83
83
  }
84
84
  const dotString = (0, converterClasses2Dot_1.convertUmlClasses2Dot)(filteredUmlClasses, combinedOptions.clusterFolders, combinedOptions);
85
- await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, combinedOptions.outputFormat, combinedOptions.outputFileName);
85
+ await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, contractName, combinedOptions.outputFormat, combinedOptions.outputFileName);
86
86
  debug(`Finished generating UML`);
87
87
  }
88
88
  catch (err) {
@@ -91,25 +91,49 @@ If an Ethereum address with a 0x prefix is passed, the verified source code from
91
91
  });
92
92
  program
93
93
  .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 storage arrays declared with a constant, immutable or expression will show as only taking one slot but it could be more. Storage arrays declared with an integer work.")
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.")
95
95
  .argument('<fileFolderAddress>', 'file name, base folder or contract address')
96
- .option('-c, --contractName <value>', 'Contract name in local Solidity files. Not needed when using an address as the first argument.')
97
- // .option('-d, --data', 'gets the data in the storage slots')
96
+ .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
+ .option('-d, --data', 'Gets the values in the storage slots from an Ethereum node.', false)
98
+ .option('-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.')
99
+ .addOption(new commander_1.Option('-u, --url <url>', 'URL of the Ethereum node to get storage values if the `data` option is used.')
100
+ .env('NODE_URL')
101
+ .default('http://localhost:8545'))
102
+ .option('-bn, --block <number>', 'Block number to get the contract storage values from.', 'latest')
98
103
  .action(async (fileFolderAddress, options, command) => {
99
104
  try {
100
105
  const combinedOptions = {
101
106
  ...command.parent._optionValues,
102
107
  ...options,
103
108
  };
104
- const { umlClasses, contractName } = await (0, parserGeneral_1.parserUmlClasses)(fileFolderAddress, combinedOptions);
105
- const storageObjects = (0, converterClasses2Storage_1.convertClasses2StorageObjects)(combinedOptions.contractName || contractName, umlClasses);
109
+ let { umlClasses, contractName } = await (0, parserGeneral_1.parserUmlClasses)(fileFolderAddress, combinedOptions);
110
+ contractName = combinedOptions.contract || contractName;
111
+ const storages = (0, converterClasses2Storage_1.convertClasses2Storages)(contractName, umlClasses);
106
112
  if ((0, regEx_1.isAddress)(fileFolderAddress)) {
107
- // The first object is the contract
108
- storageObjects[0].address = fileFolderAddress;
113
+ // The first storage is the contract
114
+ storages[0].address = fileFolderAddress;
115
+ }
116
+ debug(storages);
117
+ if (combinedOptions.data) {
118
+ let storageAddress = combinedOptions.storage;
119
+ if (storageAddress) {
120
+ if (!(0, regEx_1.isAddress)(storageAddress)) {
121
+ throw Error(`Invalid address to get storage data from "${storageAddress}"`);
122
+ }
123
+ }
124
+ else {
125
+ if (!(0, regEx_1.isAddress)(fileFolderAddress)) {
126
+ throw Error(`Can not get storage slot values if first param is not an address and the \`address\` option is not used.`);
127
+ }
128
+ storageAddress = fileFolderAddress;
129
+ }
130
+ const storage = storages.find((so) => so.name === contractName);
131
+ if (!storageAddress)
132
+ throw Error(`Could not find the "${contractName}" contract in list of parsed storages`);
133
+ await (0, converterClasses2Storage_1.addStorageValues)(combinedOptions.url, storageAddress, storage, combinedOptions.blockNumber);
109
134
  }
110
- debug(storageObjects);
111
- const dotString = (0, converterStorage2Dot_1.convertStorage2Dot)(storageObjects);
112
- await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, combinedOptions.outputFormat, combinedOptions.outputFileName);
135
+ const dotString = (0, converterStorage2Dot_1.convertStorages2Dot)(storages);
136
+ await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, contractName, combinedOptions.outputFormat, combinedOptions.outputFileName);
113
137
  }
114
138
  catch (err) {
115
139
  console.error(`Failed to generate storage diagram ${err}`);
package/lib/umlClass.d.ts CHANGED
@@ -63,6 +63,10 @@ export interface Association {
63
63
  targetUmlClassStereotype?: ClassStereotype;
64
64
  realization?: boolean;
65
65
  }
66
+ export interface Constants {
67
+ name: string;
68
+ value: number;
69
+ }
66
70
  export interface ClassProperties {
67
71
  name: string;
68
72
  absolutePath: string;
@@ -76,6 +80,7 @@ export interface ClassProperties {
76
80
  associations?: {
77
81
  [name: string]: Association;
78
82
  };
83
+ constants?: Constants[];
79
84
  }
80
85
  export declare class UmlClass implements ClassProperties {
81
86
  static idCounter: number;
@@ -85,6 +90,7 @@ export declare class UmlClass implements ClassProperties {
85
90
  relativePath: string;
86
91
  imports?: Import[];
87
92
  stereotype?: ClassStereotype;
93
+ constants: Constants[];
88
94
  attributes: Attribute[];
89
95
  operators: Operator[];
90
96
  enums: number[];
package/lib/umlClass.js CHANGED
@@ -43,6 +43,7 @@ var ReferenceType;
43
43
  })(ReferenceType = exports.ReferenceType || (exports.ReferenceType = {}));
44
44
  class UmlClass {
45
45
  constructor(properties) {
46
+ this.constants = [];
46
47
  this.attributes = [];
47
48
  this.operators = [];
48
49
  this.enums = [];
@@ -1,7 +1,7 @@
1
1
  export declare type OutputFormats = 'svg' | 'png' | 'dot' | 'all';
2
- export declare const writeOutputFiles: (dot: string, outputBaseName: string, outputFormat?: OutputFormats, outputFilename?: string) => Promise<void>;
2
+ export declare const writeOutputFiles: (dot: string, fileFolderAddress: string, contractName: string, outputFormat?: OutputFormats, outputFilename?: string) => Promise<void>;
3
3
  export declare function convertDot2Svg(dot: string): any;
4
4
  export declare function writeSolidity(code: string, filename?: string): void;
5
- export declare function writeDot(dot: string, filename?: string): void;
5
+ export declare function writeDot(dot: string, filename: string): void;
6
6
  export declare function writeSVG(svg: any, svgFilename?: string, outputFormats?: OutputFormats): Promise<void>;
7
7
  export declare function writePng(svg: any, filename: string): Promise<void>;
@@ -9,7 +9,26 @@ const path_1 = __importDefault(require("path"));
9
9
  const sync_1 = __importDefault(require("@aduh95/viz.js/sync"));
10
10
  const { convert } = require('convert-svg-to-png');
11
11
  const debug = require('debug')('sol2uml');
12
- const writeOutputFiles = async (dot, outputBaseName, outputFormat = 'svg', outputFilename) => {
12
+ const writeOutputFiles = async (dot, fileFolderAddress, contractName, outputFormat = 'svg', outputFilename) => {
13
+ // If all output then extension is svg
14
+ const outputExt = outputFormat === 'all' ? 'svg' : outputFormat;
15
+ if (!outputFilename) {
16
+ outputFilename =
17
+ path_1.default.join(process.cwd(), contractName) + '.' + outputExt;
18
+ }
19
+ else {
20
+ // check if outputFilename is a folder
21
+ try {
22
+ const folderOrFile = (0, fs_1.lstatSync)(outputFilename);
23
+ if (folderOrFile.isDirectory()) {
24
+ outputFilename =
25
+ path_1.default.join(process.cwd(), outputFilename, contractName) +
26
+ '.' +
27
+ outputExt;
28
+ }
29
+ }
30
+ catch (err) { } // we can ignore errors as it just means outputFilename does not exist yet
31
+ }
13
32
  if (outputFormat === 'dot' || outputFormat === 'all') {
14
33
  writeDot(dot, outputFilename);
15
34
  // No need to continue if only generating a dot file
@@ -17,20 +36,6 @@ const writeOutputFiles = async (dot, outputBaseName, outputFormat = 'svg', outpu
17
36
  return;
18
37
  }
19
38
  }
20
- if (!outputFilename) {
21
- // If all output then extension is svg
22
- const outputExt = outputFormat === 'all' ? 'svg' : outputFormat;
23
- // if outputBaseName is a folder
24
- try {
25
- const folderOrFile = (0, fs_1.lstatSync)(outputBaseName);
26
- if (folderOrFile.isDirectory()) {
27
- const parsedDir = path_1.default.parse(process.cwd());
28
- outputBaseName = path_1.default.join(process.cwd(), parsedDir.name);
29
- }
30
- }
31
- catch (err) { } // we can ignore errors as it just means outputBaseName is not a folder
32
- outputFilename = outputBaseName + '.' + outputExt;
33
- }
34
39
  const svg = convertDot2Svg(dot);
35
40
  if (outputFormat === 'svg' || outputFormat === 'all') {
36
41
  await writeSVG(svg, outputFilename, outputFormat);
@@ -68,18 +73,16 @@ function writeSolidity(code, filename = 'solidity') {
68
73
  });
69
74
  }
70
75
  exports.writeSolidity = writeSolidity;
71
- function writeDot(dot, filename = 'classDiagram.dot') {
72
- const extension = path_1.default.extname(filename);
73
- const outputFile = extension === '.dot' ? filename : filename + '.dot';
74
- debug(`About to write Dot file to ${outputFile}`);
75
- (0, fs_1.writeFile)(outputFile, dot, (err) => {
76
+ function writeDot(dot, filename) {
77
+ debug(`About to write Dot file to ${filename}`);
78
+ (0, fs_1.writeFile)(filename, dot, (err) => {
76
79
  if (err) {
77
- throw new Error(`Failed to write Dot file to ${outputFile}`, {
80
+ throw new Error(`Failed to write Dot file to ${filename}`, {
78
81
  cause: err,
79
82
  });
80
83
  }
81
84
  else {
82
- console.log(`Dot file written to ${outputFile}`);
85
+ console.log(`Dot file written to ${filename}`);
83
86
  }
84
87
  });
85
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sol2uml",
3
- "version": "2.0.3",
3
+ "version": "2.1.0",
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",
@@ -8,8 +8,8 @@
8
8
  "buildSol": "cd ./src/contracts && solc **/*.sol",
9
9
  "build": "tsc --build ./tsconfig.json",
10
10
  "clean": "tsc --build --clean ./tsconfig.json",
11
- "prettier": "prettier --write \"src/**/*.ts\"",
12
- "prettier:check": "prettier --check \"src/**/*.ts\"",
11
+ "prettier": "prettier --write src/**/*.ts **/*.md",
12
+ "prettier:check": "prettier --check src/**/*.ts **/*.md",
13
13
  "test": "npx jest"
14
14
  },
15
15
  "preferGlobal": true,
@@ -25,6 +25,7 @@
25
25
  "commander": "^9.4.0",
26
26
  "convert-svg-to-png": "^0.6.4",
27
27
  "debug": "^4.3.4",
28
+ "ethers": "^5.6.9",
28
29
  "js-graph-algorithms": "^1.0.18",
29
30
  "klaw": "^4.0.1"
30
31
  },