sol2uml 2.0.5 → 2.1.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/LICENSE +1 -1
- package/README.md +8 -4
- package/lib/converterAST2Classes.js +3 -1
- package/lib/converterClasses2Storage.d.ts +14 -7
- package/lib/converterClasses2Storage.js +66 -49
- package/lib/converterStorage2Dot.d.ts +3 -3
- package/lib/converterStorage2Dot.js +64 -55
- package/lib/slotValues.d.ts +3 -0
- package/lib/slotValues.js +50 -20
- package/lib/sol2uml.js +33 -10
- package/lib/writerFiles.d.ts +1 -1
- package/lib/writerFiles.js +12 -14
- package/package.json +2 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -115,12 +115,16 @@ Visually display a contract's storage slots.
|
|
|
115
115
|
WARNING: sol2uml does not use the Solidity compiler so may differ with solc. A known example is fixed-sized arrays declared with an expression will fail to be sized.
|
|
116
116
|
|
|
117
117
|
Arguments:
|
|
118
|
-
fileFolderAddress
|
|
118
|
+
fileFolderAddress file name, base folder or contract address
|
|
119
119
|
|
|
120
120
|
Options:
|
|
121
|
-
-c, --
|
|
122
|
-
-
|
|
123
|
-
|
|
121
|
+
-c, --contract <name> Contract name in local Solidity files. Not needed when using an address as the first argument as the contract name can be derived from Etherscan.
|
|
122
|
+
-d, --data Gets the values in the storage slots from an Ethereum node. (default: false)
|
|
123
|
+
-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
|
|
124
|
+
the contract is not proxied.
|
|
125
|
+
-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)
|
|
126
|
+
-bn, --block <number> Block number to get the contract storage values from. (default: "latest")
|
|
127
|
+
-h, --help display help for command
|
|
124
128
|
```
|
|
125
129
|
|
|
126
130
|
### Flatten usage
|
|
@@ -98,7 +98,9 @@ function convertAST2UmlClasses(node, relativePath, filesystem = false) {
|
|
|
98
98
|
}
|
|
99
99
|
else {
|
|
100
100
|
// this has come from Etherscan
|
|
101
|
-
const importPath =
|
|
101
|
+
const importPath = childNode.path[0] === '@'
|
|
102
|
+
? childNode.path
|
|
103
|
+
: path.join(codeFolder, childNode.path);
|
|
102
104
|
imports.push({
|
|
103
105
|
absolutePath: importPath,
|
|
104
106
|
classNames: childNode.symbolAliases
|
|
@@ -3,7 +3,7 @@ export declare enum StorageType {
|
|
|
3
3
|
Contract = 0,
|
|
4
4
|
Struct = 1
|
|
5
5
|
}
|
|
6
|
-
export interface
|
|
6
|
+
export interface Variable {
|
|
7
7
|
id: number;
|
|
8
8
|
fromSlot: number;
|
|
9
9
|
toSlot: number;
|
|
@@ -12,18 +12,25 @@ export interface Storage {
|
|
|
12
12
|
type: string;
|
|
13
13
|
variable: string;
|
|
14
14
|
contractName?: string;
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
values: string[];
|
|
16
|
+
structStorageId?: number;
|
|
17
17
|
enumId?: number;
|
|
18
18
|
}
|
|
19
|
-
export interface
|
|
19
|
+
export interface Storage {
|
|
20
20
|
id: number;
|
|
21
21
|
name: string;
|
|
22
22
|
address?: string;
|
|
23
23
|
type: StorageType;
|
|
24
|
-
|
|
24
|
+
variables: Variable[];
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
*
|
|
28
|
+
* @param url
|
|
29
|
+
* @param contractAddress Contract address to get the storage slot values from
|
|
30
|
+
* @param storage is mutated with the storage values
|
|
31
|
+
*/
|
|
32
|
+
export declare const addStorageValues: (url: string, contractAddress: string, storage: Storage, blockTag: string) => Promise<void>;
|
|
33
|
+
export declare const convertClasses2Storages: (contractName: string, umlClasses: UmlClass[]) => Storage[];
|
|
34
|
+
export declare const parseStructStorage: (attribute: Attribute, otherClasses: UmlClass[], storages: Storage[]) => Storage | undefined;
|
|
28
35
|
export declare const calcStorageByteSize: (attribute: Attribute, umlClass: UmlClass, otherClasses: UmlClass[]) => number;
|
|
29
36
|
export declare const isElementary: (type: string) => boolean;
|
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isElementary = exports.calcStorageByteSize = exports.
|
|
3
|
+
exports.isElementary = exports.calcStorageByteSize = exports.parseStructStorage = exports.convertClasses2Storages = exports.addStorageValues = exports.StorageType = void 0;
|
|
4
4
|
const umlClass_1 = require("./umlClass");
|
|
5
5
|
const associations_1 = require("./associations");
|
|
6
|
+
const slotValues_1 = require("./slotValues");
|
|
6
7
|
var StorageType;
|
|
7
8
|
(function (StorageType) {
|
|
8
9
|
StorageType[StorageType["Contract"] = 0] = "Contract";
|
|
9
10
|
StorageType[StorageType["Struct"] = 1] = "Struct";
|
|
10
11
|
})(StorageType = exports.StorageType || (exports.StorageType = {}));
|
|
11
|
-
let storageObjectId = 1;
|
|
12
12
|
let storageId = 1;
|
|
13
|
-
|
|
13
|
+
let variableId = 1;
|
|
14
|
+
/**
|
|
15
|
+
*
|
|
16
|
+
* @param url
|
|
17
|
+
* @param contractAddress Contract address to get the storage slot values from
|
|
18
|
+
* @param storage is mutated with the storage values
|
|
19
|
+
*/
|
|
20
|
+
const addStorageValues = async (url, contractAddress, storage, blockTag) => {
|
|
21
|
+
const slots = storage.variables.map((s) => s.fromSlot);
|
|
22
|
+
const values = await (0, slotValues_1.getStorageValues)(url, contractAddress, slots, blockTag);
|
|
23
|
+
storage.variables.forEach((storage, i) => {
|
|
24
|
+
storage.values = [values[i]];
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
exports.addStorageValues = addStorageValues;
|
|
28
|
+
const convertClasses2Storages = (contractName, umlClasses) => {
|
|
14
29
|
// Find the base UML Class from the base contract name
|
|
15
30
|
const umlClass = umlClasses.find(({ name }) => {
|
|
16
31
|
return name === contractName;
|
|
@@ -18,25 +33,25 @@ const convertClasses2StorageObjects = (contractName, umlClasses) => {
|
|
|
18
33
|
if (!umlClass) {
|
|
19
34
|
throw Error(`Failed to find contract with name "${contractName}"`);
|
|
20
35
|
}
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
id:
|
|
36
|
+
const storages = [];
|
|
37
|
+
const variables = parseVariables(umlClass, umlClasses, [], storages, []);
|
|
38
|
+
storages.unshift({
|
|
39
|
+
id: storageId++,
|
|
25
40
|
name: contractName,
|
|
26
41
|
type: StorageType.Contract,
|
|
27
|
-
|
|
42
|
+
variables: variables,
|
|
28
43
|
});
|
|
29
|
-
return
|
|
44
|
+
return storages;
|
|
30
45
|
};
|
|
31
|
-
exports.
|
|
46
|
+
exports.convertClasses2Storages = convertClasses2Storages;
|
|
32
47
|
/**
|
|
33
|
-
* Recursively parses the storage for a given contract.
|
|
48
|
+
* Recursively parses the storage variables for a given contract.
|
|
34
49
|
* @param umlClass contract or file level struct
|
|
35
50
|
* @param umlClasses other contracts, structs and enums that may be a type of a storage variable.
|
|
36
|
-
* @param
|
|
37
|
-
* @param
|
|
51
|
+
* @param variables mutable array of storage slots that is appended to
|
|
52
|
+
* @param storages mutable array of storages that is appended with structs
|
|
38
53
|
*/
|
|
39
|
-
const
|
|
54
|
+
const parseVariables = (umlClass, umlClasses, variables, storages, inheritedContracts) => {
|
|
40
55
|
// Add storage slots from inherited contracts first.
|
|
41
56
|
// Get immediate parent contracts that the class inherits from
|
|
42
57
|
const parentContracts = umlClass.getParentContracts();
|
|
@@ -50,7 +65,7 @@ const parseStorage = (umlClass, umlClasses, storages, storageObjects, inheritedC
|
|
|
50
65
|
if (!parentClass)
|
|
51
66
|
throw Error(`Failed to find parent contract ${parent.targetUmlClassName} of ${umlClass.absolutePath}`);
|
|
52
67
|
// recursively parse inherited contract
|
|
53
|
-
|
|
68
|
+
parseVariables(parentClass, umlClasses, variables, storages, inheritedContracts);
|
|
54
69
|
});
|
|
55
70
|
// Parse storage for each attribute
|
|
56
71
|
umlClass.attributes.forEach((attribute) => {
|
|
@@ -59,20 +74,20 @@ const parseStorage = (umlClass, umlClasses, storages, storageObjects, inheritedC
|
|
|
59
74
|
return;
|
|
60
75
|
const byteSize = (0, exports.calcStorageByteSize)(attribute, umlClass, umlClasses);
|
|
61
76
|
// find any dependent structs
|
|
62
|
-
const linkedStruct = (0, exports.
|
|
63
|
-
const
|
|
77
|
+
const linkedStruct = (0, exports.parseStructStorage)(attribute, umlClasses, storages);
|
|
78
|
+
const structStorageId = linkedStruct?.id;
|
|
64
79
|
// Get the toSlot of the last storage item
|
|
65
80
|
let lastToSlot = 0;
|
|
66
81
|
let nextOffset = 0;
|
|
67
|
-
if (
|
|
68
|
-
const lastStorage =
|
|
82
|
+
if (variables.length > 0) {
|
|
83
|
+
const lastStorage = variables[variables.length - 1];
|
|
69
84
|
lastToSlot = lastStorage.toSlot;
|
|
70
85
|
nextOffset = lastStorage.byteOffset + lastStorage.byteSize;
|
|
71
86
|
}
|
|
72
87
|
if (nextOffset + byteSize > 32) {
|
|
73
|
-
const nextFromSlot =
|
|
74
|
-
|
|
75
|
-
id:
|
|
88
|
+
const nextFromSlot = variables.length > 0 ? lastToSlot + 1 : 0;
|
|
89
|
+
variables.push({
|
|
90
|
+
id: variableId++,
|
|
76
91
|
fromSlot: nextFromSlot,
|
|
77
92
|
toSlot: nextFromSlot + Math.floor((byteSize - 1) / 32),
|
|
78
93
|
byteSize,
|
|
@@ -80,12 +95,13 @@ const parseStorage = (umlClass, umlClasses, storages, storageObjects, inheritedC
|
|
|
80
95
|
type: attribute.type,
|
|
81
96
|
variable: attribute.name,
|
|
82
97
|
contractName: umlClass.name,
|
|
83
|
-
|
|
98
|
+
structStorageId,
|
|
99
|
+
values: [],
|
|
84
100
|
});
|
|
85
101
|
}
|
|
86
102
|
else {
|
|
87
|
-
|
|
88
|
-
id:
|
|
103
|
+
variables.push({
|
|
104
|
+
id: variableId++,
|
|
89
105
|
fromSlot: lastToSlot,
|
|
90
106
|
toSlot: lastToSlot,
|
|
91
107
|
byteSize,
|
|
@@ -93,18 +109,19 @@ const parseStorage = (umlClass, umlClasses, storages, storageObjects, inheritedC
|
|
|
93
109
|
type: attribute.type,
|
|
94
110
|
variable: attribute.name,
|
|
95
111
|
contractName: umlClass.name,
|
|
96
|
-
|
|
112
|
+
structStorageId,
|
|
113
|
+
values: [],
|
|
97
114
|
});
|
|
98
115
|
}
|
|
99
116
|
});
|
|
100
|
-
return
|
|
117
|
+
return variables;
|
|
101
118
|
};
|
|
102
|
-
const
|
|
119
|
+
const parseStructStorage = (attribute, otherClasses, storages) => {
|
|
103
120
|
if (attribute.attributeType === umlClass_1.AttributeType.UserDefined) {
|
|
104
|
-
// Have we already created the
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
return
|
|
121
|
+
// Have we already created the storage?
|
|
122
|
+
const existingStorage = storages.find((dep) => dep.name === attribute.type);
|
|
123
|
+
if (existingStorage) {
|
|
124
|
+
return existingStorage;
|
|
108
125
|
}
|
|
109
126
|
// Is the user defined type linked to another Contract, Struct or Enum?
|
|
110
127
|
const dependentClass = otherClasses.find(({ name }) => {
|
|
@@ -114,15 +131,15 @@ const parseStructStorageObject = (attribute, otherClasses, storageObjects) => {
|
|
|
114
131
|
throw Error(`Failed to find user defined type "${attribute.type}"`);
|
|
115
132
|
}
|
|
116
133
|
if (dependentClass.stereotype === umlClass_1.ClassStereotype.Struct) {
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
id:
|
|
134
|
+
const variables = parseVariables(dependentClass, otherClasses, [], storages, []);
|
|
135
|
+
const newStorage = {
|
|
136
|
+
id: storageId++,
|
|
120
137
|
name: attribute.type,
|
|
121
138
|
type: StorageType.Struct,
|
|
122
|
-
|
|
139
|
+
variables,
|
|
123
140
|
};
|
|
124
|
-
|
|
125
|
-
return
|
|
141
|
+
storages.push(newStorage);
|
|
142
|
+
return newStorage;
|
|
126
143
|
}
|
|
127
144
|
return undefined;
|
|
128
145
|
}
|
|
@@ -135,10 +152,10 @@ const parseStructStorageObject = (attribute, otherClasses, storageObjects) => {
|
|
|
135
152
|
? attribute.type.match(/=\\>((?!mapping)\w*)[\\[]/)
|
|
136
153
|
: attribute.type.match(/(\w+)\[/);
|
|
137
154
|
if (result !== null && result[1] && !(0, exports.isElementary)(result[1])) {
|
|
138
|
-
// Have we already created the
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
return
|
|
155
|
+
// Have we already created the storage?
|
|
156
|
+
const existingStorage = storages.find(({ name }) => name === result[1] || name === result[1].split('.')[1]);
|
|
157
|
+
if (existingStorage) {
|
|
158
|
+
return existingStorage;
|
|
142
159
|
}
|
|
143
160
|
// Find UserDefined type
|
|
144
161
|
const typeClass = otherClasses.find(({ name }) => name === result[1] || name === result[1].split('.')[1]);
|
|
@@ -146,22 +163,22 @@ const parseStructStorageObject = (attribute, otherClasses, storageObjects) => {
|
|
|
146
163
|
throw Error(`Failed to find user defined type "${result[1]}" in attribute type "${attribute.type}"`);
|
|
147
164
|
}
|
|
148
165
|
if (typeClass.stereotype === umlClass_1.ClassStereotype.Struct) {
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
id:
|
|
166
|
+
const variables = parseVariables(typeClass, otherClasses, [], storages, []);
|
|
167
|
+
const newStorage = {
|
|
168
|
+
id: storageId++,
|
|
152
169
|
name: typeClass.name,
|
|
153
170
|
type: StorageType.Struct,
|
|
154
|
-
|
|
171
|
+
variables,
|
|
155
172
|
};
|
|
156
|
-
|
|
157
|
-
return
|
|
173
|
+
storages.push(newStorage);
|
|
174
|
+
return newStorage;
|
|
158
175
|
}
|
|
159
176
|
}
|
|
160
177
|
return undefined;
|
|
161
178
|
}
|
|
162
179
|
return undefined;
|
|
163
180
|
};
|
|
164
|
-
exports.
|
|
181
|
+
exports.parseStructStorage = parseStructStorage;
|
|
165
182
|
// Calculates the storage size of an attribute in bytes
|
|
166
183
|
const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
|
|
167
184
|
if (attribute.attributeType === umlClass_1.AttributeType.Mapping ||
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare const
|
|
3
|
-
export declare function
|
|
1
|
+
import { Storage } from './converterClasses2Storage';
|
|
2
|
+
export declare const convertStorages2Dot: (storages: Storage[]) => string;
|
|
3
|
+
export declare function convertStorage2Dot(storage: Storage, dotString: string): string;
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.convertStorage2Dot = exports.convertStorages2Dot = void 0;
|
|
4
4
|
const converterClasses2Storage_1 = require("./converterClasses2Storage");
|
|
5
5
|
const debug = require('debug')('sol2uml');
|
|
6
|
-
const
|
|
6
|
+
const convertStorages2Dot = (storages) => {
|
|
7
7
|
let dotString = `
|
|
8
8
|
digraph StorageDiagram {
|
|
9
9
|
rankdir=LR
|
|
10
10
|
color=black
|
|
11
11
|
arrowhead=open
|
|
12
12
|
node [shape=record, style=filled, fillcolor=gray95]`;
|
|
13
|
-
// process contract and the struct
|
|
14
|
-
|
|
15
|
-
dotString =
|
|
13
|
+
// process contract and the struct storages
|
|
14
|
+
storages.forEach((storage) => {
|
|
15
|
+
dotString = convertStorage2Dot(storage, dotString);
|
|
16
16
|
});
|
|
17
17
|
// link contract and structs to structs
|
|
18
|
-
|
|
19
|
-
slot.
|
|
20
|
-
if (storage.
|
|
21
|
-
dotString += `\n ${slot.id}:${storage.id} -> ${storage.
|
|
18
|
+
storages.forEach((slot) => {
|
|
19
|
+
slot.variables.forEach((storage) => {
|
|
20
|
+
if (storage.structStorageId) {
|
|
21
|
+
dotString += `\n ${slot.id}:${storage.id} -> ${storage.structStorageId}`;
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
24
|
});
|
|
@@ -27,63 +27,72 @@ node [shape=record, style=filled, fillcolor=gray95]`;
|
|
|
27
27
|
debug(dotString);
|
|
28
28
|
return dotString;
|
|
29
29
|
};
|
|
30
|
-
exports.
|
|
31
|
-
function
|
|
32
|
-
const steorotype =
|
|
33
|
-
// write
|
|
34
|
-
dotString += `\n${
|
|
30
|
+
exports.convertStorages2Dot = convertStorages2Dot;
|
|
31
|
+
function convertStorage2Dot(storage, dotString) {
|
|
32
|
+
const steorotype = storage.type === converterClasses2Storage_1.StorageType.Struct ? 'Struct' : 'Contract';
|
|
33
|
+
// write storage header with name and optional address
|
|
34
|
+
dotString += `\n${storage.id} [label="${storage.name} \\<\\<${steorotype}\\>\\>\\n${storage.address || ''} | {`;
|
|
35
|
+
const startingVariables = storage.variables.filter((s) => s.byteOffset === 0);
|
|
35
36
|
// write slot numbers
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
dotString += '{ slot';
|
|
38
|
+
startingVariables.forEach((variable, i) => {
|
|
39
|
+
if (variable.fromSlot === variable.toSlot) {
|
|
40
|
+
dotString += `| ${variable.fromSlot} `;
|
|
39
41
|
}
|
|
40
|
-
else
|
|
41
|
-
|
|
42
|
-
dotString += `| ${storage.fromSlot}`;
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
dotString += `| ${storage.fromSlot}-${storage.toSlot}`;
|
|
46
|
-
}
|
|
42
|
+
else {
|
|
43
|
+
dotString += `| ${variable.fromSlot}-${variable.toSlot} `;
|
|
47
44
|
}
|
|
48
45
|
});
|
|
49
|
-
// write
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
46
|
+
// write slot values if available
|
|
47
|
+
if (startingVariables[0]?.values[0]) {
|
|
48
|
+
dotString += '} | {value';
|
|
49
|
+
startingVariables.forEach((variable, i) => {
|
|
50
|
+
dotString += ` | ${variable.values[0]}`;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const contractVariablePrefix = storage.type === converterClasses2Storage_1.StorageType.Contract ? '\\<inherited contract\\>.' : '';
|
|
54
|
+
dotString += `} | { type: ${contractVariablePrefix}variable (bytes)`;
|
|
55
|
+
// For each slot
|
|
56
|
+
startingVariables.forEach((variable) => {
|
|
57
|
+
// Get all the storage variables in this slot
|
|
58
|
+
const slotVariables = storage.variables.filter((s) => s.fromSlot === variable.fromSlot);
|
|
59
|
+
const usedBytes = slotVariables.reduce((acc, s) => acc + s.byteSize, 0);
|
|
60
|
+
if (usedBytes < 32) {
|
|
61
|
+
slotVariables.push({
|
|
62
|
+
id: 0,
|
|
63
|
+
fromSlot: variable.fromSlot,
|
|
64
|
+
toSlot: variable.fromSlot,
|
|
65
|
+
byteSize: 32 - usedBytes,
|
|
66
|
+
byteOffset: usedBytes,
|
|
67
|
+
type: 'unallocated',
|
|
68
|
+
contractName: variable.contractName,
|
|
69
|
+
variable: '',
|
|
70
|
+
values: [],
|
|
71
|
+
});
|
|
75
72
|
}
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
const slotVariablesReversed = slotVariables.reverse();
|
|
74
|
+
// For each variable in the slot
|
|
75
|
+
slotVariablesReversed.forEach((variable, i) => {
|
|
76
|
+
if (i === 0) {
|
|
77
|
+
dotString += ` | { ${dotVariable(variable, storage.name)} `;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
dotString += ` | ${dotVariable(variable, storage.name)} `;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
dotString += '}';
|
|
78
84
|
});
|
|
79
85
|
// Need to close off the last label
|
|
80
86
|
dotString += '}}"]\n';
|
|
81
87
|
return dotString;
|
|
82
88
|
}
|
|
83
|
-
exports.
|
|
89
|
+
exports.convertStorage2Dot = convertStorage2Dot;
|
|
84
90
|
const dotVariable = (storage, contractName) => {
|
|
85
|
-
const port = storage.
|
|
91
|
+
const port = storage.structStorageId !== undefined ? `<${storage.id}>` : '';
|
|
86
92
|
const contractNamePrefix = storage.contractName !== contractName ? `${storage.contractName}.` : '';
|
|
87
|
-
|
|
93
|
+
const variable = storage.variable
|
|
94
|
+
? `: ${contractNamePrefix}${storage.variable}`
|
|
95
|
+
: '';
|
|
96
|
+
return `${port} ${storage.type}${variable} (${storage.byteSize})`;
|
|
88
97
|
};
|
|
89
98
|
//# sourceMappingURL=converterStorage2Dot.js.map
|
package/lib/slotValues.d.ts
CHANGED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { BigNumberish } from '@ethersproject/bignumber';
|
|
2
|
+
export declare const getStorageValue: (url: string, contractAddress: string, slot: BigNumberish, blockTag?: BigNumberish | 'latest') => Promise<string>;
|
|
3
|
+
export declare const getStorageValues: (url: string, contractAddress: string, slots: BigNumberish[], blockTag?: BigNumberish | 'latest') => Promise<string[]>;
|
package/lib/slotValues.js
CHANGED
|
@@ -1,21 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getStorageValues = exports.getStorageValue = void 0;
|
|
7
|
+
const bignumber_1 = require("@ethersproject/bignumber");
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
const debug = require('debug')('sol2uml');
|
|
10
|
+
const getStorageValue = async (url, contractAddress, slot, blockTag = 'latest') => {
|
|
11
|
+
debug(`About to get storage slot ${slot} value for ${contractAddress}`);
|
|
12
|
+
const values = await (0, exports.getStorageValues)(url, contractAddress, [slot], blockTag);
|
|
13
|
+
debug(`Got slot ${slot} value: ${values[0]}`);
|
|
14
|
+
return values[0];
|
|
15
|
+
};
|
|
16
|
+
exports.getStorageValue = getStorageValue;
|
|
17
|
+
let jsonRpcId = 0;
|
|
18
|
+
const getStorageValues = async (url, contractAddress, slots, blockTag = 'latest') => {
|
|
19
|
+
try {
|
|
20
|
+
debug(`About to get ${slots.length} storage values for ${contractAddress} at block ${blockTag}`);
|
|
21
|
+
const block = blockTag === 'latest'
|
|
22
|
+
? blockTag
|
|
23
|
+
: bignumber_1.BigNumber.from(blockTag).toHexString();
|
|
24
|
+
const payload = slots.map((slot) => ({
|
|
25
|
+
id: (jsonRpcId++).toString(),
|
|
26
|
+
jsonrpc: '2.0',
|
|
27
|
+
method: 'eth_getStorageAt',
|
|
28
|
+
params: [
|
|
29
|
+
contractAddress,
|
|
30
|
+
bignumber_1.BigNumber.from(slot).toHexString(),
|
|
31
|
+
block,
|
|
32
|
+
],
|
|
33
|
+
}));
|
|
34
|
+
const response = await axios_1.default.post(url, payload);
|
|
35
|
+
console.log(response.data);
|
|
36
|
+
if (response.data?.error?.message) {
|
|
37
|
+
throw new Error(response.data.error.message);
|
|
38
|
+
}
|
|
39
|
+
if (response.data.length !== slots.length) {
|
|
40
|
+
throw new Error(`Requested ${slots.length} storage slot values but only got ${response.data.length}`);
|
|
41
|
+
}
|
|
42
|
+
const responseData = response.data;
|
|
43
|
+
const sortedResponses = responseData.sort((a, b) => bignumber_1.BigNumber.from(a.id).gt(b.id) ? 1 : -1);
|
|
44
|
+
return sortedResponses.map((data) => data.result);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
throw new Error(`Failed to get ${slots.length} storage values for ${contractAddress} from ${url}`, { cause: err });
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
exports.getStorageValues = getStorageValues;
|
|
21
51
|
//# sourceMappingURL=slotValues.js.map
|
package/lib/sol2uml.js
CHANGED
|
@@ -75,14 +75,14 @@ If an Ethereum address with a 0x prefix is passed, the verified source code from
|
|
|
75
75
|
...command.parent._optionValues,
|
|
76
76
|
...options,
|
|
77
77
|
};
|
|
78
|
-
const { umlClasses } = await (0, parserGeneral_1.parserUmlClasses)(fileFolderAddress, combinedOptions);
|
|
78
|
+
const { umlClasses, contractName } = await (0, parserGeneral_1.parserUmlClasses)(fileFolderAddress, combinedOptions);
|
|
79
79
|
let filteredUmlClasses = umlClasses;
|
|
80
80
|
if (options.baseContractNames) {
|
|
81
81
|
const baseContractNames = options.baseContractNames.split(',');
|
|
82
82
|
filteredUmlClasses = (0, filterClasses_1.classesConnectedToBaseContracts)(umlClasses, baseContractNames, options.depth);
|
|
83
83
|
}
|
|
84
84
|
const dotString = (0, converterClasses2Dot_1.convertUmlClasses2Dot)(filteredUmlClasses, combinedOptions.clusterFolders, combinedOptions);
|
|
85
|
-
await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, combinedOptions.outputFormat, combinedOptions.outputFileName);
|
|
85
|
+
await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, contractName, combinedOptions.outputFormat, combinedOptions.outputFileName);
|
|
86
86
|
debug(`Finished generating UML`);
|
|
87
87
|
}
|
|
88
88
|
catch (err) {
|
|
@@ -93,8 +93,13 @@ program
|
|
|
93
93
|
.command('storage')
|
|
94
94
|
.description("Visually display a contract's storage slots.\n\nWARNING: sol2uml does not use the Solidity compiler so may differ with solc. A known example is fixed-sized arrays declared with an expression will fail to be sized.")
|
|
95
95
|
.argument('<fileFolderAddress>', 'file name, base folder or contract address')
|
|
96
|
-
.option('-c, --
|
|
97
|
-
|
|
96
|
+
.option('-c, --contract <name>', 'Contract name in local Solidity files. Not needed when using an address as the first argument as the contract name can be derived from Etherscan.')
|
|
97
|
+
.option('-d, --data', 'Gets the values in the storage slots from an Ethereum node.', false)
|
|
98
|
+
.option('-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.')
|
|
99
|
+
.addOption(new commander_1.Option('-u, --url <url>', 'URL of the Ethereum node to get storage values if the `data` option is used.')
|
|
100
|
+
.env('NODE_URL')
|
|
101
|
+
.default('http://localhost:8545'))
|
|
102
|
+
.option('-bn, --block <number>', 'Block number to get the contract storage values from.', 'latest')
|
|
98
103
|
.action(async (fileFolderAddress, options, command) => {
|
|
99
104
|
try {
|
|
100
105
|
const combinedOptions = {
|
|
@@ -102,14 +107,32 @@ program
|
|
|
102
107
|
...options,
|
|
103
108
|
};
|
|
104
109
|
let { umlClasses, contractName } = await (0, parserGeneral_1.parserUmlClasses)(fileFolderAddress, combinedOptions);
|
|
105
|
-
contractName = combinedOptions.
|
|
106
|
-
const
|
|
110
|
+
contractName = combinedOptions.contract || contractName;
|
|
111
|
+
const storages = (0, converterClasses2Storage_1.convertClasses2Storages)(contractName, umlClasses);
|
|
107
112
|
if ((0, regEx_1.isAddress)(fileFolderAddress)) {
|
|
108
|
-
// The first
|
|
109
|
-
|
|
113
|
+
// The first storage is the contract
|
|
114
|
+
storages[0].address = fileFolderAddress;
|
|
115
|
+
}
|
|
116
|
+
debug(storages);
|
|
117
|
+
if (combinedOptions.data) {
|
|
118
|
+
let storageAddress = combinedOptions.storage;
|
|
119
|
+
if (storageAddress) {
|
|
120
|
+
if (!(0, regEx_1.isAddress)(storageAddress)) {
|
|
121
|
+
throw Error(`Invalid address to get storage data from "${storageAddress}"`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
if (!(0, regEx_1.isAddress)(fileFolderAddress)) {
|
|
126
|
+
throw Error(`Can not get storage slot values if first param is not an address and the \`address\` option is not used.`);
|
|
127
|
+
}
|
|
128
|
+
storageAddress = fileFolderAddress;
|
|
129
|
+
}
|
|
130
|
+
const storage = storages.find((so) => so.name === contractName);
|
|
131
|
+
if (!storageAddress)
|
|
132
|
+
throw Error(`Could not find the "${contractName}" contract in list of parsed storages`);
|
|
133
|
+
await (0, converterClasses2Storage_1.addStorageValues)(combinedOptions.url, storageAddress, storage, combinedOptions.blockNumber);
|
|
110
134
|
}
|
|
111
|
-
|
|
112
|
-
const dotString = (0, converterStorage2Dot_1.convertStorage2Dot)(storageObjects);
|
|
135
|
+
const dotString = (0, converterStorage2Dot_1.convertStorages2Dot)(storages);
|
|
113
136
|
await (0, writerFiles_1.writeOutputFiles)(dotString, fileFolderAddress, contractName, combinedOptions.outputFormat, combinedOptions.outputFileName);
|
|
114
137
|
}
|
|
115
138
|
catch (err) {
|
package/lib/writerFiles.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export declare type OutputFormats = 'svg' | 'png' | 'dot' | 'all';
|
|
|
2
2
|
export declare const writeOutputFiles: (dot: string, fileFolderAddress: string, contractName: string, outputFormat?: OutputFormats, outputFilename?: string) => Promise<void>;
|
|
3
3
|
export declare function convertDot2Svg(dot: string): any;
|
|
4
4
|
export declare function writeSolidity(code: string, filename?: string): void;
|
|
5
|
-
export declare function writeDot(dot: string, filename
|
|
5
|
+
export declare function writeDot(dot: string, filename: string): void;
|
|
6
6
|
export declare function writeSVG(svg: any, svgFilename?: string, outputFormats?: OutputFormats): Promise<void>;
|
|
7
7
|
export declare function writePng(svg: any, filename: string): Promise<void>;
|
package/lib/writerFiles.js
CHANGED
|
@@ -10,13 +10,6 @@ const sync_1 = __importDefault(require("@aduh95/viz.js/sync"));
|
|
|
10
10
|
const { convert } = require('convert-svg-to-png');
|
|
11
11
|
const debug = require('debug')('sol2uml');
|
|
12
12
|
const writeOutputFiles = async (dot, fileFolderAddress, contractName, outputFormat = 'svg', outputFilename) => {
|
|
13
|
-
if (outputFormat === 'dot' || outputFormat === 'all') {
|
|
14
|
-
writeDot(dot, outputFilename);
|
|
15
|
-
// No need to continue if only generating a dot file
|
|
16
|
-
if (outputFormat === 'dot') {
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
13
|
// If all output then extension is svg
|
|
21
14
|
const outputExt = outputFormat === 'all' ? 'svg' : outputFormat;
|
|
22
15
|
if (!outputFilename) {
|
|
@@ -36,6 +29,13 @@ const writeOutputFiles = async (dot, fileFolderAddress, contractName, outputForm
|
|
|
36
29
|
}
|
|
37
30
|
catch (err) { } // we can ignore errors as it just means outputFilename does not exist yet
|
|
38
31
|
}
|
|
32
|
+
if (outputFormat === 'dot' || outputFormat === 'all') {
|
|
33
|
+
writeDot(dot, outputFilename);
|
|
34
|
+
// No need to continue if only generating a dot file
|
|
35
|
+
if (outputFormat === 'dot') {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
39
|
const svg = convertDot2Svg(dot);
|
|
40
40
|
if (outputFormat === 'svg' || outputFormat === 'all') {
|
|
41
41
|
await writeSVG(svg, outputFilename, outputFormat);
|
|
@@ -73,18 +73,16 @@ function writeSolidity(code, filename = 'solidity') {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
exports.writeSolidity = writeSolidity;
|
|
76
|
-
function writeDot(dot, filename
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
debug(`About to write Dot file to ${outputFile}`);
|
|
80
|
-
(0, fs_1.writeFile)(outputFile, dot, (err) => {
|
|
76
|
+
function writeDot(dot, filename) {
|
|
77
|
+
debug(`About to write Dot file to ${filename}`);
|
|
78
|
+
(0, fs_1.writeFile)(filename, dot, (err) => {
|
|
81
79
|
if (err) {
|
|
82
|
-
throw new Error(`Failed to write Dot file to ${
|
|
80
|
+
throw new Error(`Failed to write Dot file to ${filename}`, {
|
|
83
81
|
cause: err,
|
|
84
82
|
});
|
|
85
83
|
}
|
|
86
84
|
else {
|
|
87
|
-
console.log(`Dot file written to ${
|
|
85
|
+
console.log(`Dot file written to ${filename}`);
|
|
88
86
|
}
|
|
89
87
|
});
|
|
90
88
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sol2uml",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Unified Modeling Language (UML) class diagram generator for Solidity contracts",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"commander": "^9.4.0",
|
|
26
26
|
"convert-svg-to-png": "^0.6.4",
|
|
27
27
|
"debug": "^4.3.4",
|
|
28
|
+
"ethers": "^5.6.9",
|
|
28
29
|
"js-graph-algorithms": "^1.0.18",
|
|
29
30
|
"klaw": "^4.0.1"
|
|
30
31
|
},
|