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 +1 -1
- package/README.md +16 -10
- package/lib/associations.d.ts +1 -1
- package/lib/associations.js +65 -21
- package/lib/converterAST2Classes.js +13 -1
- package/lib/converterClasses2Storage.d.ts +29 -10
- package/lib/converterClasses2Storage.js +256 -99
- package/lib/converterStorage2Dot.d.ts +7 -3
- package/lib/converterStorage2Dot.js +67 -56
- package/lib/parserEtherscan.js +8 -1
- package/lib/slotValues.d.ts +3 -0
- package/lib/slotValues.js +51 -0
- package/lib/sol2uml.js +56 -26
- package/lib/umlClass.d.ts +7 -1
- package/lib/umlClass.js +1 -0
- package/lib/writerFiles.d.ts +1 -1
- package/lib/writerFiles.js +12 -14
- package/package.json +1 -1
package/LICENSE
CHANGED
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
|
|
|
@@ -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
|
|
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
|
|
120
|
+
fileFolderAddress file name, base folder or contract address
|
|
119
121
|
|
|
120
122
|
Options:
|
|
121
|
-
-c, --
|
|
122
|
-
-
|
|
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
|
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
|
|
@@ -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 =
|
|
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 =
|
|
4
|
-
Struct =
|
|
3
|
+
Contract = "Contract",
|
|
4
|
+
Struct = "Struct",
|
|
5
|
+
Array = "Array"
|
|
5
6
|
}
|
|
6
|
-
export interface
|
|
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
|
-
|
|
14
|
+
dynamic: boolean;
|
|
15
|
+
variable?: string;
|
|
14
16
|
contractName?: string;
|
|
17
|
+
noValue: boolean;
|
|
15
18
|
value?: string;
|
|
16
|
-
|
|
19
|
+
referenceStorageId?: number;
|
|
17
20
|
enumId?: number;
|
|
18
21
|
}
|
|
19
|
-
export interface
|
|
22
|
+
export interface Storage {
|
|
20
23
|
id: number;
|
|
21
24
|
name: string;
|
|
22
25
|
address?: string;
|
|
26
|
+
slotKey?: string;
|
|
23
27
|
type: StorageType;
|
|
24
|
-
|
|
28
|
+
arrayLength?: number;
|
|
29
|
+
arrayDynamic?: boolean;
|
|
30
|
+
variables: Variable[];
|
|
25
31
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
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[
|
|
9
|
-
StorageType[
|
|
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
|
-
|
|
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
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
id:
|
|
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
|
-
|
|
46
|
+
variables: variables,
|
|
28
47
|
});
|
|
29
|
-
return
|
|
48
|
+
return storages;
|
|
30
49
|
};
|
|
31
|
-
exports.
|
|
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
|
|
37
|
-
* @param
|
|
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
|
|
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
|
|
69
|
+
if (!parentClass) {
|
|
70
|
+
throw Error(`Failed to find inherited contract "${parent.targetUmlClassName}" of "${umlClass.absolutePath}"`);
|
|
71
|
+
}
|
|
52
72
|
// recursively parse inherited contract
|
|
53
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 (
|
|
68
|
-
const lastStorage =
|
|
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 =
|
|
74
|
-
|
|
75
|
-
id:
|
|
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
|
-
|
|
84
|
-
}
|
|
107
|
+
referenceStorageId: referenceStorage?.id,
|
|
108
|
+
};
|
|
85
109
|
}
|
|
86
110
|
else {
|
|
87
|
-
|
|
88
|
-
id:
|
|
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
|
-
|
|
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
|
|
135
|
+
return variables;
|
|
101
136
|
};
|
|
102
|
-
const
|
|
103
|
-
if (attribute.attributeType === umlClass_1.AttributeType.
|
|
104
|
-
//
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
118
|
-
const
|
|
119
|
-
id:
|
|
216
|
+
const variables = parseVariables(dependentClass, otherClasses, [], storages, []);
|
|
217
|
+
const newStorage = {
|
|
218
|
+
id: storageId++,
|
|
120
219
|
name: attribute.type,
|
|
121
220
|
type: StorageType.Struct,
|
|
122
|
-
|
|
221
|
+
variables,
|
|
123
222
|
};
|
|
124
|
-
|
|
125
|
-
return
|
|
223
|
+
storages.push(newStorage);
|
|
224
|
+
return newStorage;
|
|
126
225
|
}
|
|
127
226
|
return undefined;
|
|
128
227
|
}
|
|
129
|
-
if (attribute.attributeType === umlClass_1.AttributeType.Mapping
|
|
130
|
-
|
|
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.
|
|
135
|
-
|
|
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
|
|
150
|
-
const
|
|
151
|
-
id:
|
|
241
|
+
const variables = parseVariables(typeClass, otherClasses, [], storages, []);
|
|
242
|
+
const newStorage = {
|
|
243
|
+
id: storageId++,
|
|
152
244
|
name: typeClass.name,
|
|
153
245
|
type: StorageType.Struct,
|
|
154
|
-
|
|
246
|
+
variables,
|
|
155
247
|
};
|
|
156
|
-
|
|
157
|
-
return
|
|
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.
|
|
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
|
-
//
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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)(
|
|
290
|
+
if ((0, exports.isElementary)(type)) {
|
|
189
291
|
const elementAttribute = {
|
|
190
292
|
attributeType: umlClass_1.AttributeType.Elementary,
|
|
191
|
-
type
|
|
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
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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
|
|
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 {
|
|
2
|
-
export declare const
|
|
3
|
-
|
|
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.
|
|
3
|
+
exports.convertStorage2Dot = exports.convertStorages2Dot = void 0;
|
|
4
4
|
const converterClasses2Storage_1 = require("./converterClasses2Storage");
|
|
5
5
|
const debug = require('debug')('sol2uml');
|
|
6
|
-
const
|
|
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
|
|
14
|
-
|
|
15
|
-
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
|
-
|
|
19
|
-
slot.
|
|
20
|
-
if (storage.
|
|
21
|
-
dotString += `\n ${slot.id}:${storage.id} -> ${storage.
|
|
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.
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
dotString +=
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
dotString += '{ slot';
|
|
38
|
+
startingVariables.forEach((variable, i) => {
|
|
39
|
+
if (variable.fromSlot === variable.toSlot) {
|
|
40
|
+
dotString += `| ${variable.fromSlot} `;
|
|
39
41
|
}
|
|
40
|
-
else
|
|
41
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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.
|
|
91
|
+
exports.convertStorage2Dot = convertStorage2Dot;
|
|
84
92
|
const dotVariable = (storage, contractName) => {
|
|
85
|
-
const port = storage.
|
|
93
|
+
const port = storage.referenceStorageId !== undefined ? `<${storage.id}>` : '';
|
|
86
94
|
const contractNamePrefix = storage.contractName !== contractName ? `${storage.contractName}.` : '';
|
|
87
|
-
|
|
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
|
package/lib/parserEtherscan.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
|
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
|
|
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, --
|
|
97
|
-
|
|
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.
|
|
106
|
-
const
|
|
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
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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
package/lib/writerFiles.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/lib/writerFiles.js
CHANGED
|
@@ -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
|
|
77
|
-
|
|
78
|
-
|
|
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 ${
|
|
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 ${
|
|
85
|
+
console.log(`Dot file written to ${filename}`);
|
|
88
86
|
}
|
|
89
87
|
});
|
|
90
88
|
}
|