sol2uml 2.0.4 → 2.1.1

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