sol2uml 2.1.1 → 2.1.4
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 +16 -15
- package/lib/converterClasses2Storage.js +4 -5
- package/lib/filterClasses.d.ts +2 -1
- package/lib/filterClasses.js +34 -9
- package/lib/parserEtherscan.d.ts +1 -1
- package/lib/parserEtherscan.js +85 -8
- package/lib/sol2uml.js +29 -21
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -44,22 +44,22 @@ 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
|
|
|
56
55
|
Options:
|
|
56
|
+
-V, --version output the version number
|
|
57
57
|
-sf, --subfolders <value> number of subfolders that will be recursively searched for Solidity files. (default: all)
|
|
58
58
|
-f, --outputFormat <value> output file format. (choices: "svg", "png", "dot", "all", default: "svg")
|
|
59
59
|
-o, --outputFileName <value> output file name
|
|
60
60
|
-i, --ignoreFilesOrFolders <filesOrFolders> comma separated list of files or folders to ignore
|
|
61
|
-
-n, --network <network> Ethereum network (choices: "mainnet", "polygon", "
|
|
62
|
-
-k, --apiKey <key> Etherscan,
|
|
61
|
+
-n, --network <network> Ethereum network (choices: "mainnet", "ropsten", "kovan", "rinkeby", "goerli", "sepolia", "polygon", "testnet.polygon", "arbitrum", "testnet.arbitrum", "avalanche", "testnet.avalanche", "bsc", "testnet.bsc", "crono", "fantom", "testnet.fantom", "moonbeam", "optimistic", "kovan-optimistic", default: "mainnet", env: ETH_NETWORK)
|
|
62
|
+
-k, --apiKey <key> Blockchain explorer API key. eg Etherscan, Arbiscan, BscScan, CronoScan, FTMScan, PolygonScan or SnowTrace API key (env: SCAN_API_KEY)
|
|
63
63
|
-v, --verbose run with debugging statements (default: false)
|
|
64
64
|
-h, --help display help for command
|
|
65
65
|
|
|
@@ -73,7 +73,6 @@ Commands:
|
|
|
73
73
|
### Class usage
|
|
74
74
|
|
|
75
75
|
```
|
|
76
|
-
$sol2uml class --help
|
|
77
76
|
Usage: sol2uml class <fileFolderAddress> [options]
|
|
78
77
|
|
|
79
78
|
Generates UML diagrams from Solidity source code.
|
|
@@ -110,20 +109,19 @@ Options:
|
|
|
110
109
|
### Storage usage
|
|
111
110
|
|
|
112
111
|
```
|
|
113
|
-
Usage: sol2uml storage [options]
|
|
114
|
-
|
|
115
|
-
Visually display a contract's storage slots.
|
|
112
|
+
Usage: sol2uml storage <fileFolderAddress> [options]
|
|
116
113
|
|
|
117
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.
|
|
118
115
|
|
|
116
|
+
Visually display a contract's storage slots.
|
|
117
|
+
|
|
119
118
|
Arguments:
|
|
120
119
|
fileFolderAddress file name, base folder or contract address
|
|
121
120
|
|
|
122
121
|
Options:
|
|
123
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.
|
|
124
123
|
-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.
|
|
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.
|
|
127
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)
|
|
128
126
|
-bn, --block <number> Block number to get the contract storage values from. (default: "latest")
|
|
129
127
|
-h, --help display help for command
|
|
@@ -132,17 +130,20 @@ Options:
|
|
|
132
130
|
### Flatten usage
|
|
133
131
|
|
|
134
132
|
```
|
|
135
|
-
|
|
136
|
-
|
|
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.
|
|
137
139
|
|
|
138
|
-
|
|
140
|
+
Merges verified source files for a contract from a Blockchain explorer into one local file.
|
|
139
141
|
|
|
140
142
|
Arguments:
|
|
141
|
-
contractAddress Contract address
|
|
143
|
+
contractAddress Contract address in hexadecimal format with a 0x prefix.
|
|
142
144
|
|
|
143
145
|
Options:
|
|
144
146
|
-h, --help display help for command
|
|
145
|
-
|
|
146
147
|
```
|
|
147
148
|
|
|
148
149
|
## 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.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ASTNode } from '@solidity-parser/parser/dist/src/ast-types';
|
|
2
2
|
import { UmlClass } from './umlClass';
|
|
3
|
-
declare const networks: readonly ["mainnet", "ropsten", "kovan", "rinkeby", "goerli", "sepolia", "polygon", "
|
|
3
|
+
export declare const networks: readonly ["mainnet", "ropsten", "kovan", "rinkeby", "goerli", "sepolia", "polygon", "testnet.polygon", "arbitrum", "testnet.arbitrum", "avalanche", "testnet.avalanche", "bsc", "testnet.bsc", "crono", "fantom", "testnet.fantom", "moonbeam", "optimistic", "kovan-optimistic"];
|
|
4
4
|
declare type Network = typeof networks[number];
|
|
5
5
|
export declare class EtherscanParser {
|
|
6
6
|
protected apikey: string;
|
package/lib/parserEtherscan.js
CHANGED
|
@@ -3,11 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.EtherscanParser = void 0;
|
|
6
|
+
exports.EtherscanParser = exports.networks = 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
|
|
10
|
+
const filterClasses_1 = require("./filterClasses");
|
|
11
|
+
const debug = require('debug')('sol2uml');
|
|
12
|
+
exports.networks = [
|
|
11
13
|
'mainnet',
|
|
12
14
|
'ropsten',
|
|
13
15
|
'kovan',
|
|
@@ -15,15 +17,26 @@ const networks = [
|
|
|
15
17
|
'goerli',
|
|
16
18
|
'sepolia',
|
|
17
19
|
'polygon',
|
|
18
|
-
'
|
|
20
|
+
'testnet.polygon',
|
|
19
21
|
'arbitrum',
|
|
22
|
+
'testnet.arbitrum',
|
|
23
|
+
'avalanche',
|
|
24
|
+
'testnet.avalanche',
|
|
25
|
+
'bsc',
|
|
26
|
+
'testnet.bsc',
|
|
27
|
+
'crono',
|
|
28
|
+
'fantom',
|
|
29
|
+
'testnet.fantom',
|
|
30
|
+
'moonbeam',
|
|
31
|
+
'optimistic',
|
|
32
|
+
'kovan-optimistic',
|
|
20
33
|
];
|
|
21
34
|
class EtherscanParser {
|
|
22
35
|
constructor(apikey = 'ZAD4UI2RCXCQTP38EXS3UY2MPHFU5H9KB1', network = 'mainnet') {
|
|
23
36
|
this.apikey = apikey;
|
|
24
37
|
this.network = network;
|
|
25
|
-
if (!networks.includes(network)) {
|
|
26
|
-
throw new Error(`Invalid network "${network}". Must be one of ${networks}`);
|
|
38
|
+
if (!exports.networks.includes(network)) {
|
|
39
|
+
throw new Error(`Invalid network "${network}". Must be one of ${exports.networks}`);
|
|
27
40
|
}
|
|
28
41
|
else if (network === 'mainnet') {
|
|
29
42
|
this.url = 'https://api.etherscan.io/api';
|
|
@@ -32,12 +45,52 @@ class EtherscanParser {
|
|
|
32
45
|
this.url = 'https://api.polygonscan.com/api';
|
|
33
46
|
this.apikey = 'AMHGNTV5A7XYGX2M781JB3RC1DZFVRWQEB';
|
|
34
47
|
}
|
|
48
|
+
else if (network === 'testnet.polygon') {
|
|
49
|
+
this.url = 'https://api-testnet.polygonscan.com/api';
|
|
50
|
+
this.apikey = 'AMHGNTV5A7XYGX2M781JB3RC1DZFVRWQEB';
|
|
51
|
+
}
|
|
52
|
+
else if (network === 'arbitrum') {
|
|
53
|
+
this.url = 'https://api.arbiscan.io/api';
|
|
54
|
+
this.apikey = 'ZGTK2TAGWMAB6IAC12BMK8YYPNCPIM8VDQ';
|
|
55
|
+
}
|
|
56
|
+
else if (network === 'testnet.arbitrum') {
|
|
57
|
+
this.url = 'https://api-testnet.arbiscan.io/api';
|
|
58
|
+
this.apikey = 'ZGTK2TAGWMAB6IAC12BMK8YYPNCPIM8VDQ';
|
|
59
|
+
}
|
|
60
|
+
else if (network === 'avalanche') {
|
|
61
|
+
this.url = 'https://api.snowtrace.io/api';
|
|
62
|
+
this.apikey = 'U5FAN98S5XNH5VI83TI4H35R9I4TDCKEJY';
|
|
63
|
+
}
|
|
64
|
+
else if (network === 'testnet.avalanche') {
|
|
65
|
+
this.url = 'https://api-testnet.snowtrace.io/api';
|
|
66
|
+
this.apikey = 'U5FAN98S5XNH5VI83TI4H35R9I4TDCKEJY';
|
|
67
|
+
}
|
|
35
68
|
else if (network === 'bsc') {
|
|
36
69
|
this.url = 'https://api.bscscan.com/api';
|
|
70
|
+
}
|
|
71
|
+
else if (network === 'testnet.bsc') {
|
|
72
|
+
this.url = 'https://api-testnet.bscscan.com/api';
|
|
73
|
+
}
|
|
74
|
+
else if (network === 'crono') {
|
|
75
|
+
this.url = 'https://api.cronoscan.com/api';
|
|
76
|
+
this.apikey = '76A3RG5WHTPMMR66E9SFI2EIDT6MP976W2';
|
|
37
77
|
this.apikey = 'APYH49FXVY9UA3KTDI6F4WP3KPIC86NITN';
|
|
38
78
|
}
|
|
39
|
-
else if (network === '
|
|
40
|
-
this.url = 'https://api.
|
|
79
|
+
else if (network === 'fantom') {
|
|
80
|
+
this.url = 'https://api.ftmscan.com/api';
|
|
81
|
+
this.apikey = '71KRX13XPZMGR3D1Q85W78G2DSZ4JPMAEX';
|
|
82
|
+
}
|
|
83
|
+
else if (network === 'testnet.fantom') {
|
|
84
|
+
this.url = 'https://api-testnet.ftmscan.com/api';
|
|
85
|
+
this.apikey = '71KRX13XPZMGR3D1Q85W78G2DSZ4JPMAEX';
|
|
86
|
+
}
|
|
87
|
+
else if (network === 'optimistic' || network === 'kovan-optimistic') {
|
|
88
|
+
this.url = `https://api-${network}.etherscan.io/api`;
|
|
89
|
+
this.apikey = 'FEXS1HXVA4Y2RNTMEA8V1UTK21S4JWHH9U';
|
|
90
|
+
}
|
|
91
|
+
else if (network === 'moonbeam') {
|
|
92
|
+
this.url = 'https://api-moonbeam.moonscan.io/api';
|
|
93
|
+
this.apikey = '5EUFXW6TDC16VERF3D9SCWRRU6AEMTBHNJ';
|
|
41
94
|
}
|
|
42
95
|
else {
|
|
43
96
|
this.url = `https://api-${network}.etherscan.io/api`;
|
|
@@ -69,8 +122,31 @@ class EtherscanParser {
|
|
|
69
122
|
*/
|
|
70
123
|
async getSolidityCode(contractAddress) {
|
|
71
124
|
const { files, contractName } = await this.getSourceCode(contractAddress);
|
|
125
|
+
// Parse the UmlClasses from the Solidity code in each file
|
|
126
|
+
let umlClasses = [];
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const node = await this.parseSourceCode(file.code);
|
|
129
|
+
const umlClass = (0, converterAST2Classes_1.convertAST2UmlClasses)(node, file.filename);
|
|
130
|
+
umlClasses = umlClasses.concat(umlClass);
|
|
131
|
+
}
|
|
132
|
+
// Sort the classes so dependent code is first
|
|
133
|
+
const topologicalSortedClasses = (0, filterClasses_1.topologicalSortClasses)(umlClasses);
|
|
134
|
+
// Get a list of filenames the classes are in
|
|
135
|
+
const sortedFilenames = topologicalSortedClasses.map((umlClass) => umlClass.relativePath);
|
|
136
|
+
// Remove duplicate filenames from the list
|
|
137
|
+
const dependentFilenames = [...new Set(sortedFilenames)];
|
|
138
|
+
// find any files that didn't have dependencies found
|
|
139
|
+
const nonDependentFiles = files.filter((f) => !dependentFilenames.includes(f.filename));
|
|
140
|
+
const nonDependentFilenames = nonDependentFiles.map((f) => f.filename);
|
|
72
141
|
let solidityCode = '';
|
|
73
|
-
files
|
|
142
|
+
// output non dependent code before the dependent files just in case sol2uml missed some dependencies
|
|
143
|
+
const filenames = [...nonDependentFilenames, ...dependentFilenames];
|
|
144
|
+
// For each filename
|
|
145
|
+
filenames.forEach((filename) => {
|
|
146
|
+
// Lookup the file that contains the Solidity code
|
|
147
|
+
const file = files.find((f) => f.filename === filename);
|
|
148
|
+
if (!file)
|
|
149
|
+
throw Error(`Failed to find file with filename "${filename}"`);
|
|
74
150
|
// comment out any import statements
|
|
75
151
|
// match whitespace before import
|
|
76
152
|
// and characters after import up to ;
|
|
@@ -106,6 +182,7 @@ class EtherscanParser {
|
|
|
106
182
|
async getSourceCode(contractAddress) {
|
|
107
183
|
const description = `get verified source code for address ${contractAddress} from Etherscan API.`;
|
|
108
184
|
try {
|
|
185
|
+
debug(`About to get Solidity source code for ${contractAddress} from ${this.url}`);
|
|
109
186
|
const response = await axios_1.default.get(this.url, {
|
|
110
187
|
params: {
|
|
111
188
|
module: 'contract',
|
package/lib/sol2uml.js
CHANGED
|
@@ -10,15 +10,21 @@ const converterClasses2Storage_1 = require("./converterClasses2Storage");
|
|
|
10
10
|
const converterStorage2Dot_1 = require("./converterStorage2Dot");
|
|
11
11
|
const regEx_1 = require("./utils/regEx");
|
|
12
12
|
const writerFiles_1 = require("./writerFiles");
|
|
13
|
+
const path_1 = require("path");
|
|
13
14
|
const program = new commander_1.Command();
|
|
15
|
+
const version = (0, path_1.basename)(__dirname) === 'lib'
|
|
16
|
+
? require('../package.json').version // used when run from compile js in /lib
|
|
17
|
+
: require('../../package.json').version; // used when run from TypeScript source files under src/ts via ts-node
|
|
18
|
+
program.version(version);
|
|
14
19
|
const debugControl = require('debug');
|
|
15
20
|
const debug = require('debug')('sol2uml');
|
|
16
21
|
program
|
|
17
22
|
.usage(`[subcommand] <options>
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
|
|
24
|
+
sol2uml comes with three subcommands:
|
|
25
|
+
* class: Generates a UML class diagram from Solidity source code. (default)
|
|
20
26
|
* storage: Generates a diagram of a contract's storage slots.
|
|
21
|
-
* flatten:
|
|
27
|
+
* flatten: Merges verified source files from a Blockchain explorer into one local file.
|
|
22
28
|
|
|
23
29
|
The Solidity code can be pulled from verified source code on Blockchain explorers like Etherscan or from local Solidity files.`)
|
|
24
30
|
.addOption(new commander_1.Option('-sf, --subfolders <value>', 'number of subfolders that will be recursively searched for Solidity files.').default('-1', 'all'))
|
|
@@ -28,20 +34,10 @@ The Solidity code can be pulled from verified source code on Blockchain explorer
|
|
|
28
34
|
.option('-o, --outputFileName <value>', 'output file name')
|
|
29
35
|
.option('-i, --ignoreFilesOrFolders <filesOrFolders>', 'comma separated list of files or folders to ignore')
|
|
30
36
|
.addOption(new commander_1.Option('-n, --network <network>', 'Ethereum network')
|
|
31
|
-
.choices(
|
|
32
|
-
'mainnet',
|
|
33
|
-
'polygon',
|
|
34
|
-
'bsc',
|
|
35
|
-
'arbitrum',
|
|
36
|
-
'ropsten',
|
|
37
|
-
'kovan',
|
|
38
|
-
'rinkeby',
|
|
39
|
-
'goerli',
|
|
40
|
-
'sepolia',
|
|
41
|
-
])
|
|
37
|
+
.choices(parserEtherscan_1.networks)
|
|
42
38
|
.default('mainnet')
|
|
43
39
|
.env('ETH_NETWORK'))
|
|
44
|
-
.addOption(new commander_1.Option('-k, --apiKey <key>', 'Etherscan,
|
|
40
|
+
.addOption(new commander_1.Option('-k, --apiKey <key>', 'Blockchain explorer API key. eg Etherscan, Arbiscan, BscScan, CronoScan, FTMScan, PolygonScan or SnowTrace API key').env('SCAN_API_KEY'))
|
|
45
41
|
.option('-v, --verbose', 'run with debugging statements', false);
|
|
46
42
|
program
|
|
47
43
|
.command('class', { isDefault: true })
|
|
@@ -88,12 +84,16 @@ If an Ethereum address with a 0x prefix is passed, the verified source code from
|
|
|
88
84
|
debug(`Finished generating UML`);
|
|
89
85
|
}
|
|
90
86
|
catch (err) {
|
|
91
|
-
console.error(
|
|
87
|
+
console.error(err);
|
|
88
|
+
process.exit(2);
|
|
92
89
|
}
|
|
93
90
|
});
|
|
94
91
|
program
|
|
95
92
|
.command('storage')
|
|
96
|
-
.description("Visually display a contract's storage slots
|
|
93
|
+
.description("Visually display a contract's storage slots.")
|
|
94
|
+
.usage(`<fileFolderAddress> [options]
|
|
95
|
+
|
|
96
|
+
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
97
|
.argument('<fileFolderAddress>', 'file name, base folder or contract address')
|
|
98
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
99
|
.option('-d, --data', 'Gets the values in the storage slots from an Ethereum node.', false)
|
|
@@ -138,13 +138,20 @@ program
|
|
|
138
138
|
await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, contractName || 'storageDiagram', combinedOptions.outputFormat, combinedOptions.outputFileName);
|
|
139
139
|
}
|
|
140
140
|
catch (err) {
|
|
141
|
-
console.error(
|
|
141
|
+
console.error(err.stack);
|
|
142
|
+
process.exit(2);
|
|
142
143
|
}
|
|
143
144
|
});
|
|
144
145
|
program
|
|
145
146
|
.command('flatten')
|
|
146
|
-
.description('
|
|
147
|
-
.
|
|
147
|
+
.description('Merges verified source files for a contract from a Blockchain explorer into one local file.')
|
|
148
|
+
.usage(`<contractAddress> [options]
|
|
149
|
+
|
|
150
|
+
In order for the merged code to compile, the following is done:
|
|
151
|
+
1. File imports are commented out.
|
|
152
|
+
2. "SPDX-License-Identifier" is renamed to "SPDX--License-Identifier".
|
|
153
|
+
3. Contract dependencies are analysed so the files are merged in an order that will compile.`)
|
|
154
|
+
.argument('<contractAddress>', 'Contract address in hexadecimal format with a 0x prefix.')
|
|
148
155
|
.action(async (contractAddress, options, command) => {
|
|
149
156
|
try {
|
|
150
157
|
debug(`About to flatten ${contractAddress}`);
|
|
@@ -159,7 +166,8 @@ program
|
|
|
159
166
|
await (0, writerFiles_1.writeSolidity)(solidityCode, outputFilename);
|
|
160
167
|
}
|
|
161
168
|
catch (err) {
|
|
162
|
-
console.error(
|
|
169
|
+
console.error(err);
|
|
170
|
+
process.exit(2);
|
|
163
171
|
}
|
|
164
172
|
});
|
|
165
173
|
program.on('option:verbose', () => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sol2uml",
|
|
3
|
-
"version": "2.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.4",
|
|
4
|
+
"description": "Solidity contract visualisation tool.",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
@@ -54,14 +54,15 @@
|
|
|
54
54
|
"UML",
|
|
55
55
|
"Solidity",
|
|
56
56
|
"Ethereum",
|
|
57
|
-
"Class diagram",
|
|
58
|
-
"Class",
|
|
59
57
|
"diagram",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"SVG",
|
|
58
|
+
"class",
|
|
59
|
+
"diagram",
|
|
60
|
+
"contract",
|
|
64
61
|
"Blockchain",
|
|
65
|
-
"
|
|
62
|
+
"storage",
|
|
63
|
+
"flatten",
|
|
64
|
+
"visual",
|
|
65
|
+
"tool",
|
|
66
|
+
"cli"
|
|
66
67
|
]
|
|
67
68
|
}
|