sol2uml 2.1.0 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -18
- package/lib/associations.d.ts +1 -1
- package/lib/associations.js +65 -21
- package/lib/converterAST2Classes.js +1 -1
- package/lib/converterClasses2Storage.d.ts +19 -7
- package/lib/converterClasses2Storage.js +211 -82
- package/lib/converterStorage2Dot.d.ts +6 -2
- package/lib/converterStorage2Dot.js +14 -12
- package/lib/filterClasses.d.ts +2 -1
- package/lib/filterClasses.js +34 -9
- package/lib/parserEtherscan.js +35 -2
- package/lib/slotValues.js +1 -1
- package/lib/sol2uml.js +42 -24
- package/lib/umlClass.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/js/sol2uml)
|
|
4
4
|
|
|
5
|
-
|
|
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
|

|
|
9
11
|
|
|
10
12
|
See more contract diagrams [here](./examples/README.md).
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
 on Etherscan.
|
|
15
|
+

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