sol2uml 2.1.1 → 2.1.2
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 +13 -13
- package/lib/converterClasses2Storage.js +4 -5
- package/lib/filterClasses.d.ts +2 -1
- package/lib/filterClasses.js +34 -9
- package/lib/parserEtherscan.js +25 -1
- package/lib/sol2uml.js +16 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,12 +44,11 @@ npm ls sol2uml -g
|
|
|
44
44
|
## Command Line Interface (CLI)
|
|
45
45
|
|
|
46
46
|
```
|
|
47
|
-
$ sol2uml --help
|
|
48
47
|
Usage: sol2uml [subcommand] <options>
|
|
49
48
|
The three subcommands:
|
|
50
49
|
* class: Generates a UML class diagram from Solidity source code. default
|
|
51
50
|
* storage: Generates a diagram of a contract's storage slots.
|
|
52
|
-
* flatten:
|
|
51
|
+
* flatten: Merges verified source files from a Blockchain explorer into one local file.
|
|
53
52
|
|
|
54
53
|
The Solidity code can be pulled from verified source code on Blockchain explorers like Etherscan or from local Solidity files.
|
|
55
54
|
|
|
@@ -73,7 +72,6 @@ Commands:
|
|
|
73
72
|
### Class usage
|
|
74
73
|
|
|
75
74
|
```
|
|
76
|
-
$sol2uml class --help
|
|
77
75
|
Usage: sol2uml class <fileFolderAddress> [options]
|
|
78
76
|
|
|
79
77
|
Generates UML diagrams from Solidity source code.
|
|
@@ -110,20 +108,19 @@ Options:
|
|
|
110
108
|
### Storage usage
|
|
111
109
|
|
|
112
110
|
```
|
|
113
|
-
Usage: sol2uml storage [options]
|
|
114
|
-
|
|
115
|
-
Visually display a contract's storage slots.
|
|
111
|
+
Usage: sol2uml storage <fileFolderAddress> [options]
|
|
116
112
|
|
|
117
113
|
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.
|
|
118
114
|
|
|
115
|
+
Visually display a contract's storage slots.
|
|
116
|
+
|
|
119
117
|
Arguments:
|
|
120
118
|
fileFolderAddress file name, base folder or contract address
|
|
121
119
|
|
|
122
120
|
Options:
|
|
123
121
|
-c, --contract <name> Contract name in local Solidity files. Not needed when using an address as the first argument as the contract name can be derived from Etherscan.
|
|
124
122
|
-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.
|
|
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 the contract is not proxied.
|
|
127
124
|
-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
125
|
-bn, --block <number> Block number to get the contract storage values from. (default: "latest")
|
|
129
126
|
-h, --help display help for command
|
|
@@ -132,17 +129,20 @@ Options:
|
|
|
132
129
|
### Flatten usage
|
|
133
130
|
|
|
134
131
|
```
|
|
135
|
-
|
|
136
|
-
|
|
132
|
+
Usage: sol2uml flatten <contractAddress> [options]
|
|
133
|
+
|
|
134
|
+
In order for the merged code to compile, the following is done:
|
|
135
|
+
1. File imports are commented out.
|
|
136
|
+
2. "SPDX-License-Identifier" is renamed to "SPDX--License-Identifier".
|
|
137
|
+
3. Contract dependencies are analysed so the files are merged in an order that will compile.
|
|
137
138
|
|
|
138
|
-
|
|
139
|
+
Merges verified source files for a contract from a Blockchain explorer into one local file.
|
|
139
140
|
|
|
140
141
|
Arguments:
|
|
141
|
-
contractAddress Contract address
|
|
142
|
+
contractAddress Contract address in hexadecimal format with a 0x prefix.
|
|
142
143
|
|
|
143
144
|
Options:
|
|
144
145
|
-h, --help display help for command
|
|
145
|
-
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
## UML Class diagram examples
|
|
@@ -162,11 +162,10 @@ const parseReferenceStorage = (attribute, umlClass, otherClasses, storages) => {
|
|
|
162
162
|
attributeType: baseAttributeType,
|
|
163
163
|
};
|
|
164
164
|
const { size: arrayItemSize } = (0, exports.calcStorageByteSize)(baseAttribute, umlClass, otherClasses);
|
|
165
|
-
const slotSize = arrayItemSize > 16 ? 32 : arrayItemSize;
|
|
166
165
|
const firstVariable = {
|
|
167
166
|
id: variableId++,
|
|
168
167
|
fromSlot: 0,
|
|
169
|
-
toSlot: Math.floor((
|
|
168
|
+
toSlot: Math.floor((arrayItemSize - 1) / 32),
|
|
170
169
|
byteSize: arrayItemSize,
|
|
171
170
|
byteOffset: 0,
|
|
172
171
|
type: baseType,
|
|
@@ -178,10 +177,10 @@ const parseReferenceStorage = (attribute, umlClass, otherClasses, storages) => {
|
|
|
178
177
|
for (let i = 1; i < arrayLength; i++) {
|
|
179
178
|
variables.push({
|
|
180
179
|
id: variableId++,
|
|
181
|
-
fromSlot: Math.floor((i *
|
|
182
|
-
toSlot: Math.floor(((i + 1) *
|
|
180
|
+
fromSlot: Math.floor((i * arrayItemSize) / 32),
|
|
181
|
+
toSlot: Math.floor(((i + 1) * arrayItemSize - 1) / 32),
|
|
183
182
|
byteSize: arrayItemSize,
|
|
184
|
-
byteOffset: (i *
|
|
183
|
+
byteOffset: (i * arrayItemSize) % 32,
|
|
185
184
|
type: baseType,
|
|
186
185
|
dynamic,
|
|
187
186
|
noValue: false,
|
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,7 @@ 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");
|
|
10
11
|
const networks = [
|
|
11
12
|
'mainnet',
|
|
12
13
|
'ropsten',
|
|
@@ -69,8 +70,31 @@ class EtherscanParser {
|
|
|
69
70
|
*/
|
|
70
71
|
async getSolidityCode(contractAddress) {
|
|
71
72
|
const { files, contractName } = await this.getSourceCode(contractAddress);
|
|
73
|
+
// Parse the UmlClasses from the Solidity code in each file
|
|
74
|
+
let umlClasses = [];
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
const node = await this.parseSourceCode(file.code);
|
|
77
|
+
const umlClass = (0, converterAST2Classes_1.convertAST2UmlClasses)(node, file.filename);
|
|
78
|
+
umlClasses = umlClasses.concat(umlClass);
|
|
79
|
+
}
|
|
80
|
+
// Sort the classes so dependent code is first
|
|
81
|
+
const topologicalSortedClasses = (0, filterClasses_1.topologicalSortClasses)(umlClasses);
|
|
82
|
+
// Get a list of filenames the classes are in
|
|
83
|
+
const sortedFilenames = topologicalSortedClasses.map((umlClass) => umlClass.relativePath);
|
|
84
|
+
// Remove duplicate filenames from the list
|
|
85
|
+
const dependentFilenames = [...new Set(sortedFilenames)];
|
|
86
|
+
// find any files that didn't have dependencies found
|
|
87
|
+
const nonDependentFiles = files.filter((f) => !dependentFilenames.includes(f.filename));
|
|
88
|
+
const nonDependentFilenames = nonDependentFiles.map((f) => f.filename);
|
|
72
89
|
let solidityCode = '';
|
|
73
|
-
files
|
|
90
|
+
// output non dependent code before the dependent files just in case sol2uml missed some dependencies
|
|
91
|
+
const filenames = [...nonDependentFilenames, ...dependentFilenames];
|
|
92
|
+
// For each filename
|
|
93
|
+
filenames.forEach((filename) => {
|
|
94
|
+
// Lookup the file that contains the Solidity code
|
|
95
|
+
const file = files.find((f) => f.filename === filename);
|
|
96
|
+
if (!file)
|
|
97
|
+
throw Error(`Failed to find file with filename "${filename}"`);
|
|
74
98
|
// comment out any import statements
|
|
75
99
|
// match whitespace before import
|
|
76
100
|
// and characters after import up to ;
|
package/lib/sol2uml.js
CHANGED
|
@@ -15,10 +15,11 @@ const debugControl = require('debug');
|
|
|
15
15
|
const debug = require('debug')('sol2uml');
|
|
16
16
|
program
|
|
17
17
|
.usage(`[subcommand] <options>
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
|
|
19
|
+
sol2uml comes with three subcommands:
|
|
20
|
+
* class: Generates a UML class diagram from Solidity source code. (default)
|
|
20
21
|
* storage: Generates a diagram of a contract's storage slots.
|
|
21
|
-
* flatten:
|
|
22
|
+
* flatten: Merges verified source files from a Blockchain explorer into one local file.
|
|
22
23
|
|
|
23
24
|
The Solidity code can be pulled from verified source code on Blockchain explorers like Etherscan or from local Solidity files.`)
|
|
24
25
|
.addOption(new commander_1.Option('-sf, --subfolders <value>', 'number of subfolders that will be recursively searched for Solidity files.').default('-1', 'all'))
|
|
@@ -93,7 +94,10 @@ If an Ethereum address with a 0x prefix is passed, the verified source code from
|
|
|
93
94
|
});
|
|
94
95
|
program
|
|
95
96
|
.command('storage')
|
|
96
|
-
.description("Visually display a contract's storage slots
|
|
97
|
+
.description("Visually display a contract's storage slots.")
|
|
98
|
+
.usage(`<fileFolderAddress> [options]
|
|
99
|
+
|
|
100
|
+
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.`)
|
|
97
101
|
.argument('<fileFolderAddress>', 'file name, base folder or contract address')
|
|
98
102
|
.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
103
|
.option('-d, --data', 'Gets the values in the storage slots from an Ethereum node.', false)
|
|
@@ -143,8 +147,14 @@ program
|
|
|
143
147
|
});
|
|
144
148
|
program
|
|
145
149
|
.command('flatten')
|
|
146
|
-
.description('
|
|
147
|
-
.
|
|
150
|
+
.description('Merges verified source files for a contract from a Blockchain explorer into one local file.')
|
|
151
|
+
.usage(`<contractAddress> [options]
|
|
152
|
+
|
|
153
|
+
In order for the merged code to compile, the following is done:
|
|
154
|
+
1. File imports are commented out.
|
|
155
|
+
2. "SPDX-License-Identifier" is renamed to "SPDX--License-Identifier".
|
|
156
|
+
3. Contract dependencies are analysed so the files are merged in an order that will compile.`)
|
|
157
|
+
.argument('<contractAddress>', 'Contract address in hexadecimal format with a 0x prefix.')
|
|
148
158
|
.action(async (contractAddress, options, command) => {
|
|
149
159
|
try {
|
|
150
160
|
debug(`About to flatten ${contractAddress}`);
|