sol2uml 2.5.13 → 2.5.15
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 +8 -4
- package/lib/converterClasses2Storage.d.ts +7 -2
- package/lib/converterClasses2Storage.js +78 -28
- package/lib/converterStorage2Dot.js +7 -2
- package/lib/parserGeneral.d.ts +1 -1
- package/lib/parserGeneral.js +1 -4
- package/lib/slotValues.js +24 -8
- package/lib/sol2uml.js +17 -11
- package/lib/utils/formatters.d.ts +1 -0
- package/lib/utils/formatters.js +12 -0
- package/lib/utils/regEx.d.ts +1 -0
- package/lib/utils/regEx.js +2 -1
- package/lib/utils/validators.d.ts +6 -1
- package/lib/utils/validators.js +47 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
A visualisation tool for [Solidity](https://solidity.readthedocs.io/) contracts featuring:
|
|
7
7
|
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.
|
|
8
8
|
2. Contract storage layout diagrams.
|
|
9
|
+
3. Flatten Solidity files on Etherscan-like explorers to a local file.
|
|
10
|
+
4. Diff contracts on Etherscan-like explorers.
|
|
9
11
|
|
|
10
12
|
UML class diagram of Open Zeppelin's ERC20 token contracts generated from [version 4.7.3](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/v4.7.3/contracts/token/ERC20)
|
|
11
13
|

|
|
@@ -54,7 +56,7 @@ Options:
|
|
|
54
56
|
-sf, --subfolders <value> number of subfolders that will be recursively searched for Solidity files. (default: all)
|
|
55
57
|
-f, --outputFormat <value> output file format. (choices: "svg", "png", "dot", "all", default: "svg")
|
|
56
58
|
-o, --outputFileName <value> output file name
|
|
57
|
-
-i, --ignoreFilesOrFolders <
|
|
59
|
+
-i, --ignoreFilesOrFolders <names> comma-separated list of files or folders to ignore
|
|
58
60
|
-n, --network <network> Ethereum network which maps to a blockchain explorer (choices: "mainnet", "goerli", "sepolia", "polygon", "arbitrum", "avalanche", "bsc", "crono", "fantom", "moonbeam", "optimism", "gnosis", "celo", "base", default: "mainnet", env: ETH_NETWORK)
|
|
59
61
|
-e, --explorerUrl <url> Override the `network` option with a custom blockchain explorer API URL. eg Polygon Mumbai testnet https://api-testnet.polygonscan.com/api (env: EXPLORER_URL)
|
|
60
62
|
-k, --apiKey <key> Blockchain explorer API key. eg Etherscan, Arbiscan, Optimism, BscScan, CronoScan, FTMScan, PolygonScan or SnowTrace API key (env: SCAN_API_KEY)
|
|
@@ -104,7 +106,7 @@ Arguments:
|
|
|
104
106
|
sol2uml 0x79fEbF6B9F76853EDBcBc913e6aAE8232cFB9De9
|
|
105
107
|
|
|
106
108
|
Options:
|
|
107
|
-
-b, --baseContractNames <
|
|
109
|
+
-b, --baseContractNames <names> only output contracts connected to these comma separated base contract names
|
|
108
110
|
-d, --depth <value> depth of connected classes to the base contracts. 1 will only show directly connected contracts, interfaces, libraries, structs and enums. (default: all)
|
|
109
111
|
-c, --clusterFolders cluster contracts into source folders (default: false)
|
|
110
112
|
-hv, --hideVariables hide variables from contracts, interfaces, structs and enums (default: false)
|
|
@@ -149,9 +151,11 @@ Options:
|
|
|
149
151
|
-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.
|
|
150
152
|
-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)
|
|
151
153
|
-bn, --block <number> Block number to get the contract storage values from. (default: "latest")
|
|
154
|
+
-sn, --slotNames <names> Comma-separated list of slot names when accessed by assembly. The names can be a string, which will be hashed to a slot, or a 32 bytes hexadecimal string with a 0x prefix.
|
|
155
|
+
-st, --slotTypes <types> Comma-separated list of types for the slots listed in the `slotNames` option. eg address,uint256,bool. If all types are the same, a single type can be used. eg address (default: ["bytes32"])
|
|
152
156
|
-a, --array <number> Number of slots to display at the start and end of arrays. (default: "2")
|
|
153
157
|
-hx, --hideExpand <variables> Comma-separated list of storage variables to not expand. That's arrays, structs, strings or bytes.
|
|
154
|
-
-hv, --
|
|
158
|
+
-hv, --hideValues Hide storage slot value column. (default: false)
|
|
155
159
|
-h, --help display help for command
|
|
156
160
|
```
|
|
157
161
|
|
|
@@ -181,7 +185,7 @@ Options:
|
|
|
181
185
|
```
|
|
182
186
|
Usage: sol2uml diff [options] <addressA> <addressB or comma-separated folders>
|
|
183
187
|
|
|
184
|
-
Compare verified
|
|
188
|
+
Compare verified contract code on Etherscan-like explorers to another verified contract, a local file or multiple local files.
|
|
185
189
|
|
|
186
190
|
The results show the comparison of contract A to B.
|
|
187
191
|
The green sections are additions to contract B that are not in contract A.
|
|
@@ -9,8 +9,9 @@ export declare enum StorageSectionType {
|
|
|
9
9
|
}
|
|
10
10
|
export interface Variable {
|
|
11
11
|
id: number;
|
|
12
|
-
fromSlot
|
|
13
|
-
toSlot
|
|
12
|
+
fromSlot?: number;
|
|
13
|
+
toSlot?: number;
|
|
14
|
+
offset?: string;
|
|
14
15
|
byteSize: number;
|
|
15
16
|
byteOffset: number;
|
|
16
17
|
type: string;
|
|
@@ -45,6 +46,10 @@ export interface StorageSection {
|
|
|
45
46
|
* @return storageSections array of storageSection objects
|
|
46
47
|
*/
|
|
47
48
|
export declare const convertClasses2StorageSections: (contractName: string, umlClasses: UmlClass[], arrayItems: number, contractFilename?: string, noExpandVariables?: string[]) => StorageSection[];
|
|
49
|
+
export declare const optionStorageVariables: (contractName: string, slotNames?: {
|
|
50
|
+
name: string;
|
|
51
|
+
offset: string;
|
|
52
|
+
}[], slotTypes?: string[]) => Variable[];
|
|
48
53
|
/**
|
|
49
54
|
* Recursively adds new storage sections under a class attribute.
|
|
50
55
|
* also returns the allowed enum values
|
|
@@ -3,7 +3,7 @@ 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.addDynamicVariables = exports.findDimensionLength = exports.calcSectionOffset = exports.isElementary = exports.calcStorageByteSize = exports.parseStorageSectionFromAttribute = exports.convertClasses2StorageSections = exports.StorageSectionType = void 0;
|
|
6
|
+
exports.addDynamicVariables = exports.findDimensionLength = exports.calcSectionOffset = exports.isElementary = exports.calcStorageByteSize = exports.parseStorageSectionFromAttribute = exports.optionStorageVariables = exports.convertClasses2StorageSections = exports.StorageSectionType = void 0;
|
|
7
7
|
const umlClass_1 = require("./umlClass");
|
|
8
8
|
const associations_1 = require("./associations");
|
|
9
9
|
const utils_1 = require("ethers/lib/utils");
|
|
@@ -61,6 +61,53 @@ const convertClasses2StorageSections = (contractName, umlClasses, arrayItems, co
|
|
|
61
61
|
return storageSections;
|
|
62
62
|
};
|
|
63
63
|
exports.convertClasses2StorageSections = convertClasses2StorageSections;
|
|
64
|
+
const optionStorageVariables = (contractName, slotNames, slotTypes) => {
|
|
65
|
+
// If no slot names
|
|
66
|
+
if (!slotNames?.length) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
// The slotTypes default should mean this never happens
|
|
70
|
+
if (!slotTypes.length) {
|
|
71
|
+
throw Error(`The slotTypes option must be used with the slotNames option`);
|
|
72
|
+
}
|
|
73
|
+
if (slotNames.length > 1 && slotTypes.length === 1) {
|
|
74
|
+
slotTypes = Array(slotNames.length).fill(slotTypes[0]);
|
|
75
|
+
// slotTypes = slotTypes.fill(slotTypes[0], 1, slotNames.length - 1)
|
|
76
|
+
}
|
|
77
|
+
const variables = [];
|
|
78
|
+
slotNames.forEach((slotName, i) => {
|
|
79
|
+
const { size: byteSize, dynamic } = calcElementaryTypeSize(slotTypes[i]);
|
|
80
|
+
variables.push({
|
|
81
|
+
id: variableId++,
|
|
82
|
+
fromSlot: undefined,
|
|
83
|
+
toSlot: undefined,
|
|
84
|
+
offset: slotName.offset,
|
|
85
|
+
byteSize,
|
|
86
|
+
byteOffset: 0,
|
|
87
|
+
type: slotTypes[i],
|
|
88
|
+
attributeType: umlClass_1.AttributeType.Elementary,
|
|
89
|
+
dynamic,
|
|
90
|
+
getValue: true,
|
|
91
|
+
displayValue: true,
|
|
92
|
+
name: slotName.name,
|
|
93
|
+
contractName,
|
|
94
|
+
referenceSectionId: undefined,
|
|
95
|
+
enumValues: undefined,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
// Sort variables by offset hash
|
|
99
|
+
const sortedVariables = variables.sort((a, b) => {
|
|
100
|
+
if (a.offset < b.offset) {
|
|
101
|
+
return -1;
|
|
102
|
+
}
|
|
103
|
+
if (a.offset > b.offset) {
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
return 0;
|
|
107
|
+
});
|
|
108
|
+
return sortedVariables;
|
|
109
|
+
};
|
|
110
|
+
exports.optionStorageVariables = optionStorageVariables;
|
|
64
111
|
/**
|
|
65
112
|
* Recursively parse the storage variables for a given contract or struct.
|
|
66
113
|
* @param umlClass contract or file level struct
|
|
@@ -526,37 +573,40 @@ const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
|
|
|
526
573
|
}
|
|
527
574
|
}
|
|
528
575
|
if (attribute.attributeType === umlClass_1.AttributeType.Elementary) {
|
|
529
|
-
|
|
530
|
-
case 'bool':
|
|
531
|
-
return { size: 1, dynamic: false };
|
|
532
|
-
case 'address':
|
|
533
|
-
return { size: 20, dynamic: false };
|
|
534
|
-
case 'string':
|
|
535
|
-
case 'bytes':
|
|
536
|
-
return { size: 32, dynamic: true };
|
|
537
|
-
case 'uint':
|
|
538
|
-
case 'int':
|
|
539
|
-
case 'ufixed':
|
|
540
|
-
case 'fixed':
|
|
541
|
-
return { size: 32, dynamic: false };
|
|
542
|
-
default:
|
|
543
|
-
const result = attribute.type.match(/[u]*(int|fixed|bytes)([0-9]+)/);
|
|
544
|
-
if (result === null || !result[2]) {
|
|
545
|
-
throw Error(`Failed size elementary type "${attribute.type}"`);
|
|
546
|
-
}
|
|
547
|
-
// If bytes
|
|
548
|
-
if (result[1] === 'bytes') {
|
|
549
|
-
return { size: parseInt(result[2]), dynamic: false };
|
|
550
|
-
}
|
|
551
|
-
// TODO need to handle fixed types when they are supported
|
|
552
|
-
// If an int
|
|
553
|
-
const bitSize = parseInt(result[2]);
|
|
554
|
-
return { size: bitSize / 8, dynamic: false };
|
|
555
|
-
}
|
|
576
|
+
return calcElementaryTypeSize(attribute.type);
|
|
556
577
|
}
|
|
557
578
|
throw new Error(`Failed to calc bytes size of attribute with name "${attribute.name}" and type ${attribute.type}`);
|
|
558
579
|
};
|
|
559
580
|
exports.calcStorageByteSize = calcStorageByteSize;
|
|
581
|
+
const calcElementaryTypeSize = (type) => {
|
|
582
|
+
switch (type) {
|
|
583
|
+
case 'bool':
|
|
584
|
+
return { size: 1, dynamic: false };
|
|
585
|
+
case 'address':
|
|
586
|
+
return { size: 20, dynamic: false };
|
|
587
|
+
case 'string':
|
|
588
|
+
case 'bytes':
|
|
589
|
+
return { size: 32, dynamic: true };
|
|
590
|
+
case 'uint':
|
|
591
|
+
case 'int':
|
|
592
|
+
case 'ufixed':
|
|
593
|
+
case 'fixed':
|
|
594
|
+
return { size: 32, dynamic: false };
|
|
595
|
+
default:
|
|
596
|
+
const result = type.match(/[u]*(int|fixed|bytes)([0-9]+)/);
|
|
597
|
+
if (result === null || !result[2]) {
|
|
598
|
+
throw Error(`Failed size elementary type "${type}"`);
|
|
599
|
+
}
|
|
600
|
+
// If bytes
|
|
601
|
+
if (result[1] === 'bytes') {
|
|
602
|
+
return { size: parseInt(result[2]), dynamic: false };
|
|
603
|
+
}
|
|
604
|
+
// TODO need to handle fixed types when they are supported
|
|
605
|
+
// If an int
|
|
606
|
+
const bitSize = parseInt(result[2]);
|
|
607
|
+
return { size: bitSize / 8, dynamic: false };
|
|
608
|
+
}
|
|
609
|
+
};
|
|
560
610
|
const isElementary = (type) => {
|
|
561
611
|
switch (type) {
|
|
562
612
|
case 'bool':
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.convertStorage2Dot = exports.convertStorages2Dot = void 0;
|
|
4
4
|
const converterClasses2Storage_1 = require("./converterClasses2Storage");
|
|
5
5
|
const umlClass_1 = require("./umlClass");
|
|
6
|
+
const formatters_1 = require("./utils/formatters");
|
|
6
7
|
const debug = require('debug')('sol2uml');
|
|
7
8
|
const convertStorages2Dot = (storageSections, options) => {
|
|
8
9
|
let dotString = `
|
|
@@ -46,7 +47,10 @@ function convertStorage2Dot(storageSection, dotString, options) {
|
|
|
46
47
|
: `{ slot${dataLine}`;
|
|
47
48
|
startingVariables.forEach((variable, i) => {
|
|
48
49
|
const dataLine = options.data && displayData[i] ? linePad : '';
|
|
49
|
-
if (variable.
|
|
50
|
+
if (variable.offset) {
|
|
51
|
+
dotString += ` | ${(0, formatters_1.shortBytes32)(variable.offset)}${dataLine}`;
|
|
52
|
+
}
|
|
53
|
+
else if (variable.fromSlot === variable.toSlot) {
|
|
50
54
|
dotString += ` | ${variable.fromSlot}${dataLine}`;
|
|
51
55
|
}
|
|
52
56
|
else {
|
|
@@ -73,7 +77,8 @@ function convertStorage2Dot(storageSection, dotString, options) {
|
|
|
73
77
|
// For each slot
|
|
74
78
|
startingVariables.forEach((variable) => {
|
|
75
79
|
// Get all the storage variables in this slot
|
|
76
|
-
const slotVariables = storageSection.variables.filter((s) => s.fromSlot === variable.fromSlot)
|
|
80
|
+
const slotVariables = storageSection.variables.filter((s) => (!s.offset && s.fromSlot === variable.fromSlot) ||
|
|
81
|
+
(s.offset && s.offset === variable.offset));
|
|
77
82
|
const usedBytes = slotVariables.reduce((acc, s) => acc + s.byteSize, 0);
|
|
78
83
|
if (usedBytes < 32) {
|
|
79
84
|
// Create an unallocated variable for display purposes
|
package/lib/parserGeneral.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface ParserOptions {
|
|
|
5
5
|
network?: Network;
|
|
6
6
|
explorerUrl?: string;
|
|
7
7
|
subfolders?: string;
|
|
8
|
-
ignoreFilesOrFolders?: string;
|
|
8
|
+
ignoreFilesOrFolders?: string[];
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* Parses Solidity source code from a local filesystem or verified code on Etherscan
|
package/lib/parserGeneral.js
CHANGED
|
@@ -27,10 +27,7 @@ const parserUmlClasses = async (fileFolderAddress, options) => {
|
|
|
27
27
|
process.exit(1);
|
|
28
28
|
}
|
|
29
29
|
const filesFolders = fileFolderAddress.split(',');
|
|
30
|
-
|
|
31
|
-
? options.ignoreFilesOrFolders.split(',')
|
|
32
|
-
: [];
|
|
33
|
-
result.umlClasses = await (0, parserFiles_1.parseUmlClassesFromFiles)(filesFolders, ignoreFilesFolders, subfolders);
|
|
30
|
+
result.umlClasses = await (0, parserFiles_1.parseUmlClassesFromFiles)(filesFolders, options.ignoreFilesOrFolders || [], subfolders);
|
|
34
31
|
}
|
|
35
32
|
return result;
|
|
36
33
|
};
|
package/lib/slotValues.js
CHANGED
|
@@ -28,13 +28,18 @@ const addSlotValues = async (url, contractAddress, storageSection, arrayItems, b
|
|
|
28
28
|
// for each variable, add all the slots used by the variable.
|
|
29
29
|
const slots = [];
|
|
30
30
|
valueVariables.forEach((variable) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
if (variable.offset) {
|
|
32
|
+
slots.push(bignumber_1.BigNumber.from(variable.offset));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
for (let i = 0; variable.fromSlot + i <= variable.toSlot; i++) {
|
|
36
|
+
if (variable.attributeType === umlClass_1.AttributeType.Array &&
|
|
37
|
+
i >= arrayItems &&
|
|
38
|
+
i < variable.toSlot - arrayItems) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
slots.push(variable.fromSlot + i);
|
|
36
42
|
}
|
|
37
|
-
slots.push(variable.fromSlot + i);
|
|
38
43
|
}
|
|
39
44
|
});
|
|
40
45
|
// remove duplicate slot numbers
|
|
@@ -54,7 +59,17 @@ const addSlotValues = async (url, contractAddress, storageSection, arrayItems, b
|
|
|
54
59
|
const fromSlot = uniqueFromSlots[i];
|
|
55
60
|
// For each variable in the storage section
|
|
56
61
|
for (const variable of storageSection.variables) {
|
|
57
|
-
if (variable.getValue &&
|
|
62
|
+
if (variable.getValue &&
|
|
63
|
+
variable.offset &&
|
|
64
|
+
bignumber_1.BigNumber.from(variable.offset).eq(fromSlot)) {
|
|
65
|
+
debug(`Set slot value ${value} for section "${storageSection.name}", var type ${variable.type}, slot ${variable.offset}`);
|
|
66
|
+
variable.slotValue = value;
|
|
67
|
+
// parse variable value from slot data
|
|
68
|
+
if (variable.displayValue) {
|
|
69
|
+
variable.parsedValue = (0, exports.parseValue)(variable);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (variable.getValue && variable.fromSlot === fromSlot) {
|
|
58
73
|
debug(`Set slot value ${value} for section "${storageSection.name}", var type ${variable.type}, slot ${variable.fromSlot} offset ${storageSection.offset}`);
|
|
59
74
|
variable.slotValue = value;
|
|
60
75
|
// parse variable value from slot data
|
|
@@ -63,7 +78,8 @@ const addSlotValues = async (url, contractAddress, storageSection, arrayItems, b
|
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
80
|
// if variable is past the slot that has the value
|
|
66
|
-
else if (
|
|
81
|
+
else if (variable.toSlot &&
|
|
82
|
+
bignumber_1.BigNumber.from(variable.toSlot).gt(fromSlot)) {
|
|
67
83
|
break;
|
|
68
84
|
}
|
|
69
85
|
}
|
package/lib/sol2uml.js
CHANGED
|
@@ -29,7 +29,7 @@ Can also flatten or compare verified source files on Etherscan-like explorers.`)
|
|
|
29
29
|
.choices(['svg', 'png', 'dot', 'all'])
|
|
30
30
|
.default('svg'))
|
|
31
31
|
.option('-o, --outputFileName <value>', 'output file name')
|
|
32
|
-
.option('-i, --ignoreFilesOrFolders <
|
|
32
|
+
.option('-i, --ignoreFilesOrFolders <names>', 'comma-separated list of files or folders to ignore', validators_1.validateNames)
|
|
33
33
|
.addOption(new commander_1.Option('-n, --network <network>', 'Ethereum network which maps to a blockchain explorer')
|
|
34
34
|
.choices(parserEtherscan_1.networks)
|
|
35
35
|
.default('mainnet')
|
|
@@ -56,7 +56,7 @@ program
|
|
|
56
56
|
.usage('[options] <fileFolderAddress>')
|
|
57
57
|
.description('Generates a UML class diagram from Solidity source code.')
|
|
58
58
|
.argument('fileFolderAddress', argumentText)
|
|
59
|
-
.option('-b, --baseContractNames <
|
|
59
|
+
.option('-b, --baseContractNames <names>', 'only output contracts connected to these comma-separated base contract names', validators_1.validateNames)
|
|
60
60
|
.addOption(new commander_1.Option('-d, --depth <value>', 'depth of connected classes to the base contracts. 1 will only show directly connected contracts, interfaces, libraries, structs and enums.').default('100', 'all'))
|
|
61
61
|
.option('-c, --clusterFolders', 'cluster contracts into source folders', false)
|
|
62
62
|
.option('-hv, --hideVariables', 'hide variables from contracts, interfaces, structs and enums', false)
|
|
@@ -91,19 +91,18 @@ program
|
|
|
91
91
|
if (options.squash && options.hideContracts) {
|
|
92
92
|
throw Error('Can not hide contracts when squashing contracts.');
|
|
93
93
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
contractName = baseContractNames[0];
|
|
94
|
+
if (options.baseContractNames) {
|
|
95
|
+
contractName = options.baseContractNames[0];
|
|
97
96
|
}
|
|
98
97
|
// Filter out any class stereotypes that are to be hidden
|
|
99
98
|
let filteredUmlClasses = (0, filterClasses_1.filterHiddenClasses)(umlClasses, options);
|
|
100
99
|
// squash contracts
|
|
101
100
|
if (options.squash) {
|
|
102
|
-
filteredUmlClasses = (0, squashClasses_1.squashUmlClasses)(filteredUmlClasses, baseContractNames || [contractName]);
|
|
101
|
+
filteredUmlClasses = (0, squashClasses_1.squashUmlClasses)(filteredUmlClasses, options.baseContractNames || [contractName]);
|
|
103
102
|
}
|
|
104
|
-
if (baseContractNames || options.squash) {
|
|
103
|
+
if (options.baseContractNames || options.squash) {
|
|
105
104
|
// Find all the classes connected to the base classes after they have been squashed
|
|
106
|
-
filteredUmlClasses = (0, filterClasses_1.classesConnectedToBaseContracts)(filteredUmlClasses, baseContractNames || [contractName], options.depth);
|
|
105
|
+
filteredUmlClasses = (0, filterClasses_1.classesConnectedToBaseContracts)(filteredUmlClasses, options.baseContractNames || [contractName], options.depth);
|
|
107
106
|
}
|
|
108
107
|
// Convert UML classes to Graphviz dot format.
|
|
109
108
|
const dotString = (0, converterClasses2Dot_1.convertUmlClasses2Dot)(filteredUmlClasses, combinedOptions.clusterFolders, combinedOptions);
|
|
@@ -131,9 +130,11 @@ WARNING: sol2uml does not use the Solidity compiler so may differ with solc. A k
|
|
|
131
130
|
.env('NODE_URL')
|
|
132
131
|
.default('http://localhost:8545'))
|
|
133
132
|
.option('-bn, --block <number>', 'Block number to get the contract storage values from.', 'latest')
|
|
133
|
+
.option('-sn, --slotNames <names>', 'Comma-separated list of slot names when accessed by assembly. The names can be a string, which will be hashed to a slot, or a 32 bytes hexadecimal string with a 0x prefix.', validators_1.validateSlotNames)
|
|
134
|
+
.option('-st, --slotTypes <types>', 'Comma-separated list of types for the slots listed in the `slotNames` option. eg address,uint256,bool. If all types are the same, a single type can be used. eg address', validators_1.validateTypes, ['bytes32'])
|
|
134
135
|
.option('-a, --array <number>', 'Number of slots to display at the start and end of arrays.', '2')
|
|
135
|
-
.option('-hx, --hideExpand <variables>', "Comma-separated list of storage variables to not expand. That's arrays, structs, strings or bytes.", validators_1.
|
|
136
|
-
.option('-hv, --
|
|
136
|
+
.option('-hx, --hideExpand <variables>', "Comma-separated list of storage variables to not expand. That's arrays, structs, strings or bytes.", validators_1.validateNames)
|
|
137
|
+
.option('-hv, --hideValues', 'Hide storage slot value column.', false)
|
|
137
138
|
.action(async (fileFolderAddress, options, command) => {
|
|
138
139
|
try {
|
|
139
140
|
const combinedOptions = {
|
|
@@ -148,6 +149,11 @@ WARNING: sol2uml does not use the Solidity compiler so may differ with solc. A k
|
|
|
148
149
|
contractName = combinedOptions.contract || contractName;
|
|
149
150
|
const arrayItems = parseInt(combinedOptions.array);
|
|
150
151
|
const storageSections = (0, converterClasses2Storage_1.convertClasses2StorageSections)(contractName, umlClasses, arrayItems, combinedOptions.contractFile, options.hideExpand);
|
|
152
|
+
const optionVariables = (0, converterClasses2Storage_1.optionStorageVariables)(contractName, options.slotNames, options.slotTypes);
|
|
153
|
+
storageSections[0].variables = [
|
|
154
|
+
...storageSections[0].variables,
|
|
155
|
+
...optionVariables,
|
|
156
|
+
];
|
|
151
157
|
if ((0, regEx_1.isAddress)(fileFolderAddress)) {
|
|
152
158
|
// The first storage is the contract
|
|
153
159
|
storageSections[0].address = fileFolderAddress;
|
|
@@ -219,7 +225,7 @@ In order for the merged code to compile, the following is done:
|
|
|
219
225
|
program
|
|
220
226
|
.command('diff')
|
|
221
227
|
.usage('[options] <addressA> <addressB or comma-separated folders>')
|
|
222
|
-
.description(`Compare verified
|
|
228
|
+
.description(`Compare verified contract code on Etherscan-like explorers to another verified contract, a local file or multiple local files.
|
|
223
229
|
|
|
224
230
|
The results show the comparison of contract A to B.
|
|
225
231
|
The ${clc.green('green')} sections are additions to contract B that are not in contract A.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const shortBytes32: (bytes32: string) => string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shortBytes32 = void 0;
|
|
4
|
+
const shortBytes32 = (bytes32) => {
|
|
5
|
+
if (!bytes32)
|
|
6
|
+
return '';
|
|
7
|
+
if (typeof bytes32 !== 'string' || bytes32.length !== 66)
|
|
8
|
+
return bytes32;
|
|
9
|
+
return bytes32.slice(0, 5) + '..' + bytes32.slice(-3);
|
|
10
|
+
};
|
|
11
|
+
exports.shortBytes32 = shortBytes32;
|
|
12
|
+
//# sourceMappingURL=formatters.js.map
|
package/lib/utils/regEx.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const ethereumAddress: RegExp;
|
|
2
2
|
export declare const ethereumAddresses: RegExp;
|
|
3
|
+
export declare const bytes32: RegExp;
|
|
3
4
|
export declare const commaSeparatedList: RegExp;
|
|
4
5
|
export declare const isAddress: (input: string) => boolean;
|
|
5
6
|
export declare const parseSolidityVersion: (compilerVersion: string) => string;
|
package/lib/utils/regEx.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseSolidityVersion = exports.isAddress = exports.commaSeparatedList = exports.ethereumAddresses = exports.ethereumAddress = void 0;
|
|
3
|
+
exports.parseSolidityVersion = exports.isAddress = exports.commaSeparatedList = exports.bytes32 = exports.ethereumAddresses = exports.ethereumAddress = void 0;
|
|
4
4
|
exports.ethereumAddress = /^0x([A-Fa-f0-9]{40})$/;
|
|
5
5
|
// comma-separated list of addresses with no whitespace
|
|
6
6
|
exports.ethereumAddresses = /^(0x[A-Fa-f0-9]{40},?)+$/;
|
|
7
|
+
exports.bytes32 = /^0x([A-Fa-f0-9]{64})$/;
|
|
7
8
|
// comma-separated list of names with no whitespace
|
|
8
9
|
exports.commaSeparatedList = /^[^,\s]+(,[^,\s]+)*$/;
|
|
9
10
|
const isAddress = (input) => {
|
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export declare const validateAddress: (address: string) => string;
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const validateNames: (variables: string) => string[];
|
|
3
3
|
export declare const validateLineBuffer: (lineBufferParam: string) => number;
|
|
4
|
+
export declare const validateSlotNames: (slotNames: string) => {
|
|
5
|
+
name: string;
|
|
6
|
+
offset: string;
|
|
7
|
+
}[];
|
|
8
|
+
export declare const validateTypes: (typesString: string) => string[];
|
package/lib/utils/validators.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.validateLineBuffer = exports.
|
|
3
|
+
exports.validateTypes = exports.validateSlotNames = exports.validateLineBuffer = exports.validateNames = exports.validateAddress = void 0;
|
|
4
4
|
const regEx_1 = require("./regEx");
|
|
5
5
|
const commander_1 = require("commander");
|
|
6
6
|
const utils_1 = require("ethers/lib/utils");
|
|
7
|
+
const converterClasses2Storage_1 = require("../converterClasses2Storage");
|
|
8
|
+
const debug = require('debug')('sol2uml');
|
|
7
9
|
const validateAddress = (address) => {
|
|
8
10
|
try {
|
|
9
11
|
if (typeof address === 'string' && address?.match(regEx_1.ethereumAddress))
|
|
@@ -13,16 +15,17 @@ const validateAddress = (address) => {
|
|
|
13
15
|
throw new commander_1.InvalidArgumentError(`Address must be in hexadecimal format with a 0x prefix.`);
|
|
14
16
|
};
|
|
15
17
|
exports.validateAddress = validateAddress;
|
|
16
|
-
|
|
18
|
+
// Splits a comma-separated list of names.
|
|
19
|
+
const validateNames = (variables) => {
|
|
17
20
|
try {
|
|
18
21
|
if (typeof variables === 'string' &&
|
|
19
22
|
variables.match(regEx_1.commaSeparatedList))
|
|
20
23
|
return variables.split(',');
|
|
21
24
|
}
|
|
22
25
|
catch (err) { }
|
|
23
|
-
throw new commander_1.InvalidArgumentError(`Must be a comma-separate list of
|
|
26
|
+
throw new commander_1.InvalidArgumentError(`Must be a comma-separate list of names with no white spaces.`);
|
|
24
27
|
};
|
|
25
|
-
exports.
|
|
28
|
+
exports.validateNames = validateNames;
|
|
26
29
|
const validateLineBuffer = (lineBufferParam) => {
|
|
27
30
|
try {
|
|
28
31
|
const lineBuffer = parseInt(lineBufferParam, 10);
|
|
@@ -33,4 +36,44 @@ const validateLineBuffer = (lineBufferParam) => {
|
|
|
33
36
|
throw new commander_1.InvalidOptionArgumentError(`Must be a zero or a positive integer.`);
|
|
34
37
|
};
|
|
35
38
|
exports.validateLineBuffer = validateLineBuffer;
|
|
39
|
+
const validateSlotNames = (slotNames) => {
|
|
40
|
+
try {
|
|
41
|
+
const slots = slotNames.split(',');
|
|
42
|
+
const results = slots.map((slot) => {
|
|
43
|
+
if (slot.match(regEx_1.bytes32)) {
|
|
44
|
+
return {
|
|
45
|
+
name: undefined,
|
|
46
|
+
offset: slot,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const offset = (0, utils_1.keccak256)((0, utils_1.toUtf8Bytes)(slot));
|
|
50
|
+
debug(`Slot name "${slot}" has hash "${offset}"`);
|
|
51
|
+
return {
|
|
52
|
+
name: slot,
|
|
53
|
+
offset,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
console.log(results.length);
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
catch (err) { }
|
|
60
|
+
throw new commander_1.InvalidOptionArgumentError(`Must be a comma-separate list of slots with no white spaces.`);
|
|
61
|
+
};
|
|
62
|
+
exports.validateSlotNames = validateSlotNames;
|
|
63
|
+
const validateTypes = (typesString) => {
|
|
64
|
+
try {
|
|
65
|
+
if (typeof typesString === 'string') {
|
|
66
|
+
const types = typesString.split(',');
|
|
67
|
+
types.forEach((type) => {
|
|
68
|
+
if (!(0, converterClasses2Storage_1.isElementary)(type)) {
|
|
69
|
+
throw Error(`"${type}" is not an elementary type`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return types;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (err) { }
|
|
76
|
+
throw new commander_1.InvalidArgumentError(`Slot type must be an elementary type which includes dynamic and fixed size arrays. eg address, address[], uint256, int256[2], bytes32, string, bool`);
|
|
77
|
+
};
|
|
78
|
+
exports.validateTypes = validateTypes;
|
|
36
79
|
//# sourceMappingURL=validators.js.map
|