sol2uml 2.5.23 → 2.5.25
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 +2 -0
- package/lib/SlotValueCache.d.ts +1 -1
- package/lib/SlotValueCache.js +3 -3
- package/lib/associations.d.ts +1 -1
- package/lib/associations.js +96 -94
- package/lib/converterAST2Classes.js +18 -1
- package/lib/converterClass2Dot.d.ts +2 -0
- package/lib/converterClass2Dot.js +8 -4
- package/lib/converterClasses2Dot.js +33 -0
- package/lib/converterClasses2Storage.d.ts +1 -1
- package/lib/converterClasses2Storage.js +4 -5
- package/lib/parserEtherscan.js +23 -8
- package/lib/slotValues.d.ts +1 -1
- package/lib/slotValues.js +42 -40
- package/lib/sol2uml.js +3 -1
- package/lib/squashClasses.js +6 -0
- package/lib/umlClass.d.ts +2 -0
- package/lib/umlClass.js +16 -0
- package/lib/utils/block.js +1 -1
- package/lib/utils/validators.js +3 -3
- package/lib/writerFiles.js +5 -2
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -111,6 +111,7 @@ Options:
|
|
|
111
111
|
-c, --clusterFolders cluster contracts into source folders (default: false)
|
|
112
112
|
-hv, --hideVariables hide variables from contracts, interfaces, structs and enums (default: false)
|
|
113
113
|
-hf, --hideFunctions hide functions from contracts, interfaces and libraries (default: false)
|
|
114
|
+
-hy, --hideTypes hide types of variables, function arguments and return types (default: false)
|
|
114
115
|
-hp, --hidePrivates hide private and internal attributes and operators (default: false)
|
|
115
116
|
-hm, --hideModifiers hide modifier functions from contracts (default: false)
|
|
116
117
|
-ht, --hideEvents hide events from contracts, interfaces and libraries (default: false)
|
|
@@ -122,6 +123,7 @@ Options:
|
|
|
122
123
|
-hi, --hideInterfaces hide interfaces (default: false)
|
|
123
124
|
-ha, --hideAbstracts hide abstract contracts (default: false)
|
|
124
125
|
-hn, --hideFilename hide relative path and file name (default: false)
|
|
126
|
+
-hd, --hideDepFunctions hide function names on dependency arrows (default: false)
|
|
125
127
|
-s, --squash squash inherited contracts to the base contract(s) (default: false)
|
|
126
128
|
-hsc, --hideSourceContract hide the source contract when using squash (default: false)
|
|
127
129
|
-h, --help display help for command
|
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.
|
|
@@ -17,7 +17,7 @@ class SlotValueCache {
|
|
|
17
17
|
const cachedValues = [];
|
|
18
18
|
const missingKeys = [];
|
|
19
19
|
slotKeys.forEach((slotKey) => {
|
|
20
|
-
const key =
|
|
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 = _findInheritedType(association, parent, umlClasses);
|
|
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
|
|
@@ -533,7 +533,24 @@ function parseExpression(expression, umlClass) {
|
|
|
533
533
|
parseExpression(expression.right, umlClass);
|
|
534
534
|
}
|
|
535
535
|
else if (expression.type === 'FunctionCall') {
|
|
536
|
-
|
|
536
|
+
if (expression.expression.type === 'MemberAccess' &&
|
|
537
|
+
expression.expression.expression?.type === 'Identifier') {
|
|
538
|
+
// Pattern: ClassName.functionName(args) — explicit library/contract call
|
|
539
|
+
const memberName = expression.expression.memberName;
|
|
540
|
+
umlClass.addAssociation({
|
|
541
|
+
referenceType: umlClass_1.ReferenceType.Memory,
|
|
542
|
+
targetUmlClassName: expression.expression.expression.name,
|
|
543
|
+
functionsCalled: [memberName],
|
|
544
|
+
});
|
|
545
|
+
umlClass.memberAccessCalls.add(memberName);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
// Track member access calls inside other FunctionCall patterns (e.g. x.functionName())
|
|
549
|
+
if (expression.expression.type === 'MemberAccess') {
|
|
550
|
+
umlClass.memberAccessCalls.add(expression.expression.memberName);
|
|
551
|
+
}
|
|
552
|
+
parseExpression(expression.expression, umlClass);
|
|
553
|
+
}
|
|
537
554
|
expression.arguments.forEach((arg) => {
|
|
538
555
|
parseExpression(arg, umlClass);
|
|
539
556
|
});
|
|
@@ -4,6 +4,7 @@ export interface ClassOptions {
|
|
|
4
4
|
hideContracts?: boolean;
|
|
5
5
|
hideVariables?: boolean;
|
|
6
6
|
hideFunctions?: boolean;
|
|
7
|
+
hideTypes?: boolean;
|
|
7
8
|
hideModifiers?: boolean;
|
|
8
9
|
hideEvents?: boolean;
|
|
9
10
|
hideStructs?: boolean;
|
|
@@ -14,6 +15,7 @@ export interface ClassOptions {
|
|
|
14
15
|
hideAbstracts?: boolean;
|
|
15
16
|
hideFilename?: boolean;
|
|
16
17
|
hideSourceContract?: boolean;
|
|
18
|
+
hideDepFunctions?: boolean;
|
|
17
19
|
backColor?: string;
|
|
18
20
|
shapeColor?: string;
|
|
19
21
|
fillColor?: string;
|
|
@@ -116,7 +116,8 @@ const dotAttributes = (attributes, options, vizGroup, indent = true) => {
|
|
|
116
116
|
const sourceContract = attribute.sourceContract && !options.hideSourceContract
|
|
117
117
|
? ` \\<\\<${attribute.sourceContract}\\>\\>`
|
|
118
118
|
: '';
|
|
119
|
-
|
|
119
|
+
const type = options.hideTypes ? '' : `: ${attribute.type}`;
|
|
120
|
+
dotString += `${indentString}${attribute.name}${type}${sourceContract}\\l`;
|
|
120
121
|
});
|
|
121
122
|
return dotString;
|
|
122
123
|
};
|
|
@@ -176,8 +177,8 @@ const dotOperators = (umlClass, vizGroup, operators, options) => {
|
|
|
176
177
|
dotString += dotOperatorStereotype(umlClass, operator.stereotype);
|
|
177
178
|
}
|
|
178
179
|
dotString += operator.name;
|
|
179
|
-
dotString += dotParameters(operator.parameters);
|
|
180
|
-
if (operator.returnParameters?.length > 0) {
|
|
180
|
+
dotString += dotParameters(operator.parameters, false, options.hideTypes);
|
|
181
|
+
if (operator.returnParameters?.length > 0 && !options.hideTypes) {
|
|
181
182
|
dotString += ': ' + dotParameters(operator.returnParameters, true);
|
|
182
183
|
}
|
|
183
184
|
if (options.hideModifiers === false && operator.modifiers?.length > 0) {
|
|
@@ -214,7 +215,10 @@ const dotOperatorStereotype = (umlClass, operatorStereotype) => {
|
|
|
214
215
|
}
|
|
215
216
|
return dotString + ' ';
|
|
216
217
|
};
|
|
217
|
-
const dotParameters = (parameters, returnParams = false) => {
|
|
218
|
+
const dotParameters = (parameters, returnParams = false, hideTypes = false) => {
|
|
219
|
+
if (hideTypes && !returnParams) {
|
|
220
|
+
return '()';
|
|
221
|
+
}
|
|
218
222
|
if (parameters.length == 1 && !parameters[0].name) {
|
|
219
223
|
if (returnParams) {
|
|
220
224
|
return parameters[0].type;
|
|
@@ -139,6 +139,39 @@ function addAssociationToDot(sourceUmlClass, targetUmlClass, association, classO
|
|
|
139
139
|
dotString += 'weight=3, ';
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
// Add function name labels on dependency arrows (not on realization/inheritance)
|
|
143
|
+
if (!association.realization && !classOptions.hideDepFunctions) {
|
|
144
|
+
const functionNames = getAssociationFunctionNames(sourceUmlClass, targetUmlClass, association);
|
|
145
|
+
if (functionNames.length > 0) {
|
|
146
|
+
const label = functionNames.map((fn) => fn + '\\l').join('');
|
|
147
|
+
dotString += `label="${label}", fontsize=10, `;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
142
150
|
return dotString + ']';
|
|
143
151
|
}
|
|
152
|
+
function getAssociationFunctionNames(sourceUmlClass, targetUmlClass, association) {
|
|
153
|
+
const names = new Set();
|
|
154
|
+
// Add explicitly captured function names
|
|
155
|
+
if (association.functionsCalled) {
|
|
156
|
+
for (const fn of association.functionsCalled) {
|
|
157
|
+
names.add(fn);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Cross-reference: if no explicit calls, check if source class's member
|
|
161
|
+
// access calls match any of the target class's operator names.
|
|
162
|
+
// Applies to libraries (using...for) and interfaces (e.g. IERC20(token).transfer())
|
|
163
|
+
if (names.size === 0 &&
|
|
164
|
+
(targetUmlClass.stereotype === umlClass_1.ClassStereotype.Library ||
|
|
165
|
+
targetUmlClass.stereotype === umlClass_1.ClassStereotype.Interface) &&
|
|
166
|
+
sourceUmlClass.memberAccessCalls?.size > 0 &&
|
|
167
|
+
targetUmlClass.operators?.length > 0) {
|
|
168
|
+
const targetOperatorNames = new Set(targetUmlClass.operators.map((op) => op.name));
|
|
169
|
+
for (const callName of sourceUmlClass.memberAccessCalls) {
|
|
170
|
+
if (targetOperatorNames.has(callName)) {
|
|
171
|
+
names.add(callName);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return [...names].sort();
|
|
176
|
+
}
|
|
144
177
|
//# sourceMappingURL=converterClasses2Dot.js.map
|
|
@@ -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");
|
|
@@ -630,10 +629,10 @@ const isElementary = (type) => {
|
|
|
630
629
|
exports.isElementary = isElementary;
|
|
631
630
|
const calcSectionOffset = (variable, sectionOffset = '0') => {
|
|
632
631
|
if (variable.dynamic) {
|
|
633
|
-
const hexStringOf32Bytes = (0,
|
|
634
|
-
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);
|
|
635
634
|
}
|
|
636
|
-
return ethers_1.
|
|
635
|
+
return (0, ethers_1.toBeHex)(BigInt(variable.fromSlot) + BigInt(sectionOffset));
|
|
637
636
|
};
|
|
638
637
|
exports.calcSectionOffset = calcSectionOffset;
|
|
639
638
|
const findDimensionLength = (umlClass, dimension, otherClasses) => {
|
|
@@ -791,7 +790,7 @@ const addDynamicVariables = async (storageSection, storageSections, url, contrac
|
|
|
791
790
|
continue;
|
|
792
791
|
}
|
|
793
792
|
// Add missing dynamic array variables
|
|
794
|
-
const arrayLength =
|
|
793
|
+
const arrayLength = Number(BigInt(variable.slotValue));
|
|
795
794
|
if (arrayLength > 1) {
|
|
796
795
|
// Add missing array variables to the referenced dynamic array
|
|
797
796
|
addArrayVariables(arrayLength, arrayItems, referenceStorageSection.variables);
|
package/lib/parserEtherscan.js
CHANGED
|
@@ -192,14 +192,29 @@ class EtherscanParser {
|
|
|
192
192
|
const description = `get verified source code for address ${contractAddress} from Etherscan API.`;
|
|
193
193
|
try {
|
|
194
194
|
debug(`About to get Solidity source code for ${contractAddress} from ${this.url}`);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
195
|
+
let response;
|
|
196
|
+
const maxRetries = 3;
|
|
197
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
198
|
+
response = await axios_1.default.get(this.url, {
|
|
199
|
+
params: {
|
|
200
|
+
module: 'contract',
|
|
201
|
+
action: 'getsourcecode',
|
|
202
|
+
address: contractAddress,
|
|
203
|
+
apikey: this.apiKey,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
// Retry on rate limit errors
|
|
207
|
+
if (!Array.isArray(response?.data?.result) &&
|
|
208
|
+
typeof response?.data?.result === 'string' &&
|
|
209
|
+
response.data.result.includes('rate limit') &&
|
|
210
|
+
attempt < maxRetries) {
|
|
211
|
+
const delay = attempt * 2000;
|
|
212
|
+
debug(`Rate limited on attempt ${attempt}. Retrying in ${delay}ms...`);
|
|
213
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
203
218
|
if (!Array.isArray(response?.data?.result)) {
|
|
204
219
|
throw new Error(`Failed to ${description}. No result array in HTTP data: ${JSON.stringify(response?.data)}`);
|
|
205
220
|
}
|
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
|
|
55
|
+
// Convert slot numbers to BigInts and offset dynamic arrays
|
|
49
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
|
@@ -34,7 +34,7 @@ Can also flatten or compare verified source files on Etherscan-like explorers.`)
|
|
|
34
34
|
parserEtherscan_1.networks.join(', '))
|
|
35
35
|
.default('ethereum')
|
|
36
36
|
.env('ETH_NETWORK'))
|
|
37
|
-
.addOption(new commander_1.Option('-e, --explorerUrl <url>', 'Override the `network` option with a custom blockchain explorer API URL. eg Polygon
|
|
37
|
+
.addOption(new commander_1.Option('-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'))
|
|
38
38
|
.addOption(new commander_1.Option('-k, --apiKey <key>', 'Blockchain explorer API key.').env('SCAN_API_KEY'))
|
|
39
39
|
.option('-bc, --backColor <color>', 'Canvas background color. "none" will use a transparent canvas.', 'white')
|
|
40
40
|
.option('-sc, --shapeColor <color>', 'Basic drawing color for graphics, not text', 'black')
|
|
@@ -61,6 +61,7 @@ program
|
|
|
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)
|
|
63
63
|
.option('-hf, --hideFunctions', 'hide functions from contracts, interfaces and libraries', false)
|
|
64
|
+
.option('-hy, --hideTypes', 'hide types of variables, function arguments and return types', false)
|
|
64
65
|
.option('-hp, --hidePrivates', 'hide private and internal attributes and operators', false)
|
|
65
66
|
.option('-hm, --hideModifiers', 'hide modifier functions from contracts', false)
|
|
66
67
|
.option('-ht, --hideEvents', 'hide events from contracts, interfaces and libraries', false)
|
|
@@ -72,6 +73,7 @@ program
|
|
|
72
73
|
.option('-hi, --hideInterfaces', 'hide interfaces', false)
|
|
73
74
|
.option('-ha, --hideAbstracts', 'hide abstract contracts', false)
|
|
74
75
|
.option('-hn, --hideFilename', 'hide relative path and file name', false)
|
|
76
|
+
.option('-hd, --hideDepFunctions', 'hide function names on dependency arrows', false)
|
|
75
77
|
.option('-s, --squash', 'squash inherited contracts to the base contract(s)', false)
|
|
76
78
|
.option('-hsc, --hideSourceContract', 'hide the source contract when using squash', false)
|
|
77
79
|
.action(async (fileFolderAddress, options, command) => {
|
package/lib/squashClasses.js
CHANGED
|
@@ -117,6 +117,12 @@ const recursiveSquash = (squashedClass, inheritedContractNames, baseClass, umlCl
|
|
|
117
117
|
baseClass.enums.forEach((e) => squashedClass.enums.push(e));
|
|
118
118
|
baseClass.structs.forEach((s) => squashedClass.structs.push(s));
|
|
119
119
|
baseClass.imports.forEach((i) => squashedClass.imports.push(i));
|
|
120
|
+
// Merge memberAccessCalls for cross-referencing function names on dependency arrows
|
|
121
|
+
if (baseClass.memberAccessCalls?.size > 0) {
|
|
122
|
+
for (const call of baseClass.memberAccessCalls) {
|
|
123
|
+
squashedClass.memberAccessCalls.add(call);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
120
126
|
// copy the functions
|
|
121
127
|
baseClass.operators.forEach((f) => squashedClass.operators.push({
|
|
122
128
|
...f,
|
package/lib/umlClass.d.ts
CHANGED
|
@@ -69,6 +69,7 @@ export interface Association {
|
|
|
69
69
|
parentUmlClassName?: string;
|
|
70
70
|
targetUmlClassName: string;
|
|
71
71
|
realization?: boolean;
|
|
72
|
+
functionsCalled?: string[];
|
|
72
73
|
}
|
|
73
74
|
export interface Constants {
|
|
74
75
|
name: string;
|
|
@@ -108,6 +109,7 @@ export declare class UmlClass implements ClassProperties {
|
|
|
108
109
|
associations: {
|
|
109
110
|
[name: string]: Association;
|
|
110
111
|
};
|
|
112
|
+
memberAccessCalls: Set<string>;
|
|
111
113
|
constructor(properties: ClassProperties);
|
|
112
114
|
addAssociation(association: Association): void;
|
|
113
115
|
/**
|
package/lib/umlClass.js
CHANGED
|
@@ -52,6 +52,8 @@ class UmlClass {
|
|
|
52
52
|
this.enums = [];
|
|
53
53
|
this.structs = [];
|
|
54
54
|
this.associations = {};
|
|
55
|
+
// Tracks all member access function call names for cross-referencing with using...for libraries
|
|
56
|
+
this.memberAccessCalls = new Set();
|
|
55
57
|
if (!properties || !properties.name) {
|
|
56
58
|
throw TypeError(`Failed to instantiate UML Class with no name property`);
|
|
57
59
|
}
|
|
@@ -73,6 +75,20 @@ class UmlClass {
|
|
|
73
75
|
if (association.referenceType === ReferenceType.Storage) {
|
|
74
76
|
this.associations[association.targetUmlClassName].referenceType = ReferenceType.Storage;
|
|
75
77
|
}
|
|
78
|
+
// Merge functionsCalled arrays with deduplication
|
|
79
|
+
if (association.functionsCalled?.length) {
|
|
80
|
+
const existing = this.associations[association.targetUmlClassName];
|
|
81
|
+
if (!existing.functionsCalled) {
|
|
82
|
+
existing.functionsCalled = [...association.functionsCalled];
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
for (const fn of association.functionsCalled) {
|
|
86
|
+
if (!existing.functionsCalled.includes(fn)) {
|
|
87
|
+
existing.functionsCalled.push(fn);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
76
92
|
}
|
|
77
93
|
}
|
|
78
94
|
/**
|
package/lib/utils/block.js
CHANGED
|
@@ -6,7 +6,7 @@ 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;
|
package/lib/utils/validators.js
CHANGED
|
@@ -3,13 +3,13 @@ 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
14
|
catch { /* validation failed */ }
|
|
15
15
|
throw new commander_1.InvalidArgumentError(`Address must be in hexadecimal format with a 0x prefix.`);
|
|
@@ -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,
|
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.25",
|
|
4
4
|
"description": "Solidity contract visualisation tool.",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
@@ -29,26 +29,27 @@
|
|
|
29
29
|
"axios-debug-log": "^1.0.0",
|
|
30
30
|
"cli-color": "^2.0.4",
|
|
31
31
|
"commander": "^12.1.0",
|
|
32
|
-
"convert-svg-to-png": "^0.
|
|
32
|
+
"convert-svg-to-png": "^0.7.1",
|
|
33
33
|
"debug": "^4.4.1",
|
|
34
34
|
"diff-match-patch": "^1.0.5",
|
|
35
|
-
"ethers": "^
|
|
35
|
+
"ethers": "^6.16.0",
|
|
36
36
|
"js-graph-algorithms": "^1.0.18",
|
|
37
|
-
"klaw": "^4.1.0"
|
|
37
|
+
"klaw": "^4.1.0",
|
|
38
|
+
"puppeteer": "^24.37.3"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@eslint/js": "^9.39.2",
|
|
41
42
|
"@openzeppelin/contracts": "^4.9.3",
|
|
42
43
|
"@types/diff-match-patch": "^1.0.36",
|
|
43
|
-
"@types/jest": "^
|
|
44
|
+
"@types/jest": "^30.0.0",
|
|
44
45
|
"@types/klaw": "^3.0.7",
|
|
45
46
|
"eslint": "^9.39.2",
|
|
46
47
|
"eslint-config-prettier": "^10.1.8",
|
|
47
|
-
"jest": "^30.
|
|
48
|
-
"prettier": "^3.
|
|
49
|
-
"ts-jest": "^29.4.
|
|
48
|
+
"jest": "^30.2.0",
|
|
49
|
+
"prettier": "^3.8.1",
|
|
50
|
+
"ts-jest": "^29.4.6",
|
|
50
51
|
"ts-node": "^10.9.2",
|
|
51
|
-
"typescript": "^5.
|
|
52
|
+
"typescript": "^5.9.3",
|
|
52
53
|
"typescript-eslint": "^8.55.0"
|
|
53
54
|
},
|
|
54
55
|
"files": [
|