sol2uml 2.4.3 → 2.5.0

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
@@ -14,7 +14,7 @@ See more contract diagrams [here](./examples/README.md).
14
14
  Storage layout diagram of USDC's [verified source code](https://etherscan.io/address/0xa2327a938febf5fec13bacfb16ae10ecbc4cbdcf#code) on Etherscan.
15
15
  ![USDC](./examples/storage/usdcData.svg)
16
16
 
17
- See more contract storage diagram examples [here](./examples/storage/README.md).
17
+ See an explanation of how storage diagrams work with lots of examples [here](./examples/storage/README.md).
18
18
 
19
19
  # Install
20
20
 
@@ -54,14 +54,19 @@ The three subcommands:
54
54
  The Solidity code can be pulled from verified source code on Blockchain explorers like Etherscan or from local Solidity files.
55
55
 
56
56
  Options:
57
- -V, --version output the version number
58
57
  -sf, --subfolders <value> number of subfolders that will be recursively searched for Solidity files. (default: all)
59
58
  -f, --outputFormat <value> output file format. (choices: "svg", "png", "dot", "all", default: "svg")
60
59
  -o, --outputFileName <value> output file name
61
60
  -i, --ignoreFilesOrFolders <filesOrFolders> comma separated list of files or folders to ignore
62
- -n, --network <network> Ethereum network (choices: "mainnet", "ropsten", "kovan", "rinkeby", "goerli", "sepolia", "polygon", "testnet.polygon", "arbitrum", "testnet.arbitrum", "avalanche", "testnet.avalanche", "bsc", "testnet.bsc", "crono", "fantom", "testnet.fantom", "moonbeam", "optimistic", "kovan-optimistic", "gnosisscan", default: "mainnet", env: ETH_NETWORK)
63
- -k, --apiKey <key> Blockchain explorer API key. eg Etherscan, Arbiscan, BscScan, CronoScan, FTMScan, PolygonScan or SnowTrace API key (env: SCAN_API_KEY)
61
+ -n, --network <network> Ethereum network (choices: "mainnet", "ropsten", "kovan", "rinkeby", "goerli", "sepolia", "polygon", "testnet.polygon", "arbitrum", "testnet.arbitrum", "avalanche", "testnet.avalanche", "bsc", "testnet.bsc", "crono", "fantom",
62
+ "testnet.fantom", "moonbeam", "optimistic", "kovan-optimistic", "gnosisscan", default: "mainnet", env: ETH_NETWORK)
63
+ -k, --apiKey <key> Blockchain explorer API key. eg Etherscan, Arbiscan, Optimism, BscScan, CronoScan, FTMScan, PolygonScan or SnowTrace API key (env: SCAN_API_KEY)
64
+ -bc, --backColor <color> Canvas background color. "none" will use a transparent canvas. (default: "white")
65
+ -sc, --shapeColor <color> Basic drawing color for graphics, not text (default: "black")
66
+ -fc, --fillColor <color> Color used to fill the background of a node (default: "gray95")
67
+ -tc, --textColor <color> Color used for text (default: "black")
64
68
  -v, --verbose run with debugging statements (default: false)
69
+ -V, --version output the version number
65
70
  -h, --help display help for command
66
71
 
67
72
  Commands:
@@ -131,6 +136,7 @@ Options:
131
136
  -s, --storage <address> The address of the contract with the storage values. This will be different from the contract with the code if a proxy contract is used. This is not needed if `fileFolderAddress` is an address and the contract is not proxied.
132
137
  -u, --url <url> URL of the Ethereum node to get storage values if the `data` option is used. (default: "http://localhost:8545", env: NODE_URL)
133
138
  -bn, --block <number> Block number to get the contract storage values from. (default: "latest")
139
+ -a, --array <number> Number of slots to display at the start and end of arrays. (default: "2")
134
140
  -h, --help display help for command
135
141
  ```
136
142
 
@@ -272,6 +278,20 @@ Heads/Tails:
272
278
 
273
279
  See more storage slot diagrams [here](./examples/storage/README.md).
274
280
 
281
+ # Styling Colors
282
+
283
+ The colors use by the diagrams can be configured using the `backColor`, `shapeColor`, `fillColor` and `textColor` options.
284
+ sol2uml uses the [X11 color scheme](https://graphviz.org/doc/info/colors.html#x11) for named colors.
285
+ Other color formats like Red-Green-Blue (RGB) can also be used. For example, #ffffff for white and #000000 for black.
286
+ See [Graphviz color](https://graphviz.org/docs/attr-types/color/) documentation for more details.
287
+
288
+ Here's an example using the color options
289
+ ```
290
+ sol2uml storage -sc deeppink -tc #ffffff -fc dimgrey -bc black 0xfCc00A1e250644d89AF0df661bC6f04891E21585
291
+ ```
292
+
293
+ ![Aave V3 Pool](./examples/storage/AaveV3PoolStorageColor.svg )
294
+
275
295
  # Version 2.x changes
276
296
 
277
297
  The biggest change with 2.x is the introduction of subcommands as sol2uml can now draw contract storage diagrams.
@@ -0,0 +1,31 @@
1
+ import { BigNumberish } from '@ethersproject/bignumber';
2
+ /**
3
+ * Singleton that caches a mapping of slot keys to values.
4
+ * Assumes all data is read from the same block and contract
5
+ */
6
+ export declare class SlotValueCache {
7
+ private static slotCache;
8
+ /**
9
+ * @param slotKeys array of slot numbers or slot keys in hexadecimal format
10
+ * @return cachedValues array of the slot values that are in the cache.
11
+ * @return missingKeys array of the slot keys that are not cached in hexadecimal format.
12
+ */
13
+ static readSlotValues(slotKeys: readonly BigNumberish[]): {
14
+ cachedValues: string[];
15
+ missingKeys: string[];
16
+ };
17
+ /**
18
+ * Adds the missing slot values to the cache and then returns all slot values from
19
+ * the cache for each of the `slotKeys`.
20
+ * @param slotKeys array of slot numbers or keys in hexadecimal format.
21
+ * @param missingKeys array of the slot keys that are not cached in hexadecimal format.
22
+ * @param missingValues array of slot values in hexadecimal format.
23
+ * @return values array of slot values for each of the `slotKeys`.
24
+ */
25
+ static addSlotValues(slotKeys: readonly BigNumberish[], missingKeys: readonly string[], missingValues: readonly string[]): string[];
26
+ /**
27
+ * Used for testing purposes to clear the cache.
28
+ * This allows tests to run against different contracts and blockTags
29
+ */
30
+ static clear(): void;
31
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SlotValueCache = void 0;
4
+ const bignumber_1 = require("@ethersproject/bignumber");
5
+ const debug = require('debug')('sol2uml');
6
+ /**
7
+ * Singleton that caches a mapping of slot keys to values.
8
+ * Assumes all data is read from the same block and contract
9
+ */
10
+ class SlotValueCache {
11
+ /**
12
+ * @param slotKeys array of slot numbers or slot keys in hexadecimal format
13
+ * @return cachedValues array of the slot values that are in the cache.
14
+ * @return missingKeys array of the slot keys that are not cached in hexadecimal format.
15
+ */
16
+ static readSlotValues(slotKeys) {
17
+ const cachedValues = [];
18
+ const missingKeys = [];
19
+ slotKeys.forEach((slotKey, i) => {
20
+ const key = bignumber_1.BigNumber.from(slotKey).toHexString();
21
+ if (this.slotCache[key]) {
22
+ cachedValues.push(this.slotCache[key]);
23
+ }
24
+ else {
25
+ missingKeys.push(key);
26
+ }
27
+ });
28
+ return { cachedValues, missingKeys };
29
+ }
30
+ /**
31
+ * Adds the missing slot values to the cache and then returns all slot values from
32
+ * the cache for each of the `slotKeys`.
33
+ * @param slotKeys array of slot numbers or keys in hexadecimal format.
34
+ * @param missingKeys array of the slot keys that are not cached in hexadecimal format.
35
+ * @param missingValues array of slot values in hexadecimal format.
36
+ * @return values array of slot values for each of the `slotKeys`.
37
+ */
38
+ static addSlotValues(slotKeys, missingKeys, missingValues) {
39
+ if (missingKeys?.length !== missingValues?.length) {
40
+ throw Error(`${missingKeys?.length} keys does not match ${missingValues?.length} values`);
41
+ }
42
+ missingKeys.forEach((key, i) => {
43
+ if (!this.slotCache[key]) {
44
+ debug(`cached slot ${key} with ${missingValues[i]}`);
45
+ this.slotCache[key] = missingValues[i];
46
+ }
47
+ });
48
+ return slotKeys.map((slotKey) => {
49
+ const key = bignumber_1.BigNumber.from(slotKey).toHexString();
50
+ // it should find the slot value in the cache. if not it'll return undefined
51
+ return this.slotCache[key];
52
+ });
53
+ }
54
+ /**
55
+ * Used for testing purposes to clear the cache.
56
+ * This allows tests to run against different contracts and blockTags
57
+ */
58
+ static clear() {
59
+ this.slotCache = {};
60
+ }
61
+ }
62
+ exports.SlotValueCache = SlotValueCache;
63
+ // Singleton of cached slot keys mapped to values
64
+ SlotValueCache.slotCache = {};
65
+ //# sourceMappingURL=SlotValueCache.js.map
@@ -1,2 +1,2 @@
1
1
  import { Association, UmlClass } from './umlClass';
2
- export declare const findAssociatedClass: (association: Association, sourceUmlClass: UmlClass, umlClasses: UmlClass[], searchedAbsolutePaths?: string[]) => UmlClass | undefined;
2
+ export declare const findAssociatedClass: (association: Association, sourceUmlClass: UmlClass, umlClasses: readonly UmlClass[], searchedAbsolutePaths?: string[]) => UmlClass | undefined;
@@ -1,29 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.findAssociatedClass = void 0;
4
- const umlClass_1 = require("./umlClass");
5
4
  // Find the UML class linked to the association
6
5
  const findAssociatedClass = (association, sourceUmlClass, umlClasses, searchedAbsolutePaths = []) => {
7
- let umlClass = umlClasses.find((targetUmlClass) => {
8
- // is the source class link via the association to the target class?
9
- if (isAssociated(association, sourceUmlClass, targetUmlClass))
10
- return true;
11
- // Not linked so now try linking to target under the node_modules folder.
12
- // eg remove node_modules from node_modules/@openzeppelin/contracts-upgradeable/proxy/Initializable.sol
13
- // is the target class under node_modules?
14
- if (targetUmlClass.relativePath.match(/^node_modules\//)) {
15
- // clone the target and updated absolutePath and relativePath so it's no longer under node_modules
16
- const clonedTargetClass = new umlClass_1.UmlClass(targetUmlClass);
17
- clonedTargetClass.absolutePath =
18
- targetUmlClass.absolutePath.replace(/^node_modules\//, '');
19
- clonedTargetClass.relativePath =
20
- targetUmlClass.relativePath.replace(/^node_modules\//, '');
21
- // is the source class link via the association to the target class?
22
- return isAssociated(association, sourceUmlClass, clonedTargetClass);
23
- }
24
- // could not find a link from the source to target via the association
25
- return false;
26
- });
6
+ const umlClass = umlClasses.find((targetUmlClass) => isAssociated(association, sourceUmlClass, targetUmlClass));
27
7
  // If a link was found
28
8
  if (umlClass)
29
9
  return umlClass;
@@ -1,10 +1,21 @@
1
1
  import { ASTNode } from '@solidity-parser/parser/dist/src/ast-types';
2
2
  import { UmlClass } from './umlClass';
3
+ import { Remapping } from './parserEtherscan';
3
4
  /**
4
5
  * Convert solidity parser output of type `ASTNode` to UML classes of type `UMLClass`
5
6
  * @param node output of Solidity parser of type `ASTNode`
6
7
  * @param relativePath relative path from the working directory to the Solidity source file
8
+ * @param remappings used to rename relative paths
7
9
  * @param filesystem flag if Solidity source code was parsed from the filesystem or Etherscan
8
10
  * @return umlClasses array of UML class definitions of type `UmlClass`
9
11
  */
10
- export declare function convertAST2UmlClasses(node: ASTNode, relativePath: string, filesystem?: boolean): UmlClass[];
12
+ export declare function convertAST2UmlClasses(node: ASTNode, relativePath: string, remappings: Remapping[], filesystem?: boolean): UmlClass[];
13
+ /**
14
+ * Used to rename import file names. For example
15
+ * @openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
16
+ * to
17
+ * lib/openzeppelin-contracts/@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
18
+ * @param fileName file name in the Solidity code
19
+ * @param mappings an array of remappings from Etherscan's settings
20
+ */
21
+ export declare const renameFile: (fileName: string, mappings: Remapping[]) => string;
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.convertAST2UmlClasses = void 0;
26
+ exports.renameFile = exports.convertAST2UmlClasses = void 0;
27
27
  const path = __importStar(require("path"));
28
28
  const path_1 = require("path");
29
29
  const umlClass_1 = require("./umlClass");
@@ -34,10 +34,11 @@ let umlClasses;
34
34
  * Convert solidity parser output of type `ASTNode` to UML classes of type `UMLClass`
35
35
  * @param node output of Solidity parser of type `ASTNode`
36
36
  * @param relativePath relative path from the working directory to the Solidity source file
37
+ * @param remappings used to rename relative paths
37
38
  * @param filesystem flag if Solidity source code was parsed from the filesystem or Etherscan
38
39
  * @return umlClasses array of UML class definitions of type `UmlClass`
39
40
  */
40
- function convertAST2UmlClasses(node, relativePath, filesystem = false) {
41
+ function convertAST2UmlClasses(node, relativePath, remappings, filesystem = false) {
41
42
  const imports = [];
42
43
  umlClasses = [];
43
44
  if (node.type === 'SourceUnit') {
@@ -110,11 +111,12 @@ function convertAST2UmlClasses(node, relativePath, filesystem = false) {
110
111
  }
111
112
  else {
112
113
  // this has come from Etherscan
113
- const importPath = childNode.path[0] === '.'
114
+ const remappedFile = (0, exports.renameFile)(childNode.path, remappings);
115
+ const importPath = remappedFile[0] === '.'
114
116
  ? // Use Linux paths, not Windows paths, to resolve Etherscan files
115
- path_1.posix.join(codeFolder.toString(), childNode.path)
116
- : childNode.path;
117
- debug(`codeFolder ${codeFolder} childNode.path ${childNode.path}`);
117
+ path_1.posix.join(codeFolder.toString(), remappedFile)
118
+ : remappedFile;
119
+ debug(`codeFolder ${codeFolder} childNode.path ${childNode.path} remapped to ${remappedFile}`);
118
120
  const newImport = {
119
121
  absolutePath: importPath,
120
122
  classNames: childNode.symbolAliases
@@ -126,7 +128,10 @@ function convertAST2UmlClasses(node, relativePath, filesystem = false) {
126
128
  })
127
129
  : [],
128
130
  };
129
- debug(`Added Etherscan import ${newImport.absolutePath} with class names: ${newImport.classNames}`);
131
+ debug(`Added Etherscan import ${newImport.absolutePath} with:`);
132
+ newImport.classNames.forEach((className) => {
133
+ debug(`\t alias ${className.className}, name ${className.className}`);
134
+ });
130
135
  imports.push(newImport);
131
136
  }
132
137
  }
@@ -671,4 +676,25 @@ function parseContractKind(kind) {
671
676
  throw Error(`Invalid kind ${kind}`);
672
677
  }
673
678
  }
679
+ /**
680
+ * Used to rename import file names. For example
681
+ * @openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
682
+ * to
683
+ * lib/openzeppelin-contracts/@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
684
+ * @param fileName file name in the Solidity code
685
+ * @param mappings an array of remappings from Etherscan's settings
686
+ */
687
+ const renameFile = (fileName, mappings) => {
688
+ let renamedFile = fileName;
689
+ for (const mapping of mappings) {
690
+ if (renamedFile.match(mapping.from)) {
691
+ const beforeFileName = renamedFile;
692
+ renamedFile = renamedFile.replace(mapping.from, mapping.to);
693
+ debug(`remapping ${beforeFileName} to ${renamedFile}`);
694
+ break;
695
+ }
696
+ }
697
+ return renamedFile;
698
+ };
699
+ exports.renameFile = renameFile;
674
700
  //# sourceMappingURL=converterAST2Classes.js.map
@@ -13,5 +13,9 @@ export interface ClassOptions {
13
13
  hideAbstracts?: boolean;
14
14
  hideFilename?: boolean;
15
15
  hideSourceContract?: boolean;
16
+ backColor?: string;
17
+ shapeColor?: string;
18
+ fillColor?: string;
19
+ textColor?: string;
16
20
  }
17
21
  export declare const convertClass2Dot: (umlClass: UmlClass, options?: ClassOptions) => string;
@@ -18,9 +18,10 @@ function convertUmlClasses2Dot(umlClasses, clusterFolders = false, classOptions
18
18
  let dotString = `
19
19
  digraph UmlClassDiagram {
20
20
  rankdir=BT
21
- color=black
22
21
  arrowhead=open
23
- node [shape=record, style=filled, fillcolor=gray95]`;
22
+ bgcolor="${classOptions.backColor}"
23
+ edge [color="${classOptions.shapeColor}"]
24
+ node [shape=record, style=filled, color="${classOptions.shapeColor}", fillcolor="${classOptions.fillColor}", fontcolor="${classOptions.textColor}"]`;
24
25
  // Sort UML Classes by folder of source file
25
26
  const umlClassesSortedByCodePath = sortUmlClassesByCodePath(umlClasses);
26
27
  let currentCodeFolder = '';
@@ -1,9 +1,11 @@
1
- import { Attribute, UmlClass } from './umlClass';
1
+ import { Attribute, AttributeType, UmlClass } from './umlClass';
2
2
  import { BigNumberish } from '@ethersproject/bignumber';
3
- export declare enum StorageType {
3
+ export declare enum StorageSectionType {
4
4
  Contract = "Contract",
5
5
  Struct = "Struct",
6
- Array = "Array"
6
+ Array = "Array",
7
+ Bytes = "Bytes",
8
+ String = "String"
7
9
  }
8
10
  export interface Variable {
9
11
  id: number;
@@ -12,47 +14,67 @@ export interface Variable {
12
14
  byteSize: number;
13
15
  byteOffset: number;
14
16
  type: string;
17
+ attributeType: AttributeType;
15
18
  dynamic: boolean;
16
- variable?: string;
19
+ name?: string;
17
20
  contractName?: string;
18
- noValue: boolean;
19
- value?: string;
20
- referenceStorageId?: number;
21
- enumId?: number;
21
+ displayValue: boolean;
22
+ getValue?: boolean;
23
+ slotValue?: string;
24
+ parsedValue?: string;
25
+ referenceSectionId?: number;
26
+ enumValues?: string[];
22
27
  }
23
- export interface Storage {
28
+ export interface StorageSection {
24
29
  id: number;
25
30
  name: string;
26
31
  address?: string;
27
- slotKey?: string;
28
- type: StorageType;
32
+ offset?: string;
33
+ type: StorageSectionType;
29
34
  arrayLength?: number;
30
35
  arrayDynamic?: boolean;
36
+ mapping: boolean;
31
37
  variables: Variable[];
32
38
  }
33
- /**
34
- *
35
- * @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy
36
- * @param contractAddress Contract address to get the storage slot values from.
37
- * If proxied, use proxy and not the implementation contract.
38
- * @param storage is mutated with the storage values
39
- * @param blockTag block number or `latest`
40
- */
41
- export declare const addStorageValues: (url: string, contractAddress: string, storage: Storage, blockTag?: BigNumberish | 'latest') => Promise<void>;
42
39
  /**
43
40
  *
44
41
  * @param contractName name of the contract to get storage layout.
45
42
  * @param umlClasses array of UML classes of type `UMLClass`
43
+ * @param arrayItems the number of items to display at the start and end of an array
46
44
  * @param contractFilename relative path of the contract in the file system
47
- * @return array of storage objects with consecutive slots
45
+ * @return storageSections array of storageSection objects
46
+ */
47
+ export declare const convertClasses2StorageSections: (contractName: string, umlClasses: UmlClass[], arrayItems: number, contractFilename?: string) => StorageSection[];
48
+ /**
49
+ * Recursively adds new storage sections under a class attribute.
50
+ * also returns the allowed enum values
51
+ * @param attribute the attribute that is referencing a storage section
52
+ * @param umlClass contract or file level struct
53
+ * @param otherClasses array of all the UML Classes
54
+ * @param storageSections mutable array of storageSection objects
55
+ * @param mapping flags that the storage section is under a mapping
56
+ * @param arrayItems the number of items to display at the start and end of an array
57
+ * @return storageSection new storage section that was added or undefined if none was added.
58
+ * @return enumValues array of allowed enum values. undefined if attribute is not an enum
48
59
  */
49
- export declare const convertClasses2Storages: (contractName: string, umlClasses: UmlClass[], contractFilename?: string) => Storage[];
50
- export declare const parseReferenceStorage: (attribute: Attribute, umlClass: UmlClass, otherClasses: UmlClass[], storages: Storage[]) => Storage | undefined;
51
- export declare const calcStorageByteSize: (attribute: Attribute, umlClass: UmlClass, otherClasses: UmlClass[]) => {
60
+ export declare const parseStorageSectionFromAttribute: (attribute: Attribute, umlClass: UmlClass, otherClasses: readonly UmlClass[], storageSections: StorageSection[], mapping: boolean, arrayItems: number) => {
61
+ storageSection: StorageSection;
62
+ enumValues?: string[];
63
+ };
64
+ export declare const calcStorageByteSize: (attribute: Attribute, umlClass: UmlClass, otherClasses: readonly UmlClass[]) => {
52
65
  size: number;
53
66
  dynamic: boolean;
54
67
  };
55
68
  export declare const isElementary: (type: string) => boolean;
56
- export declare const calcSlotKey: (variable: Variable) => string | undefined;
57
- export declare const offsetStorageSlots: (storage: Storage, slots: number, storages: Storage[]) => void;
58
- export declare const findDimensionLength: (umlClass: UmlClass, dimension: string) => number;
69
+ export declare const calcSectionOffset: (variable: Variable, sectionOffset?: string) => string;
70
+ export declare const findDimensionLength: (umlClass: UmlClass, dimension: string, otherClasses: readonly UmlClass[]) => number;
71
+ /**
72
+ * Recursively adds variables for dynamic string, bytes or arrays
73
+ * @param storageSection
74
+ * @param storageSections
75
+ * @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy
76
+ * @param contractAddress Contract address to get the storage slot values from.
77
+ * @param arrayItems the number of items to display at the start and end of an array
78
+ * @param blockTag block number or `latest`
79
+ */
80
+ export declare const addDynamicVariables: (storageSection: StorageSection, storageSections: StorageSection[], url: string, contractAddress: string, arrayItems: number, blockTag: BigNumberish) => Promise<void>;