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 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
@@ -1,4 +1,4 @@
1
- import { BigNumberish } from '@ethersproject/bignumber';
1
+ import { BigNumberish } from 'ethers';
2
2
  /**
3
3
  * Singleton that caches a mapping of slot keys to values.
4
4
  * Assumes all data is read from the same block and contract
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SlotValueCache = void 0;
4
- const bignumber_1 = require("@ethersproject/bignumber");
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 = bignumber_1.BigNumber.from(slotKey).toHexString();
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 = bignumber_1.BigNumber.from(slotKey).toHexString();
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
  });
@@ -1,2 +1,2 @@
1
1
  import { Association, UmlClass } from './umlClass';
2
- export declare const findAssociatedClass: (association: Association, sourceUmlClass: UmlClass, umlClasses: readonly UmlClass[], _searchedAbsolutePaths?: string[]) => UmlClass | undefined;
2
+ export declare const findAssociatedClass: (association: Association, sourceUmlClass: UmlClass, umlClasses: readonly UmlClass[]) => UmlClass | undefined;
@@ -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, _searchedAbsolutePaths = []) => {
6
- const umlClass = umlClasses.find((targetUmlClass) => {
7
- const targetParentClass = association.parentUmlClassName &&
8
- targetUmlClass.parentId !== undefined
9
- ? umlClasses[targetUmlClass.parentId]
10
- : undefined;
11
- return isAssociated(association, sourceUmlClass, targetUmlClass, targetParentClass);
12
- });
13
- // If a link was found
14
- if (umlClass)
15
- return umlClass;
16
- // // Could not find association so now need to recursively look at imports of imports
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
- const _findInheritedType = (association, sourceUmlClass, umlClasses) => {
87
- // Get all realized associations.
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
- const parent = (0, exports.findAssociatedClass)(parentAssociation, sourceUmlClass, umlClasses);
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
- // For each struct on the parent
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
- // For each enum on the parent
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 look for inherited types
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
- parseExpression(expression.expression, umlClass);
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
- dotString += `${indentString}${attribute.name}: ${attribute.type}${sourceContract}\\l`;
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
@@ -1,5 +1,5 @@
1
1
  import { Attribute, AttributeType, UmlClass } from './umlClass';
2
- import { BigNumberish } from '@ethersproject/bignumber';
2
+ import { BigNumberish } from 'ethers';
3
3
  export declare enum StorageSectionType {
4
4
  Contract = "Contract",
5
5
  Struct = "Struct",
@@ -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, utils_1.hexZeroPad)(ethers_1.BigNumber.from(variable.fromSlot).add(sectionOffset).toHexString(), 32);
634
- return (0, utils_1.keccak256)(hexStringOf32Bytes);
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.BigNumber.from(variable.fromSlot).add(sectionOffset).toHexString();
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 = ethers_1.BigNumber.from(variable.slotValue).toNumber();
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);
@@ -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
- const response = await axios_1.default.get(this.url, {
196
- params: {
197
- module: 'contract',
198
- action: 'getsourcecode',
199
- address: contractAddress,
200
- apikey: this.apiKey,
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
  }
@@ -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(bignumber_1.BigNumber.from(variable.offset));
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 BigNumbers and offset dynamic arrays
55
+ // Convert slot numbers to BigInts and offset dynamic arrays
49
56
  const slotKeys = uniqueFromSlots.map((fromSlot) => {
50
57
  if (storageSection.offset) {
51
- return bignumber_1.BigNumber.from(storageSection.offset).add(fromSlot);
58
+ return BigInt(storageSection.offset) + BigInt(fromSlot);
52
59
  }
53
- return bignumber_1.BigNumber.from(fromSlot);
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
- bignumber_1.BigNumber.from(variable.offset).eq(fromSlot)) {
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
- bignumber_1.BigNumber.from(variable.toSlot).gt(fromSlot)) {
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, utils_1.formatUnits)('0x' + variableValue, 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, utils_1.getAddress)('0x' + variableValue);
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 = bignumber_1.BigNumber.from('0x' + variableValue).toNumber();
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 = bignumber_1.BigNumber.from('0x' + lastByte);
152
+ const size = BigInt('0x' + lastByte);
146
153
  // Check if the last bit is set by AND the size with 0x01
147
- if (size.and(1).eq(1)) {
154
+ if ((size & 1n) === 1n) {
148
155
  // Return the number of chars or bytes
149
- return bignumber_1.BigNumber.from(variable.slotValue)
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.toNumber());
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, utils_1.getAddress)('0x' + variableValue);
169
+ return (0, ethers_1.getAddress)('0x' + variableValue);
166
170
  }
167
171
  if (variable.type.match(/^uint([0-9]*)$/)) {
168
- const parsedValue = (0, utils_1.formatUnits)('0x' + variableValue, 0);
169
- return (0, utils_1.commify)(parsedValue);
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 = bignumber_1.BigNumber.from('0x' + variableValue);
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 = bignumber_1.BigNumber.from(bitSize).div(4).toNumber();
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.and(mask);
186
- if (negative.gt(0)) {
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 = bignumber_1.BigNumber.from(negativeOne).sub(rawValue).add(1).mul(-1);
192
+ const negativeOne = BigInt('0xFF' + 'F'.repeat(hexSize - 2));
193
+ rawValue = (negativeOne - rawValue + 1n) * -1n;
190
194
  }
191
- const parsedValue = (0, utils_1.formatUnits)(rawValue, 0);
192
- return (0, utils_1.commify)(parsedValue);
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.ethers.providers.JsonRpcProvider(url);
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) => bignumber_1.BigNumber.from(a.id).gt(b.id) ? 1 : -1);
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 = bignumber_1.BigNumber.from(last4bits).toNumber();
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 = bignumber_1.BigNumber.from(variable.slotValue).toNumber();
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, utils_1.toUtf8String)(bytes);
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 Mumbai testnet https://api-testnet.polygonscan.com/api').env('EXPLORER_URL'))
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) => {
@@ -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
  /**
@@ -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.ethers.providers.JsonRpcProvider(options.url);
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;
@@ -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 utils_1 = require("ethers/lib/utils");
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, utils_1.getAddress)(address);
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, utils_1.keccak256)((0, utils_1.toUtf8Bytes)(slot));
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,
@@ -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 { /* outputFilename does not exist yet */ }
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
- outputFilePath: pngFilename,
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.23",
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.6.4",
32
+ "convert-svg-to-png": "^0.7.1",
33
33
  "debug": "^4.4.1",
34
34
  "diff-match-patch": "^1.0.5",
35
- "ethers": "^5.8.0",
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": "^29.5.14",
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.0.0",
48
- "prettier": "^3.5.3",
49
- "ts-jest": "^29.4.0",
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.8.3",
52
+ "typescript": "^5.9.3",
52
53
  "typescript-eslint": "^8.55.0"
53
54
  },
54
55
  "files": [