sol2uml 2.5.22 → 2.5.24
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 +6 -6
- package/lib/SlotValueCache.d.ts +1 -1
- package/lib/SlotValueCache.js +4 -4
- package/lib/associations.d.ts +1 -1
- package/lib/associations.js +96 -94
- package/lib/converterAST2Classes.js +9 -7
- package/lib/converterClass2Dot.js +1 -1
- package/lib/converterClasses2Storage.d.ts +1 -1
- package/lib/converterClasses2Storage.js +13 -11
- package/lib/diffContracts.d.ts +1 -1
- package/lib/diffContracts.js +16 -7
- package/lib/parserEtherscan.js +1 -1
- package/lib/parserFiles.js +2 -2
- package/lib/slotValues.d.ts +1 -1
- package/lib/slotValues.js +43 -41
- package/lib/sol2uml.js +1 -1
- package/lib/squashClasses.js +2 -2
- package/lib/utils/block.js +3 -3
- package/lib/utils/diff.js +8 -5
- package/lib/utils/validators.js +8 -8
- package/lib/writerFiles.js +5 -2
- package/package.json +15 -9
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Solidity 2 UML
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/sol2uml)
|
|
4
|
-
[](https://x.com/naddison)
|
|
5
5
|
|
|
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.
|
|
@@ -22,7 +22,7 @@ See an explanation of how storage diagrams work with lots of examples [here](./e
|
|
|
22
22
|
# Install
|
|
23
23
|
|
|
24
24
|
The following installation assumes [Node.js](https://nodejs.org/en/download/) has already been installed which comes with [Node Package Manager (NPM)](https://www.npmjs.com/).
|
|
25
|
-
`sol2uml` works with node
|
|
25
|
+
`sol2uml` works with node 20 or above.
|
|
26
26
|
|
|
27
27
|
To install globally so you can run `sol2uml` from anywhere
|
|
28
28
|
|
|
@@ -58,7 +58,7 @@ Options:
|
|
|
58
58
|
-o, --outputFileName <value> output file name
|
|
59
59
|
-i, --ignoreFilesOrFolders <names> comma-separated list of files or folders to ignore
|
|
60
60
|
-n, --network <network> Name or chain id of the blockchain explorer. A name like `ethereum` or `base` will map to a chain id, eg 1 or 8453. Alternatively, use an integer of the chain id. Supported names: ethereum, sepolia, holesky, hoodi, arbitrum, optimism, polygon, avalanche, base, bsc, crono, fantom, sonic, gnosis, moonbeam, celo, scroll, linea, blast, berachain, zksync (default: "ethereum", env: ETH_NETWORK)
|
|
61
|
-
-e, --explorerUrl <url> Override the `network` option with a custom blockchain explorer API URL. eg Polygon
|
|
61
|
+
-e, --explorerUrl <url> Override the `network` option with a custom blockchain explorer API URL. eg Polygon Amoy testnet https://api-amoy.polygonscan.com/api (env: EXPLORER_URL)
|
|
62
62
|
-k, --apiKey <key> Blockchain explorer API key. (env: SCAN_API_KEY)
|
|
63
63
|
-bc, --backColor <color> Canvas background color. "none" will use a transparent canvas. (default: "white")
|
|
64
64
|
-sc, --shapeColor <color> Basic drawing color for graphics, not text (default: "black")
|
|
@@ -225,10 +225,10 @@ To generate a diagram of EtherDelta's contract from the verified source code on
|
|
|
225
225
|
sol2uml class 0x8d12A197cB00D4747a1fe03395095ce2A5CC6819
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
-
To generate a diagram of
|
|
228
|
+
To generate a diagram of a contract on [Hoodi](https://hoodi.etherscan.io/address/0x8d62350d6DfC8A928bBF5efD2e44c66034Afa7C6#code). The output will be a svg file `0x8d62350d6DfC8A928bBF5efD2e44c66034Afa7C6.svg` in the working folder.
|
|
229
229
|
|
|
230
230
|
```bash
|
|
231
|
-
sol2uml class
|
|
231
|
+
sol2uml class 0x8d62350d6DfC8A928bBF5efD2e44c66034Afa7C6 -n hoodi
|
|
232
232
|
```
|
|
233
233
|
|
|
234
234
|
To generate all Solidity files under some root folder and output the svg file to a specific location
|
|
@@ -354,7 +354,7 @@ npm run clean
|
|
|
354
354
|
npm run package-lock
|
|
355
355
|
npm run build
|
|
356
356
|
npm run permit
|
|
357
|
-
# make
|
|
357
|
+
# make sol2uml globally available for local testing
|
|
358
358
|
npm link
|
|
359
359
|
# check all the files are included in the npm package
|
|
360
360
|
npm pack --dry-run
|
package/lib/SlotValueCache.d.ts
CHANGED
package/lib/SlotValueCache.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SlotValueCache = void 0;
|
|
4
|
-
const
|
|
4
|
+
const ethers_1 = require("ethers");
|
|
5
5
|
const debug = require('debug')('sol2uml');
|
|
6
6
|
/**
|
|
7
7
|
* Singleton that caches a mapping of slot keys to values.
|
|
@@ -16,8 +16,8 @@ class SlotValueCache {
|
|
|
16
16
|
static readSlotValues(slotKeys) {
|
|
17
17
|
const cachedValues = [];
|
|
18
18
|
const missingKeys = [];
|
|
19
|
-
slotKeys.forEach((slotKey
|
|
20
|
-
const key =
|
|
19
|
+
slotKeys.forEach((slotKey) => {
|
|
20
|
+
const key = (0, ethers_1.toBeHex)(BigInt(slotKey));
|
|
21
21
|
if (this.slotCache[key]) {
|
|
22
22
|
cachedValues.push(this.slotCache[key]);
|
|
23
23
|
}
|
|
@@ -46,7 +46,7 @@ class SlotValueCache {
|
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
return slotKeys.map((slotKey) => {
|
|
49
|
-
const key =
|
|
49
|
+
const key = (0, ethers_1.toBeHex)(BigInt(slotKey));
|
|
50
50
|
// it should find the slot value in the cache. if not it'll return undefined
|
|
51
51
|
return this.slotCache[key];
|
|
52
52
|
});
|
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: readonly UmlClass[]
|
|
2
|
+
export declare const findAssociatedClass: (association: Association, sourceUmlClass: UmlClass, umlClasses: readonly UmlClass[]) => UmlClass | undefined;
|
package/lib/associations.js
CHANGED
|
@@ -2,34 +2,18 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.findAssociatedClass = void 0;
|
|
4
4
|
// Find the UML class linked to the association
|
|
5
|
-
const findAssociatedClass = (association, sourceUmlClass, umlClasses
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// // add to already recursively processed files to avoid getting stuck in circular imports
|
|
18
|
-
// searchedAbsolutePaths.push(sourceUmlClass.absolutePath)
|
|
19
|
-
// const importedType = findChainedImport(
|
|
20
|
-
// association,
|
|
21
|
-
// sourceUmlClass,
|
|
22
|
-
// umlClasses,
|
|
23
|
-
// searchedAbsolutePaths,
|
|
24
|
-
// )
|
|
25
|
-
// if (importedType) return importedType
|
|
26
|
-
// // Still could not find association so now need to recursively look for inherited types
|
|
27
|
-
// const inheritedType = findInheritedType(
|
|
28
|
-
// association,
|
|
29
|
-
// sourceUmlClass,
|
|
30
|
-
// umlClasses,
|
|
31
|
-
// )
|
|
32
|
-
// if (inheritedType) return inheritedType
|
|
5
|
+
const findAssociatedClass = (association, sourceUmlClass, umlClasses) => {
|
|
6
|
+
// Phase 1: Iterative BFS through import chain, trying direct match at each level
|
|
7
|
+
const { result, visitedSources } = _findViaImportChain(association, sourceUmlClass, umlClasses);
|
|
8
|
+
if (result)
|
|
9
|
+
return result;
|
|
10
|
+
// Phase 2: Try inherited types for each source visited during import chain traversal
|
|
11
|
+
const visitedClassIds = new Set();
|
|
12
|
+
for (const source of visitedSources) {
|
|
13
|
+
const inherited = _findInheritedType(association, source, umlClasses, visitedClassIds);
|
|
14
|
+
if (inherited)
|
|
15
|
+
return inherited;
|
|
16
|
+
}
|
|
33
17
|
return undefined;
|
|
34
18
|
};
|
|
35
19
|
exports.findAssociatedClass = findAssociatedClass;
|
|
@@ -83,15 +67,91 @@ const isAssociated = (association, sourceUmlClass, targetUmlClass, targetParentU
|
|
|
83
67
|
importedClass.alias &&
|
|
84
68
|
importedClass.className === targetUmlClass.name))));
|
|
85
69
|
};
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
// Try to find a direct match for the association from the given source class
|
|
71
|
+
const _tryDirectMatch = (association, sourceUmlClass, umlClasses) => {
|
|
72
|
+
return umlClasses.find((targetUmlClass) => {
|
|
73
|
+
const targetParentClass = association.parentUmlClassName &&
|
|
74
|
+
targetUmlClass.parentId !== undefined
|
|
75
|
+
? umlClasses[targetUmlClass.parentId]
|
|
76
|
+
: undefined;
|
|
77
|
+
return isAssociated(association, sourceUmlClass, targetUmlClass, targetParentClass);
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
// Iterative BFS through import chain, trying direct match at each level.
|
|
81
|
+
// Returns the matched class (if found) and the list of source classes visited.
|
|
82
|
+
const _findViaImportChain = (association, sourceUmlClass, umlClasses) => {
|
|
83
|
+
const searched = new Set();
|
|
84
|
+
const visitedSources = [];
|
|
85
|
+
const visitedPaths = new Set();
|
|
86
|
+
const queue = [
|
|
87
|
+
{
|
|
88
|
+
source: sourceUmlClass,
|
|
89
|
+
targetName: association.targetUmlClassName,
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
while (queue.length > 0) {
|
|
93
|
+
const { source, targetName } = queue.shift();
|
|
94
|
+
const key = `${source.absolutePath}::${targetName}`;
|
|
95
|
+
if (searched.has(key))
|
|
96
|
+
continue;
|
|
97
|
+
searched.add(key);
|
|
98
|
+
// Track unique visited sources for phase 2 inherited type lookup
|
|
99
|
+
if (!visitedPaths.has(source.absolutePath)) {
|
|
100
|
+
visitedPaths.add(source.absolutePath);
|
|
101
|
+
visitedSources.push(source);
|
|
102
|
+
}
|
|
103
|
+
// Build association with potentially de-aliased target name
|
|
104
|
+
const currentAssoc = {
|
|
105
|
+
...association,
|
|
106
|
+
targetUmlClassName: targetName,
|
|
107
|
+
};
|
|
108
|
+
// Try direct match from this source
|
|
109
|
+
const match = _tryDirectMatch(currentAssoc, source, umlClasses);
|
|
110
|
+
if (match)
|
|
111
|
+
return { result: match, visitedSources };
|
|
112
|
+
// Get imports that could lead to the target
|
|
113
|
+
const imports = source.imports.filter((i) => i.classNames.length === 0 ||
|
|
114
|
+
i.classNames.some((cn) => (targetName === cn.className && !cn.alias) ||
|
|
115
|
+
targetName === cn.alias));
|
|
116
|
+
for (const importDetail of imports) {
|
|
117
|
+
const importedClass = umlClasses.find((c) => c.absolutePath === importDetail.absolutePath);
|
|
118
|
+
if (!importedClass)
|
|
119
|
+
continue;
|
|
120
|
+
// Queue with current target name to continue the chain
|
|
121
|
+
const origKey = `${importedClass.absolutePath}::${targetName}`;
|
|
122
|
+
if (!searched.has(origKey)) {
|
|
123
|
+
queue.push({ source: importedClass, targetName });
|
|
124
|
+
}
|
|
125
|
+
// Queue with de-aliased names for aliased imports
|
|
126
|
+
for (const cn of importDetail.classNames) {
|
|
127
|
+
if (cn.alias && targetName === cn.alias) {
|
|
128
|
+
const deAliasedKey = `${importedClass.absolutePath}::${cn.className}`;
|
|
129
|
+
if (!searched.has(deAliasedKey)) {
|
|
130
|
+
queue.push({
|
|
131
|
+
source: importedClass,
|
|
132
|
+
targetName: cn.className,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { visitedSources };
|
|
140
|
+
};
|
|
141
|
+
// Walk the inheritance chain to find types (structs, enums) defined on parent contracts.
|
|
142
|
+
// Uses visitedClassIds to prevent re-processing in diamond inheritance hierarchies.
|
|
143
|
+
const _findInheritedType = (association, sourceUmlClass, umlClasses, visitedClassIds) => {
|
|
144
|
+
if (visitedClassIds.has(sourceUmlClass.id))
|
|
145
|
+
return undefined;
|
|
146
|
+
visitedClassIds.add(sourceUmlClass.id);
|
|
88
147
|
const parentAssociations = sourceUmlClass.getParentContracts();
|
|
89
|
-
// For each parent association
|
|
90
148
|
for (const parentAssociation of parentAssociations) {
|
|
91
|
-
|
|
149
|
+
// Resolve the parent class using import chain only (no inherited types)
|
|
150
|
+
// to avoid mutual recursion between findAssociatedClass and _findInheritedType
|
|
151
|
+
const { result: parent } = _findViaImportChain(parentAssociation, sourceUmlClass, umlClasses);
|
|
92
152
|
if (!parent)
|
|
93
153
|
continue;
|
|
94
|
-
//
|
|
154
|
+
// Check parent's structs for the target type
|
|
95
155
|
for (const structId of parent.structs) {
|
|
96
156
|
const structUmlClass = umlClasses.find((c) => c.id === structId);
|
|
97
157
|
if (!structUmlClass)
|
|
@@ -100,7 +160,7 @@ const findInheritedType = (association, sourceUmlClass, umlClasses) => {
|
|
|
100
160
|
return structUmlClass;
|
|
101
161
|
}
|
|
102
162
|
}
|
|
103
|
-
//
|
|
163
|
+
// Check parent's enums for the target type
|
|
104
164
|
for (const enumId of parent.enums) {
|
|
105
165
|
const enumUmlClass = umlClasses.find((c) => c.id === enumId);
|
|
106
166
|
if (!enumUmlClass)
|
|
@@ -109,69 +169,11 @@ const findInheritedType = (association, sourceUmlClass, umlClasses) => {
|
|
|
109
169
|
return enumUmlClass;
|
|
110
170
|
}
|
|
111
171
|
}
|
|
112
|
-
// Recursively
|
|
113
|
-
const targetClass =
|
|
172
|
+
// Recursively check parent's parents
|
|
173
|
+
const targetClass = _findInheritedType(association, parent, umlClasses, visitedClassIds);
|
|
114
174
|
if (targetClass)
|
|
115
175
|
return targetClass;
|
|
116
176
|
}
|
|
117
177
|
return undefined;
|
|
118
178
|
};
|
|
119
|
-
// const findChainedImport = (
|
|
120
|
-
// association: Association,
|
|
121
|
-
// sourceUmlClass: UmlClass,
|
|
122
|
-
// umlClasses: readonly UmlClass[],
|
|
123
|
-
// searchedRelativePaths: string[],
|
|
124
|
-
// ): UmlClass | undefined => {
|
|
125
|
-
// // Get all valid imports. That is, imports that do not explicitly import contracts or interfaces
|
|
126
|
-
// // or explicitly import the source class
|
|
127
|
-
// const imports = sourceUmlClass.imports.filter(
|
|
128
|
-
// (i) =>
|
|
129
|
-
// i.classNames.length === 0 ||
|
|
130
|
-
// i.classNames.some(
|
|
131
|
-
// (cn) =>
|
|
132
|
-
// (association.targetUmlClassName === cn.className &&
|
|
133
|
-
// !cn.alias) ||
|
|
134
|
-
// association.targetUmlClassName === cn.alias,
|
|
135
|
-
// ),
|
|
136
|
-
// )
|
|
137
|
-
// // For each import
|
|
138
|
-
// for (const importDetail of imports) {
|
|
139
|
-
// // Find a class with the same absolute path as the import so we can get the new imports
|
|
140
|
-
// const newSourceUmlClass = umlClasses.find(
|
|
141
|
-
// (c) => c.absolutePath === importDetail.absolutePath,
|
|
142
|
-
// )
|
|
143
|
-
// if (!newSourceUmlClass) {
|
|
144
|
-
// // Could not find a class in the import file so just move onto the next loop
|
|
145
|
-
// continue
|
|
146
|
-
// }
|
|
147
|
-
// // Avoid circular imports
|
|
148
|
-
// if (searchedRelativePaths.includes(newSourceUmlClass.absolutePath)) {
|
|
149
|
-
// // Have already recursively looked for imports of imports in this file
|
|
150
|
-
// continue
|
|
151
|
-
// }
|
|
152
|
-
//
|
|
153
|
-
// // find class linked to the association without aliased imports
|
|
154
|
-
// const umlClass = findAssociatedClass(
|
|
155
|
-
// association,
|
|
156
|
-
// newSourceUmlClass,
|
|
157
|
-
// umlClasses,
|
|
158
|
-
// searchedRelativePaths,
|
|
159
|
-
// )
|
|
160
|
-
// if (umlClass) return umlClass
|
|
161
|
-
//
|
|
162
|
-
// // find all aliased imports
|
|
163
|
-
// const aliasedImports = importDetail.classNames.filter((cn) => cn.alias)
|
|
164
|
-
// // For each aliased import
|
|
165
|
-
// for (const aliasedImport of aliasedImports) {
|
|
166
|
-
// const umlClass = findAssociatedClass(
|
|
167
|
-
// { ...association, targetUmlClassName: aliasedImport.className },
|
|
168
|
-
// newSourceUmlClass,
|
|
169
|
-
// umlClasses,
|
|
170
|
-
// searchedRelativePaths,
|
|
171
|
-
// )
|
|
172
|
-
// if (umlClass) return umlClass
|
|
173
|
-
// }
|
|
174
|
-
// }
|
|
175
|
-
// return undefined
|
|
176
|
-
// }
|
|
177
179
|
//# sourceMappingURL=associations.js.map
|
|
@@ -55,7 +55,7 @@ function convertAST2UmlClasses(node, relativePath, remappings, filesystem = fals
|
|
|
55
55
|
if (node.type === 'SourceUnit') {
|
|
56
56
|
node.children.forEach((childNode) => {
|
|
57
57
|
if (childNode.type === 'ContractDefinition') {
|
|
58
|
-
|
|
58
|
+
const umlClass = new umlClass_1.UmlClass({
|
|
59
59
|
name: childNode.name,
|
|
60
60
|
absolutePath: filesystem
|
|
61
61
|
? path.resolve(relativePath) // resolve the absolute path
|
|
@@ -68,7 +68,7 @@ function convertAST2UmlClasses(node, relativePath, remappings, filesystem = fals
|
|
|
68
68
|
}
|
|
69
69
|
else if (childNode.type === 'StructDefinition') {
|
|
70
70
|
debug(`Adding file level struct ${childNode.name}`);
|
|
71
|
-
|
|
71
|
+
const umlClass = new umlClass_1.UmlClass({
|
|
72
72
|
name: childNode.name,
|
|
73
73
|
stereotype: umlClass_1.ClassStereotype.Struct,
|
|
74
74
|
absolutePath: filesystem
|
|
@@ -82,7 +82,7 @@ function convertAST2UmlClasses(node, relativePath, remappings, filesystem = fals
|
|
|
82
82
|
}
|
|
83
83
|
else if (childNode.type === 'EnumDefinition') {
|
|
84
84
|
debug(`Adding file level enum ${childNode.name}`);
|
|
85
|
-
|
|
85
|
+
const umlClass = new umlClass_1.UmlClass({
|
|
86
86
|
name: childNode.name,
|
|
87
87
|
stereotype: umlClass_1.ClassStereotype.Enum,
|
|
88
88
|
absolutePath: filesystem
|
|
@@ -116,7 +116,7 @@ function convertAST2UmlClasses(node, relativePath, remappings, filesystem = fals
|
|
|
116
116
|
debug(`Added filesystem import ${newImport.absolutePath} with class names ${newImport.classNames.map((i) => i.className)}`);
|
|
117
117
|
imports.push(newImport);
|
|
118
118
|
}
|
|
119
|
-
catch
|
|
119
|
+
catch {
|
|
120
120
|
debug(`Failed to resolve import ${childNode.path} from file ${relativePath}`);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
@@ -626,7 +626,7 @@ function parseTypeName(typeName) {
|
|
|
626
626
|
case 'FunctionTypeName':
|
|
627
627
|
// TODO add params and return type
|
|
628
628
|
return [typeName.type + '\\(\\)', umlClass_1.AttributeType.Function];
|
|
629
|
-
case 'ArrayTypeName':
|
|
629
|
+
case 'ArrayTypeName': {
|
|
630
630
|
const [arrayElementType] = parseTypeName(typeName.baseTypeName);
|
|
631
631
|
let length = '';
|
|
632
632
|
if (Number.isInteger(typeName.length)) {
|
|
@@ -640,7 +640,8 @@ function parseTypeName(typeName) {
|
|
|
640
640
|
}
|
|
641
641
|
// TODO does not currently handle Expression types like BinaryOperation
|
|
642
642
|
return [arrayElementType + '[' + length + ']', umlClass_1.AttributeType.Array];
|
|
643
|
-
|
|
643
|
+
}
|
|
644
|
+
case 'Mapping': {
|
|
644
645
|
const key = typeName.keyType?.name ||
|
|
645
646
|
typeName.keyType?.namePath;
|
|
646
647
|
const [valueType] = parseTypeName(typeName.valueType);
|
|
@@ -648,6 +649,7 @@ function parseTypeName(typeName) {
|
|
|
648
649
|
'mapping\\(' + key + '=\\>' + valueType + '\\)',
|
|
649
650
|
umlClass_1.AttributeType.Mapping,
|
|
650
651
|
];
|
|
652
|
+
}
|
|
651
653
|
default:
|
|
652
654
|
throw Error(`Invalid typeName ${typeName}`);
|
|
653
655
|
}
|
|
@@ -661,7 +663,7 @@ function parseParameters(params) {
|
|
|
661
663
|
if (!params || !params) {
|
|
662
664
|
return [];
|
|
663
665
|
}
|
|
664
|
-
|
|
666
|
+
const parameters = [];
|
|
665
667
|
for (const param of params) {
|
|
666
668
|
const [type] = parseTypeName(param.typeName);
|
|
667
669
|
parameters.push({
|
|
@@ -166,7 +166,7 @@ const dotOperators = (umlClass, vizGroup, operators, options) => {
|
|
|
166
166
|
return b.stereotype - a.stereotype;
|
|
167
167
|
});
|
|
168
168
|
// Filter out any modifiers or events if options are flagged to hide them
|
|
169
|
-
|
|
169
|
+
const operatorsFiltered = operatorsSortedByStereotype.filter((o) => !((options.hideModifiers === true &&
|
|
170
170
|
o.stereotype === umlClass_1.OperatorStereotype.Modifier) ||
|
|
171
171
|
(options.hideEvents === true &&
|
|
172
172
|
o.stereotype === umlClass_1.OperatorStereotype.Event)));
|
|
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
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
|
-
const utils_1 = require("ethers/lib/utils");
|
|
10
9
|
const ethers_1 = require("ethers");
|
|
11
10
|
const path_1 = __importDefault(require("path"));
|
|
12
11
|
const slotValues_1 = require("./slotValues");
|
|
@@ -151,8 +150,8 @@ const parseVariables = (umlClass, umlClasses, variables, storageSections, inheri
|
|
|
151
150
|
const getValue = calcGetValue(attribute.attributeType, mapping);
|
|
152
151
|
// Get the toSlot of the last storage item
|
|
153
152
|
const lastVariable = variables[variables.length - 1];
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
const lastToSlot = lastVariable ? lastVariable.toSlot : 0;
|
|
154
|
+
const nextOffset = lastVariable
|
|
156
155
|
? lastVariable.byteOffset + lastVariable.byteSize
|
|
157
156
|
: 0;
|
|
158
157
|
let fromSlot;
|
|
@@ -347,7 +346,7 @@ const parseStorageSectionFromAttribute = (attribute, umlClass, otherClasses, sto
|
|
|
347
346
|
// Find UserDefined type can be a contract, struct or enum
|
|
348
347
|
const typeClass = findTypeClass(result[1], attribute, umlClass, otherClasses);
|
|
349
348
|
if (typeClass.stereotype === umlClass_1.ClassStereotype.Struct) {
|
|
350
|
-
|
|
349
|
+
const variables = parseVariables(typeClass, otherClasses, [], storageSections, [], true, arrayItems, noExpandVariables);
|
|
351
350
|
const storageSection = {
|
|
352
351
|
id: storageId++,
|
|
353
352
|
name: typeClass.name,
|
|
@@ -535,7 +534,7 @@ const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
|
|
|
535
534
|
case umlClass_1.ClassStereotype.Interface:
|
|
536
535
|
case umlClass_1.ClassStereotype.Library:
|
|
537
536
|
return { size: 20, dynamic: false };
|
|
538
|
-
case umlClass_1.ClassStereotype.Struct:
|
|
537
|
+
case umlClass_1.ClassStereotype.Struct: {
|
|
539
538
|
let structByteSize = 0;
|
|
540
539
|
attributeTypeClass.attributes.forEach((structAttribute) => {
|
|
541
540
|
// If next attribute is an array, then we need to start in a new slot
|
|
@@ -569,6 +568,7 @@ const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
|
|
|
569
568
|
size: Math.ceil(structByteSize / 32) * 32,
|
|
570
569
|
dynamic: false,
|
|
571
570
|
};
|
|
571
|
+
}
|
|
572
572
|
default:
|
|
573
573
|
return { size: 20, dynamic: false };
|
|
574
574
|
}
|
|
@@ -593,7 +593,7 @@ const calcElementaryTypeSize = (type) => {
|
|
|
593
593
|
case 'ufixed':
|
|
594
594
|
case 'fixed':
|
|
595
595
|
return { size: 32, dynamic: false };
|
|
596
|
-
default:
|
|
596
|
+
default: {
|
|
597
597
|
const result = type.match(/[u]*(int|fixed|bytes)([0-9]+)/);
|
|
598
598
|
if (result === null || !result[2]) {
|
|
599
599
|
throw Error(`Failed size elementary type "${type}"`);
|
|
@@ -606,6 +606,7 @@ const calcElementaryTypeSize = (type) => {
|
|
|
606
606
|
// If an int
|
|
607
607
|
const bitSize = parseInt(result[2]);
|
|
608
608
|
return { size: bitSize / 8, dynamic: false };
|
|
609
|
+
}
|
|
609
610
|
}
|
|
610
611
|
};
|
|
611
612
|
const isElementary = (type) => {
|
|
@@ -619,18 +620,19 @@ const isElementary = (type) => {
|
|
|
619
620
|
case 'ufixed':
|
|
620
621
|
case 'fixed':
|
|
621
622
|
return true;
|
|
622
|
-
default:
|
|
623
|
+
default: {
|
|
623
624
|
const result = type.match(/^[u]?(int|fixed|bytes)([0-9]+)$/);
|
|
624
625
|
return result !== null;
|
|
626
|
+
}
|
|
625
627
|
}
|
|
626
628
|
};
|
|
627
629
|
exports.isElementary = isElementary;
|
|
628
630
|
const calcSectionOffset = (variable, sectionOffset = '0') => {
|
|
629
631
|
if (variable.dynamic) {
|
|
630
|
-
const hexStringOf32Bytes = (0,
|
|
631
|
-
return (0,
|
|
632
|
+
const hexStringOf32Bytes = (0, ethers_1.zeroPadValue)((0, ethers_1.toBeHex)(BigInt(variable.fromSlot) + BigInt(sectionOffset)), 32);
|
|
633
|
+
return (0, ethers_1.keccak256)(hexStringOf32Bytes);
|
|
632
634
|
}
|
|
633
|
-
return ethers_1.
|
|
635
|
+
return (0, ethers_1.toBeHex)(BigInt(variable.fromSlot) + BigInt(sectionOffset));
|
|
634
636
|
};
|
|
635
637
|
exports.calcSectionOffset = calcSectionOffset;
|
|
636
638
|
const findDimensionLength = (umlClass, dimension, otherClasses) => {
|
|
@@ -788,7 +790,7 @@ const addDynamicVariables = async (storageSection, storageSections, url, contrac
|
|
|
788
790
|
continue;
|
|
789
791
|
}
|
|
790
792
|
// Add missing dynamic array variables
|
|
791
|
-
const arrayLength =
|
|
793
|
+
const arrayLength = Number(BigInt(variable.slotValue));
|
|
792
794
|
if (arrayLength > 1) {
|
|
793
795
|
// Add missing array variables to the referenced dynamic array
|
|
794
796
|
addArrayVariables(arrayLength, arrayItems, referenceStorageSection.variables);
|
package/lib/diffContracts.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export declare const compareFlattenContracts: (addressA: string, addressB: strin
|
|
|
27
27
|
contractNameB: string;
|
|
28
28
|
}>;
|
|
29
29
|
export declare const diffVerified2Local: (addressA: string, etherscanParserA: EtherscanParser, fileOrBaseFolders: string[], ignoreFilesOrFolders?: string[]) => Promise<CompareContracts>;
|
|
30
|
-
export declare const diffVerifiedContracts: (addressA: string, addressB: string, etherscanParserA: EtherscanParser, etherscanParserB: EtherscanParser,
|
|
30
|
+
export declare const diffVerifiedContracts: (addressA: string, addressB: string, etherscanParserA: EtherscanParser, etherscanParserB: EtherscanParser, _options: DiffOptions) => Promise<CompareContracts>;
|
|
31
31
|
export declare const displayFileDiffSummary: (fileDiffs: DiffFiles[]) => void;
|
|
32
32
|
export declare const displayFileDiffs: (fileDiffs: DiffFiles[], options?: {
|
|
33
33
|
lineBuffer?: number;
|
package/lib/diffContracts.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.displayFileDiffs = exports.displayFileDiffSummary = exports.diffVerifiedContracts = exports.diffVerified2Local = exports.compareFlattenContracts = exports.compareVerified2Local = exports.compareVerifiedContracts = void 0;
|
|
4
4
|
const clc = require('cli-color');
|
|
5
|
+
const fs_1 = require("fs");
|
|
5
6
|
const path_1 = require("path");
|
|
6
7
|
const parserFiles_1 = require("./parserFiles");
|
|
7
8
|
const writerFiles_1 = require("./writerFiles");
|
|
@@ -91,12 +92,20 @@ const diffVerified2Local = async (addressA, etherscanParserA, fileOrBaseFolders,
|
|
|
91
92
|
let bFile;
|
|
92
93
|
// for each of the base folders
|
|
93
94
|
for (const baseFolder of fileOrBaseFolders) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
const aFileResolvedPath = (0, path_1.resolve)(process.cwd(), baseFolder, aFile.filename);
|
|
96
|
+
try {
|
|
97
|
+
// Resolve full path including symlinks. For example,
|
|
98
|
+
// node_modules/@openzeppelin/contracts is a symlink to
|
|
99
|
+
// node_modules/.pnpm/@openzeppelin+contracts@x.y.z/node_modules/@openzeppelin/contracts
|
|
100
|
+
const aFileRealPath = (0, fs_1.realpathSync)(aFileResolvedPath);
|
|
101
|
+
bFile = bFiles.find((bFile) => bFile === aFileRealPath);
|
|
102
|
+
if (bFile) {
|
|
103
|
+
// Found match of aFile in bFiles, break out of loop to try next aFile
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Do nothing, continue to next bFile in loop
|
|
100
109
|
}
|
|
101
110
|
}
|
|
102
111
|
if (bFile) {
|
|
@@ -139,7 +148,7 @@ const diffVerified2Local = async (addressA, etherscanParserA, fileOrBaseFolders,
|
|
|
139
148
|
};
|
|
140
149
|
};
|
|
141
150
|
exports.diffVerified2Local = diffVerified2Local;
|
|
142
|
-
const diffVerifiedContracts = async (addressA, addressB, etherscanParserA, etherscanParserB,
|
|
151
|
+
const diffVerifiedContracts = async (addressA, addressB, etherscanParserA, etherscanParserB, _options) => {
|
|
143
152
|
const files = [];
|
|
144
153
|
const { files: aFiles, contractName: contractNameA } = await etherscanParserA.getSourceCode(addressA);
|
|
145
154
|
const { files: bFiles, contractName: contractNameB } = await etherscanParserB.getSourceCode(addressB);
|
package/lib/parserEtherscan.js
CHANGED
|
@@ -90,7 +90,7 @@ class EtherscanParser {
|
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
if (!apiKey) {
|
|
93
|
-
console.error(`The apiKey option must be set when getting verified source code from an Etherscan like explorer`);
|
|
93
|
+
console.error(`The -k, --apiKey option or SCAN_API_KEY env var must be set when getting verified source code from an Etherscan like explorer`);
|
|
94
94
|
process.exit(1);
|
|
95
95
|
}
|
|
96
96
|
const chainId = (0, exports.setChainId)(network);
|
package/lib/parserFiles.js
CHANGED
|
@@ -128,7 +128,7 @@ const isFile = (fileName) => {
|
|
|
128
128
|
const file = (0, fs_1.lstatSync)(fileName);
|
|
129
129
|
return file.isFile();
|
|
130
130
|
}
|
|
131
|
-
catch
|
|
131
|
+
catch {
|
|
132
132
|
return false;
|
|
133
133
|
}
|
|
134
134
|
};
|
|
@@ -138,7 +138,7 @@ const isFolder = (fileName) => {
|
|
|
138
138
|
const file = (0, fs_1.lstatSync)(fileName);
|
|
139
139
|
return file.isDirectory();
|
|
140
140
|
}
|
|
141
|
-
catch
|
|
141
|
+
catch {
|
|
142
142
|
return false;
|
|
143
143
|
}
|
|
144
144
|
};
|
package/lib/slotValues.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { BigNumberish } from '@ethersproject/bignumber';
|
|
2
1
|
import { StorageSection, Variable } from './converterClasses2Storage';
|
|
2
|
+
import { BigNumberish } from 'ethers';
|
|
3
3
|
/**
|
|
4
4
|
* Adds the slot values to the variables in the storage section.
|
|
5
5
|
* This can be rerun for a section as it will only get if the slot value
|
package/lib/slotValues.js
CHANGED
|
@@ -4,13 +4,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.escapeString = exports.convert2String = exports.dynamicSlotSize = exports.getSlotValue = exports.getSlotValues = exports.parseValue = exports.addSlotValues = void 0;
|
|
7
|
-
const bignumber_1 = require("@ethersproject/bignumber");
|
|
8
7
|
const axios_1 = __importDefault(require("axios"));
|
|
9
8
|
const umlClass_1 = require("./umlClass");
|
|
10
|
-
const utils_1 = require("ethers/lib/utils");
|
|
11
|
-
const SlotValueCache_1 = require("./SlotValueCache");
|
|
12
9
|
const ethers_1 = require("ethers");
|
|
10
|
+
const SlotValueCache_1 = require("./SlotValueCache");
|
|
13
11
|
const debug = require('debug')('sol2uml');
|
|
12
|
+
const commify = (value) => {
|
|
13
|
+
const match = value.match(/^(-?)(\d+)(\.(.*))?$/);
|
|
14
|
+
if (!match)
|
|
15
|
+
return value;
|
|
16
|
+
const neg = match[1];
|
|
17
|
+
const whole = match[2];
|
|
18
|
+
const frac = match[4] ? '.' + match[4] : '';
|
|
19
|
+
return neg + whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',') + frac;
|
|
20
|
+
};
|
|
14
21
|
/**
|
|
15
22
|
* Adds the slot values to the variables in the storage section.
|
|
16
23
|
* This can be rerun for a section as it will only get if the slot value
|
|
@@ -30,7 +37,7 @@ const addSlotValues = async (url, contractAddress, storageSection, arrayItems, b
|
|
|
30
37
|
const slots = [];
|
|
31
38
|
valueVariables.forEach((variable) => {
|
|
32
39
|
if (variable.offset) {
|
|
33
|
-
slots.push(
|
|
40
|
+
slots.push(BigInt(variable.offset));
|
|
34
41
|
}
|
|
35
42
|
else {
|
|
36
43
|
for (let i = 0; variable.fromSlot + i <= variable.toSlot; i++) {
|
|
@@ -45,12 +52,12 @@ const addSlotValues = async (url, contractAddress, storageSection, arrayItems, b
|
|
|
45
52
|
});
|
|
46
53
|
// remove duplicate slot numbers
|
|
47
54
|
const uniqueFromSlots = [...new Set(slots)];
|
|
48
|
-
// Convert slot numbers to
|
|
49
|
-
|
|
55
|
+
// Convert slot numbers to BigInts and offset dynamic arrays
|
|
56
|
+
const slotKeys = uniqueFromSlots.map((fromSlot) => {
|
|
50
57
|
if (storageSection.offset) {
|
|
51
|
-
return
|
|
58
|
+
return BigInt(storageSection.offset) + BigInt(fromSlot);
|
|
52
59
|
}
|
|
53
|
-
return
|
|
60
|
+
return BigInt(fromSlot);
|
|
54
61
|
});
|
|
55
62
|
// Get the contract slot values from the node provider
|
|
56
63
|
const values = await (0, exports.getSlotValues)(url, contractAddress, slotKeys, blockTag);
|
|
@@ -62,7 +69,7 @@ const addSlotValues = async (url, contractAddress, storageSection, arrayItems, b
|
|
|
62
69
|
for (const variable of storageSection.variables) {
|
|
63
70
|
if (variable.getValue &&
|
|
64
71
|
variable.offset &&
|
|
65
|
-
|
|
72
|
+
BigInt(variable.offset) === BigInt(fromSlot)) {
|
|
66
73
|
debug(`Set slot value ${value} for section "${storageSection.name}", var type ${variable.type}, slot ${variable.offset}`);
|
|
67
74
|
variable.slotValue = value;
|
|
68
75
|
// parse variable value from slot data
|
|
@@ -80,7 +87,7 @@ const addSlotValues = async (url, contractAddress, storageSection, arrayItems, b
|
|
|
80
87
|
}
|
|
81
88
|
// if variable is past the slot that has the value
|
|
82
89
|
else if (variable.toSlot &&
|
|
83
|
-
|
|
90
|
+
BigInt(variable.toSlot) > BigInt(fromSlot)) {
|
|
84
91
|
break;
|
|
85
92
|
}
|
|
86
93
|
}
|
|
@@ -103,7 +110,7 @@ const parseValue = (variable) => {
|
|
|
103
110
|
// dynamic arrays
|
|
104
111
|
if (variable.attributeType === umlClass_1.AttributeType.Array &&
|
|
105
112
|
variable.dynamic) {
|
|
106
|
-
return (0,
|
|
113
|
+
return (0, ethers_1.formatUnits)('0x' + variableValue, 0);
|
|
107
114
|
}
|
|
108
115
|
return undefined;
|
|
109
116
|
}
|
|
@@ -118,12 +125,12 @@ const parseUserDefinedValue = (variable, variableValue) => {
|
|
|
118
125
|
// https://blog.soliditylang.org/2021/09/27/user-defined-value-types/
|
|
119
126
|
// using byteSize is crude and will be incorrect for aliases types like int160 or uint160
|
|
120
127
|
if (variable.byteSize === 20) {
|
|
121
|
-
return (0,
|
|
128
|
+
return (0, ethers_1.getAddress)('0x' + variableValue);
|
|
122
129
|
}
|
|
123
130
|
// this will also be wrong if the alias is to a 1 byte type. eg bytes1, int8 or uint8
|
|
124
131
|
if (variable.byteSize === 1) {
|
|
125
132
|
// assume 1 byte is an enum so convert value to enum index number
|
|
126
|
-
const index =
|
|
133
|
+
const index = Number(BigInt('0x' + variableValue));
|
|
127
134
|
// lookup enum value if its available
|
|
128
135
|
return variable?.enumValues ? variable?.enumValues[index] : undefined;
|
|
129
136
|
}
|
|
@@ -142,17 +149,14 @@ const parseElementaryValue = (variable, variableValue) => {
|
|
|
142
149
|
if (variable.type === 'string' || variable.type === 'bytes') {
|
|
143
150
|
if (variable.dynamic) {
|
|
144
151
|
const lastByte = variable.slotValue.slice(-2);
|
|
145
|
-
const size =
|
|
152
|
+
const size = BigInt('0x' + lastByte);
|
|
146
153
|
// Check if the last bit is set by AND the size with 0x01
|
|
147
|
-
if (size
|
|
154
|
+
if ((size & 1n) === 1n) {
|
|
148
155
|
// Return the number of chars or bytes
|
|
149
|
-
return
|
|
150
|
-
.sub(1)
|
|
151
|
-
.div(2)
|
|
152
|
-
.toString();
|
|
156
|
+
return ((BigInt(variable.slotValue) - 1n) / 2n).toString();
|
|
153
157
|
}
|
|
154
158
|
// The last byte holds the length of the string or bytes in the slot
|
|
155
|
-
const valueHex = '0x' + variableValue.slice(0, size
|
|
159
|
+
const valueHex = '0x' + variableValue.slice(0, Number(size));
|
|
156
160
|
if (variable.type === 'bytes')
|
|
157
161
|
return valueHex;
|
|
158
162
|
return `\\"${(0, exports.convert2String)(valueHex)}\\"`;
|
|
@@ -162,34 +166,34 @@ const parseElementaryValue = (variable, variableValue) => {
|
|
|
162
166
|
return `\\"${(0, exports.convert2String)('0x' + variableValue)}\\"`;
|
|
163
167
|
}
|
|
164
168
|
if (variable.type === 'address') {
|
|
165
|
-
return (0,
|
|
169
|
+
return (0, ethers_1.getAddress)('0x' + variableValue);
|
|
166
170
|
}
|
|
167
171
|
if (variable.type.match(/^uint([0-9]*)$/)) {
|
|
168
|
-
const parsedValue = (0,
|
|
169
|
-
return
|
|
172
|
+
const parsedValue = (0, ethers_1.formatUnits)('0x' + variableValue, 0);
|
|
173
|
+
return commify(parsedValue);
|
|
170
174
|
}
|
|
171
175
|
if (variable.type.match(/^bytes([0-9]+)$/)) {
|
|
172
176
|
return '0x' + variableValue;
|
|
173
177
|
}
|
|
174
178
|
if (variable.type.match(/^int([0-9]*)/)) {
|
|
175
179
|
// parse variable value as an unsigned number
|
|
176
|
-
let rawValue =
|
|
180
|
+
let rawValue = BigInt('0x' + variableValue);
|
|
177
181
|
// parse the number of bits
|
|
178
182
|
const result = variable.type.match(/^int([0-9]*$)/);
|
|
179
|
-
const bitSize = result[1] ? result[1] : 256;
|
|
183
|
+
const bitSize = result[1] ? Number(result[1]) : 256;
|
|
180
184
|
// Convert the number of bits to the number of hex characters
|
|
181
|
-
const hexSize =
|
|
185
|
+
const hexSize = bitSize / 4;
|
|
182
186
|
// bit mask has a leading 1 and the rest 0. 0x8 = 1000 binary
|
|
183
|
-
const mask = '0x80' + '0'.repeat(hexSize - 2);
|
|
187
|
+
const mask = BigInt('0x80' + '0'.repeat(hexSize - 2));
|
|
184
188
|
// is the first bit a 1?
|
|
185
|
-
const negative = rawValue
|
|
186
|
-
if (negative
|
|
189
|
+
const negative = rawValue & mask;
|
|
190
|
+
if (negative > 0n) {
|
|
187
191
|
// Convert unsigned number to a signed negative
|
|
188
|
-
const negativeOne = '0xFF' + 'F'.repeat(hexSize - 2);
|
|
189
|
-
rawValue =
|
|
192
|
+
const negativeOne = BigInt('0xFF' + 'F'.repeat(hexSize - 2));
|
|
193
|
+
rawValue = (negativeOne - rawValue + 1n) * -1n;
|
|
190
194
|
}
|
|
191
|
-
const parsedValue = (0,
|
|
192
|
-
return
|
|
195
|
+
const parsedValue = (0, ethers_1.formatUnits)(rawValue, 0);
|
|
196
|
+
return commify(parsedValue);
|
|
193
197
|
}
|
|
194
198
|
// add fixed point numbers when they are supported by Solidity
|
|
195
199
|
return undefined;
|
|
@@ -209,9 +213,7 @@ const getSlotValues = async (url, contractAddress, slotKeys, blockTag = 'latest'
|
|
|
209
213
|
if (slotKeys.length === 0) {
|
|
210
214
|
return [];
|
|
211
215
|
}
|
|
212
|
-
const block = blockTag === 'latest'
|
|
213
|
-
? blockTag
|
|
214
|
-
: (0, utils_1.hexValue)(bignumber_1.BigNumber.from(blockTag));
|
|
216
|
+
const block = blockTag === 'latest' ? blockTag : (0, ethers_1.toQuantity)(BigInt(blockTag));
|
|
215
217
|
// get cached values and missing slot keys from the cache
|
|
216
218
|
const { cachedValues, missingKeys } = SlotValueCache_1.SlotValueCache.readSlotValues(slotKeys);
|
|
217
219
|
// If all values are in the cache then just return the cached values
|
|
@@ -219,7 +221,7 @@ const getSlotValues = async (url, contractAddress, slotKeys, blockTag = 'latest'
|
|
|
219
221
|
return cachedValues;
|
|
220
222
|
}
|
|
221
223
|
// Check we are pointing to the correct chain by checking the contract has code
|
|
222
|
-
const provider = new ethers_1.
|
|
224
|
+
const provider = new ethers_1.JsonRpcProvider(url);
|
|
223
225
|
const code = await provider.getCode(contractAddress, block);
|
|
224
226
|
if (!code || code === '0x') {
|
|
225
227
|
const msg = `Address ${contractAddress} has no code. Check your "-u, --url" option or "NODE_URL" environment variable is pointing to the correct node.\nurl: ${url}`;
|
|
@@ -242,7 +244,7 @@ const getSlotValues = async (url, contractAddress, slotKeys, blockTag = 'latest'
|
|
|
242
244
|
throw Error(`Requested ${missingKeys.length} storage slot values but only got ${response.data.length}`);
|
|
243
245
|
}
|
|
244
246
|
const responseData = response.data;
|
|
245
|
-
const sortedResponses = responseData.sort((a, b) =>
|
|
247
|
+
const sortedResponses = responseData.sort((a, b) => Number(a.id) - Number(b.id));
|
|
246
248
|
const missingValues = sortedResponses.map((data) => {
|
|
247
249
|
if (data.error) {
|
|
248
250
|
throw Error(`json rpc call with id ${data.id} failed to get storage values: ${data.error?.message}`);
|
|
@@ -284,11 +286,11 @@ const dynamicSlotSize = (variable) => {
|
|
|
284
286
|
if (!variable?.slotValue)
|
|
285
287
|
throw Error(`Missing slot value.`);
|
|
286
288
|
const last4bits = '0x' + variable.slotValue.slice(-1);
|
|
287
|
-
const last4bitsNum =
|
|
289
|
+
const last4bitsNum = Number(BigInt(last4bits));
|
|
288
290
|
// If the last 4 bits is an even number then it's not a dynamic slot
|
|
289
291
|
if (last4bitsNum % 2 === 0)
|
|
290
292
|
return 0;
|
|
291
|
-
const sizeRaw =
|
|
293
|
+
const sizeRaw = Number(BigInt(variable.slotValue));
|
|
292
294
|
// Adjust the size to bytes
|
|
293
295
|
return (sizeRaw - 1) / 2;
|
|
294
296
|
}
|
|
@@ -302,7 +304,7 @@ const convert2String = (bytes) => {
|
|
|
302
304
|
'0x0000000000000000000000000000000000000000000000000000000000000000') {
|
|
303
305
|
return '';
|
|
304
306
|
}
|
|
305
|
-
const rawString = (0,
|
|
307
|
+
const rawString = (0, ethers_1.toUtf8String)(bytes);
|
|
306
308
|
return (0, exports.escapeString)(rawString);
|
|
307
309
|
};
|
|
308
310
|
exports.convert2String = convert2String;
|
package/lib/sol2uml.js
CHANGED
|
@@ -171,7 +171,7 @@ WARNING: sol2uml does not use the Solidity compiler so may differ with solc. A k
|
|
|
171
171
|
}
|
|
172
172
|
storageAddress = fileFolderAddress;
|
|
173
173
|
}
|
|
174
|
-
|
|
174
|
+
const block = await (0, block_1.getBlock)(combinedOptions);
|
|
175
175
|
// Get slot values for each storage section
|
|
176
176
|
for (const storageSection of storageSections) {
|
|
177
177
|
await (0, slotValues_1.addSlotValues)(combinedOptions.url, storageAddress, storageSection, arrayItems, block);
|
package/lib/squashClasses.js
CHANGED
|
@@ -47,14 +47,14 @@ const squashUmlClasses = (umlClasses, baseContractNames) => {
|
|
|
47
47
|
let removedClassIds = [];
|
|
48
48
|
for (const baseContractName of baseContractNames) {
|
|
49
49
|
// Find the base UML Class to squash
|
|
50
|
-
|
|
50
|
+
const baseIndex = umlClasses.findIndex(({ name }) => {
|
|
51
51
|
return name === baseContractName;
|
|
52
52
|
});
|
|
53
53
|
if (baseIndex === undefined) {
|
|
54
54
|
throw Error(`Failed to find contract with name "${baseContractName}" to squash`);
|
|
55
55
|
}
|
|
56
56
|
const baseClass = umlClasses[baseIndex];
|
|
57
|
-
|
|
57
|
+
const squashedClass = new umlClass_1.UmlClass({
|
|
58
58
|
name: baseClass.name,
|
|
59
59
|
absolutePath: baseClass.absolutePath,
|
|
60
60
|
relativePath: baseClass.relativePath,
|
package/lib/utils/block.js
CHANGED
|
@@ -6,12 +6,12 @@ const debug = require('debug')('sol2uml');
|
|
|
6
6
|
const getBlock = async (options) => {
|
|
7
7
|
if (options.block === 'latest') {
|
|
8
8
|
try {
|
|
9
|
-
const provider = new ethers_1.
|
|
9
|
+
const provider = new ethers_1.JsonRpcProvider(options.url);
|
|
10
10
|
const block = await provider.getBlockNumber();
|
|
11
11
|
debug(`Latest block is ${block}. All storage slot values will be from this block.`);
|
|
12
12
|
return block;
|
|
13
13
|
}
|
|
14
|
-
catch
|
|
14
|
+
catch {
|
|
15
15
|
const defaultMessage = options.url === 'http://localhost:8545'
|
|
16
16
|
? 'This is the default url. Use the `-u, --url` option or `NODE_URL` environment variable to set the url of your blockchain node.'
|
|
17
17
|
: `Check your --url option or NODE_URL environment variable is pointing to the correct node for the "${options.network}" blockchain.`;
|
|
@@ -21,7 +21,7 @@ const getBlock = async (options) => {
|
|
|
21
21
|
try {
|
|
22
22
|
return parseInt(options.block);
|
|
23
23
|
}
|
|
24
|
-
catch
|
|
24
|
+
catch {
|
|
25
25
|
throw Error(`Invalid block number: ${options.block}`);
|
|
26
26
|
}
|
|
27
27
|
};
|
package/lib/utils/diff.js
CHANGED
|
@@ -44,7 +44,7 @@ const SkippedLinesMarker = `\n---`;
|
|
|
44
44
|
* @param lineBuff the number of lines to display before and after each change.
|
|
45
45
|
*/
|
|
46
46
|
const diffCode = (codeA, codeB, lineBuff) => {
|
|
47
|
-
// @ts-ignore
|
|
47
|
+
// @ts-ignore diff_match_patch constructor has no type declarations
|
|
48
48
|
const dmp = new diff_match_patch_1.default();
|
|
49
49
|
const diff = dmp.diff_main(codeA, codeB);
|
|
50
50
|
dmp.diff_cleanupSemantic(diff);
|
|
@@ -70,18 +70,20 @@ const diff_pretty = (diffs, lines, lineBuff = 2) => {
|
|
|
70
70
|
const op = diff[0]; // Operation (insert, delete, equal)
|
|
71
71
|
const text = diff[1]; // Text of change.
|
|
72
72
|
switch (op) {
|
|
73
|
-
case diff_match_patch_1.DIFF_INSERT:
|
|
73
|
+
case diff_match_patch_1.DIFF_INSERT: {
|
|
74
74
|
// If first diff then we need to add the first line number
|
|
75
75
|
const linesInserted = addLineNumbers(text, lineCount, linePad);
|
|
76
76
|
output += initialLineNumber + clc.green(linesInserted);
|
|
77
77
|
lineCount += countLines(text);
|
|
78
78
|
break;
|
|
79
|
-
|
|
79
|
+
}
|
|
80
|
+
case diff_match_patch_1.DIFF_DELETE: {
|
|
80
81
|
// zero start line means blank line numbers are used
|
|
81
82
|
const linesDeleted = addLineNumbers(text, 0, linePad);
|
|
82
83
|
output += initialLineNumber + clc.red(linesDeleted);
|
|
83
84
|
break;
|
|
84
|
-
|
|
85
|
+
}
|
|
86
|
+
case diff_match_patch_1.DIFF_EQUAL: {
|
|
85
87
|
const eolPositions = findEOLPositions(text);
|
|
86
88
|
// If no changes yet
|
|
87
89
|
if (diffIndex <= 1) {
|
|
@@ -97,6 +99,7 @@ const diff_pretty = (diffs, lines, lineBuff = 2) => {
|
|
|
97
99
|
}
|
|
98
100
|
lineCount += eolPositions.length;
|
|
99
101
|
break;
|
|
102
|
+
}
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
output += '\n';
|
|
@@ -178,7 +181,7 @@ const countLines = (text) => (text.match(/\n/g) || '').length;
|
|
|
178
181
|
const addLineNumbers = (text, lineStart, linePad) => {
|
|
179
182
|
let lineCount = lineStart;
|
|
180
183
|
let textWithLineNumbers = '';
|
|
181
|
-
text.split('').forEach((c
|
|
184
|
+
text.split('').forEach((c) => {
|
|
182
185
|
if (c === '\n') {
|
|
183
186
|
if (lineStart > 0) {
|
|
184
187
|
textWithLineNumbers += `\n${(++lineCount)
|
package/lib/utils/validators.js
CHANGED
|
@@ -3,15 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
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
|
-
const
|
|
6
|
+
const ethers_1 = require("ethers");
|
|
7
7
|
const converterClasses2Storage_1 = require("../converterClasses2Storage");
|
|
8
8
|
const debug = require('debug')('sol2uml');
|
|
9
9
|
const validateAddress = (address) => {
|
|
10
10
|
try {
|
|
11
11
|
if (typeof address === 'string' && address?.match(regEx_1.ethereumAddress))
|
|
12
|
-
return (0,
|
|
12
|
+
return (0, ethers_1.getAddress)(address);
|
|
13
13
|
}
|
|
14
|
-
catch
|
|
14
|
+
catch { /* validation failed */ }
|
|
15
15
|
throw new commander_1.InvalidArgumentError(`Address must be in hexadecimal format with a 0x prefix.`);
|
|
16
16
|
};
|
|
17
17
|
exports.validateAddress = validateAddress;
|
|
@@ -22,7 +22,7 @@ const validateNames = (variables) => {
|
|
|
22
22
|
variables.match(regEx_1.commaSeparatedList))
|
|
23
23
|
return variables.split(',');
|
|
24
24
|
}
|
|
25
|
-
catch
|
|
25
|
+
catch { /* validation failed */ }
|
|
26
26
|
throw new commander_1.InvalidArgumentError(`Must be a comma-separate list of names with no white spaces.`);
|
|
27
27
|
};
|
|
28
28
|
exports.validateNames = validateNames;
|
|
@@ -32,7 +32,7 @@ const validateLineBuffer = (lineBufferParam) => {
|
|
|
32
32
|
if (lineBuffer >= 0)
|
|
33
33
|
return lineBuffer;
|
|
34
34
|
}
|
|
35
|
-
catch
|
|
35
|
+
catch { /* validation failed */ }
|
|
36
36
|
throw new commander_1.InvalidOptionArgumentError(`Must be a zero or a positive integer.`);
|
|
37
37
|
};
|
|
38
38
|
exports.validateLineBuffer = validateLineBuffer;
|
|
@@ -46,7 +46,7 @@ const validateSlotNames = (slotNames) => {
|
|
|
46
46
|
offset: slot,
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
const offset = (0,
|
|
49
|
+
const offset = (0, ethers_1.keccak256)((0, ethers_1.toUtf8Bytes)(slot));
|
|
50
50
|
debug(`Slot name "${slot}" has hash "${offset}"`);
|
|
51
51
|
return {
|
|
52
52
|
name: slot,
|
|
@@ -56,7 +56,7 @@ const validateSlotNames = (slotNames) => {
|
|
|
56
56
|
console.log(results.length);
|
|
57
57
|
return results;
|
|
58
58
|
}
|
|
59
|
-
catch
|
|
59
|
+
catch { /* validation failed */ }
|
|
60
60
|
throw new commander_1.InvalidOptionArgumentError(`Must be a comma-separate list of slots with no white spaces.`);
|
|
61
61
|
};
|
|
62
62
|
exports.validateSlotNames = validateSlotNames;
|
|
@@ -72,7 +72,7 @@ const validateTypes = (typesString) => {
|
|
|
72
72
|
return types;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
catch
|
|
75
|
+
catch { /* validation failed */ }
|
|
76
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
77
|
};
|
|
78
78
|
exports.validateTypes = validateTypes;
|
package/lib/writerFiles.js
CHANGED
|
@@ -13,6 +13,7 @@ const fs_1 = require("fs");
|
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
14
|
const sync_1 = __importDefault(require("@aduh95/viz.js/sync"));
|
|
15
15
|
const { convert } = require('convert-svg-to-png');
|
|
16
|
+
const puppeteer = require('puppeteer');
|
|
16
17
|
const debug = require('debug')('sol2uml');
|
|
17
18
|
/**
|
|
18
19
|
* Writes output files to the file system based on the provided input and options.
|
|
@@ -39,7 +40,9 @@ const writeOutputFiles = async (dot, contractName, outputFormat = 'svg', outputF
|
|
|
39
40
|
outputExt;
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
catch
|
|
43
|
+
catch {
|
|
44
|
+
/* outputFilename does not exist yet */
|
|
45
|
+
}
|
|
43
46
|
}
|
|
44
47
|
if (outputFormat === 'dot' || outputFormat === 'all') {
|
|
45
48
|
writeDot(dot, outputFilename);
|
|
@@ -140,7 +143,7 @@ async function writePng(svg, filename) {
|
|
|
140
143
|
debug(`About to write png file ${pngFilename}`);
|
|
141
144
|
try {
|
|
142
145
|
const png = await convert(svg, {
|
|
143
|
-
|
|
146
|
+
launch: { executablePath: puppeteer.executablePath() },
|
|
144
147
|
});
|
|
145
148
|
return new Promise((resolve, reject) => {
|
|
146
149
|
(0, fs_1.writeFile)(pngFilename, png, (err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sol2uml",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.24",
|
|
4
4
|
"description": "Solidity contract visualisation tool.",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"buildSol": "cd ./src/contracts && solc **/*.sol",
|
|
9
9
|
"build": "tsc --build ./tsconfig.json",
|
|
10
10
|
"clean": "tsc --build --clean ./tsconfig.json",
|
|
11
|
+
"lint": "eslint src/ts",
|
|
11
12
|
"package-lock": "npm i --package-lock-only",
|
|
12
13
|
"permit": " chmod 775 lib/sol2uml.js",
|
|
13
14
|
"prettier": "prettier --write src/**/*.ts **/*.md",
|
|
@@ -28,23 +29,28 @@
|
|
|
28
29
|
"axios-debug-log": "^1.0.0",
|
|
29
30
|
"cli-color": "^2.0.4",
|
|
30
31
|
"commander": "^12.1.0",
|
|
31
|
-
"convert-svg-to-png": "^0.
|
|
32
|
+
"convert-svg-to-png": "^0.7.1",
|
|
32
33
|
"debug": "^4.4.1",
|
|
33
34
|
"diff-match-patch": "^1.0.5",
|
|
34
|
-
"ethers": "^
|
|
35
|
+
"ethers": "^6.16.0",
|
|
35
36
|
"js-graph-algorithms": "^1.0.18",
|
|
36
|
-
"klaw": "^4.1.0"
|
|
37
|
+
"klaw": "^4.1.0",
|
|
38
|
+
"puppeteer": "^24.37.3"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
41
|
+
"@eslint/js": "^9.39.2",
|
|
39
42
|
"@openzeppelin/contracts": "^4.9.3",
|
|
40
43
|
"@types/diff-match-patch": "^1.0.36",
|
|
41
|
-
"@types/jest": "^
|
|
44
|
+
"@types/jest": "^30.0.0",
|
|
42
45
|
"@types/klaw": "^3.0.7",
|
|
43
|
-
"
|
|
44
|
-
"prettier": "^
|
|
45
|
-
"
|
|
46
|
+
"eslint": "^9.39.2",
|
|
47
|
+
"eslint-config-prettier": "^10.1.8",
|
|
48
|
+
"jest": "^30.2.0",
|
|
49
|
+
"prettier": "^3.8.1",
|
|
50
|
+
"ts-jest": "^29.4.6",
|
|
46
51
|
"ts-node": "^10.9.2",
|
|
47
|
-
"typescript": "^5.
|
|
52
|
+
"typescript": "^5.9.3",
|
|
53
|
+
"typescript-eslint": "^8.55.0"
|
|
48
54
|
},
|
|
49
55
|
"files": [
|
|
50
56
|
"lib/*.js",
|