sol2uml 2.4.2 → 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/LICENSE +1 -1
- package/README.md +24 -4
- package/lib/SlotValueCache.d.ts +31 -0
- package/lib/SlotValueCache.js +65 -0
- package/lib/associations.d.ts +1 -1
- package/lib/associations.js +1 -21
- package/lib/converterAST2Classes.d.ts +12 -1
- package/lib/converterAST2Classes.js +33 -7
- package/lib/converterClass2Dot.d.ts +4 -0
- package/lib/converterClasses2Dot.js +3 -2
- package/lib/converterClasses2Storage.d.ts +53 -21
- package/lib/converterClasses2Storage.js +451 -180
- package/lib/converterStorage2Dot.d.ts +7 -3
- package/lib/converterStorage2Dot.js +53 -28
- package/lib/filterClasses.d.ts +4 -4
- package/lib/filterClasses.js +2 -1
- package/lib/parserEtherscan.d.ts +20 -2
- package/lib/parserEtherscan.js +38 -5
- package/lib/parserFiles.d.ts +3 -3
- package/lib/parserFiles.js +2 -2
- package/lib/slotValues.d.ts +48 -2
- package/lib/slotValues.js +260 -23
- package/lib/sol2uml.js +30 -15
- package/lib/squashClasses.d.ts +3 -3
- package/lib/squashClasses.js +2 -2
- package/lib/writerFiles.d.ts +21 -1
- package/lib/writerFiles.js +21 -2
- package/package.json +5 -5
package/lib/slotValues.js
CHANGED
|
@@ -3,49 +3,286 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.escapeString = exports.convert2String = exports.dynamicSlotSize = exports.getSlotValue = exports.getSlotValues = exports.parseValue = exports.addSlotValues = void 0;
|
|
7
7
|
const bignumber_1 = require("@ethersproject/bignumber");
|
|
8
8
|
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
const umlClass_1 = require("./umlClass");
|
|
10
|
+
const utils_1 = require("ethers/lib/utils");
|
|
11
|
+
const SlotValueCache_1 = require("./SlotValueCache");
|
|
9
12
|
const debug = require('debug')('sol2uml');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Adds the slot values to the variables in the storage section.
|
|
15
|
+
* This can be rerun for a section as it will only get if the slot value
|
|
16
|
+
* does not exist.
|
|
17
|
+
* @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy
|
|
18
|
+
* @param contractAddress Contract address to get the storage slot values from.
|
|
19
|
+
* If contract is proxied, use proxy and not the implementation contract.
|
|
20
|
+
* @param storageSection is mutated with the slot values added to the variables
|
|
21
|
+
* @param arrayItems the number of items to display at the start and end of an array
|
|
22
|
+
* @param blockTag block number or `latest`
|
|
23
|
+
*/
|
|
24
|
+
const addSlotValues = async (url, contractAddress, storageSection, arrayItems, blockTag) => {
|
|
25
|
+
const valueVariables = storageSection.variables.filter((variable) => variable.getValue && !variable.slotValue);
|
|
26
|
+
if (valueVariables.length === 0)
|
|
27
|
+
return;
|
|
28
|
+
// for each variable, add all the slots used by the variable.
|
|
29
|
+
const slots = [];
|
|
30
|
+
valueVariables.forEach((variable) => {
|
|
31
|
+
for (let i = 0; variable.fromSlot + i <= variable.toSlot; i++) {
|
|
32
|
+
if (variable.attributeType === umlClass_1.AttributeType.Array &&
|
|
33
|
+
i >= arrayItems &&
|
|
34
|
+
i < variable.toSlot - arrayItems) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
slots.push(variable.fromSlot + i);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
// remove duplicate slot numbers
|
|
41
|
+
const uniqueFromSlots = [...new Set(slots)];
|
|
42
|
+
// Convert slot numbers to BigNumbers and offset dynamic arrays
|
|
43
|
+
let slotKeys = uniqueFromSlots.map((fromSlot) => {
|
|
44
|
+
if (storageSection.offset) {
|
|
45
|
+
return bignumber_1.BigNumber.from(storageSection.offset).add(fromSlot);
|
|
46
|
+
}
|
|
47
|
+
return bignumber_1.BigNumber.from(fromSlot);
|
|
48
|
+
});
|
|
49
|
+
// Get the contract slot values from the node provider
|
|
50
|
+
const values = await (0, exports.getSlotValues)(url, contractAddress, slotKeys, blockTag);
|
|
51
|
+
// For each slot value retrieved
|
|
52
|
+
values.forEach((value, i) => {
|
|
53
|
+
// Get the corresponding slot number for the slot value
|
|
54
|
+
const fromSlot = uniqueFromSlots[i];
|
|
55
|
+
// For each variable in the storage section
|
|
56
|
+
for (const variable of storageSection.variables) {
|
|
57
|
+
if (variable.getValue && variable.fromSlot === fromSlot) {
|
|
58
|
+
debug(`Set slot value ${value} for section "${storageSection.name}", var type ${variable.type}, slot ${variable.fromSlot} offset ${storageSection.offset}`);
|
|
59
|
+
variable.slotValue = value;
|
|
60
|
+
// parse variable value from slot data
|
|
61
|
+
if (variable.displayValue) {
|
|
62
|
+
variable.parsedValue = (0, exports.parseValue)(variable);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// if variable is past the slot that has the value
|
|
66
|
+
else if (variable.toSlot > fromSlot) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
exports.addSlotValues = addSlotValues;
|
|
73
|
+
const parseValue = (variable) => {
|
|
74
|
+
if (!variable.slotValue)
|
|
75
|
+
return undefined;
|
|
76
|
+
const start = 66 - (variable.byteOffset + variable.byteSize) * 2;
|
|
77
|
+
const end = 66 - variable.byteOffset * 2;
|
|
78
|
+
const variableValue = variable.slotValue.substring(start, end);
|
|
79
|
+
try {
|
|
80
|
+
// Contracts, structs and enums
|
|
81
|
+
if (variable.attributeType === umlClass_1.AttributeType.UserDefined) {
|
|
82
|
+
return parseUserDefinedValue(variable, variableValue);
|
|
83
|
+
}
|
|
84
|
+
if (variable.attributeType === umlClass_1.AttributeType.Elementary)
|
|
85
|
+
return parseElementaryValue(variable, variableValue);
|
|
86
|
+
// dynamic arrays
|
|
87
|
+
if (variable.attributeType === umlClass_1.AttributeType.Array &&
|
|
88
|
+
variable.dynamic) {
|
|
89
|
+
return (0, utils_1.formatUnits)('0x' + variableValue, 0);
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
throw Error(`Failed to parse variable ${variable.name} of type ${variable.type}, value "${variableValue}"`, { cause: err });
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
exports.parseValue = parseValue;
|
|
98
|
+
const parseUserDefinedValue = (variable, variableValue) => {
|
|
99
|
+
// TODO need to handle User Defined Value Types introduced in Solidity
|
|
100
|
+
// https://docs.soliditylang.org/en/v0.8.18/types.html#user-defined-value-types
|
|
101
|
+
// https://blog.soliditylang.org/2021/09/27/user-defined-value-types/
|
|
102
|
+
// using byteSize is crude and will be incorrect for aliases types like int160 or uint160
|
|
103
|
+
if (variable.byteSize === 20) {
|
|
104
|
+
return (0, utils_1.getAddress)('0x' + variableValue);
|
|
105
|
+
}
|
|
106
|
+
// this will also be wrong if the alias is to a 1 byte type. eg bytes1, int8 or uint8
|
|
107
|
+
if (variable.byteSize === 1) {
|
|
108
|
+
// assume 1 byte is an enum so convert value to enum index number
|
|
109
|
+
const index = bignumber_1.BigNumber.from('0x' + variableValue).toNumber();
|
|
110
|
+
// lookup enum value if its available
|
|
111
|
+
return variable?.enumValues ? variable?.enumValues[index] : undefined;
|
|
112
|
+
}
|
|
113
|
+
// we don't parse if a struct which has a size of 32 bytes
|
|
114
|
+
return undefined;
|
|
115
|
+
};
|
|
116
|
+
const parseElementaryValue = (variable, variableValue) => {
|
|
117
|
+
// Elementary types
|
|
118
|
+
if (variable.type === 'bool') {
|
|
119
|
+
if (variableValue === '00')
|
|
120
|
+
return 'false';
|
|
121
|
+
if (variableValue === '01')
|
|
122
|
+
return 'true';
|
|
123
|
+
throw Error(`Failed to parse bool variable "${variable.name}" in slot ${variable.fromSlot}, offset ${variable.byteOffset} and slot value "${variableValue}"`);
|
|
124
|
+
}
|
|
125
|
+
if (variable.type === 'string' || variable.type === 'bytes') {
|
|
126
|
+
if (variable.dynamic) {
|
|
127
|
+
const lastByte = variable.slotValue.slice(-2);
|
|
128
|
+
const size = bignumber_1.BigNumber.from('0x' + lastByte);
|
|
129
|
+
// Check if the last bit is set by AND the size with 0x01
|
|
130
|
+
if (size.and(1).eq(1)) {
|
|
131
|
+
// Return the number of chars or bytes
|
|
132
|
+
return bignumber_1.BigNumber.from(variable.slotValue)
|
|
133
|
+
.sub(1)
|
|
134
|
+
.div(2)
|
|
135
|
+
.toString();
|
|
136
|
+
}
|
|
137
|
+
// The last byte holds the length of the string or bytes in the slot
|
|
138
|
+
const valueHex = '0x' + variableValue.slice(0, size.toNumber());
|
|
139
|
+
if (variable.type === 'bytes')
|
|
140
|
+
return valueHex;
|
|
141
|
+
return `\\"${(0, exports.convert2String)(valueHex)}\\"`;
|
|
142
|
+
}
|
|
143
|
+
if (variable.type === 'bytes')
|
|
144
|
+
return '0x' + variableValue;
|
|
145
|
+
return `\\"${(0, exports.convert2String)('0x' + variableValue)}\\"`;
|
|
146
|
+
}
|
|
147
|
+
if (variable.type === 'address') {
|
|
148
|
+
return (0, utils_1.getAddress)('0x' + variableValue);
|
|
149
|
+
}
|
|
150
|
+
if (variable.type.match(/^uint([0-9]*)$/)) {
|
|
151
|
+
const parsedValue = (0, utils_1.formatUnits)('0x' + variableValue, 0);
|
|
152
|
+
return (0, utils_1.commify)(parsedValue);
|
|
153
|
+
}
|
|
154
|
+
if (variable.type.match(/^bytes([0-9]+)$/)) {
|
|
155
|
+
return '0x' + variableValue;
|
|
156
|
+
}
|
|
157
|
+
if (variable.type.match(/^int([0-9]*)/)) {
|
|
158
|
+
// parse variable value as an unsigned number
|
|
159
|
+
let rawValue = bignumber_1.BigNumber.from('0x' + variableValue);
|
|
160
|
+
// parse the number of bits
|
|
161
|
+
const result = variable.type.match(/^int([0-9]*$)/);
|
|
162
|
+
const bitSize = result[1] ? result[1] : 256;
|
|
163
|
+
// Convert the number of bits to the number of hex characters
|
|
164
|
+
const hexSize = bignumber_1.BigNumber.from(bitSize).div(4).toNumber();
|
|
165
|
+
// bit mask has a leading 1 and the rest 0. 0x8 = 1000 binary
|
|
166
|
+
const mask = '0x80' + '0'.repeat(hexSize - 2);
|
|
167
|
+
// is the first bit a 1?
|
|
168
|
+
const negative = rawValue.and(mask);
|
|
169
|
+
if (negative.gt(0)) {
|
|
170
|
+
// Convert unsigned number to a signed negative
|
|
171
|
+
const negativeOne = '0xFF' + 'F'.repeat(hexSize - 2);
|
|
172
|
+
rawValue = bignumber_1.BigNumber.from(negativeOne).sub(rawValue).add(1).mul(-1);
|
|
173
|
+
}
|
|
174
|
+
const parsedValue = (0, utils_1.formatUnits)(rawValue, 0);
|
|
175
|
+
return (0, utils_1.commify)(parsedValue);
|
|
176
|
+
}
|
|
177
|
+
// add fixed point numbers when they are supported by Solidity
|
|
178
|
+
return undefined;
|
|
15
179
|
};
|
|
16
|
-
exports.getStorageValue = getStorageValue;
|
|
17
180
|
let jsonRpcId = 0;
|
|
18
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Get storage slot values from JSON-RPC API provider.
|
|
183
|
+
* @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy
|
|
184
|
+
* @param contractAddress Contract address to get the storage slot values from.
|
|
185
|
+
* If proxied, use proxy and not the implementation contract.
|
|
186
|
+
* @param slotKeys array of 32 byte slot keys as BigNumbers.
|
|
187
|
+
* @param blockTag block number or `latest`
|
|
188
|
+
* @return slotValues array of 32 byte slot values as hexadecimal strings
|
|
189
|
+
*/
|
|
190
|
+
const getSlotValues = async (url, contractAddress, slotKeys, blockTag = 'latest') => {
|
|
19
191
|
try {
|
|
20
|
-
|
|
192
|
+
if (slotKeys.length === 0) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
21
195
|
const block = blockTag === 'latest'
|
|
22
196
|
? blockTag
|
|
23
|
-
: bignumber_1.BigNumber.from(blockTag)
|
|
24
|
-
|
|
197
|
+
: (0, utils_1.hexValue)(bignumber_1.BigNumber.from(blockTag));
|
|
198
|
+
// get cached values and missing slot keys from from cache
|
|
199
|
+
const { cachedValues, missingKeys } = SlotValueCache_1.SlotValueCache.readSlotValues(slotKeys);
|
|
200
|
+
// If all values are in the cache then just return the cached values
|
|
201
|
+
if (missingKeys.length === 0) {
|
|
202
|
+
return cachedValues;
|
|
203
|
+
}
|
|
204
|
+
debug(`About to get ${slotKeys.length} storage values for ${contractAddress} at block ${blockTag} from slot ${missingKeys[0].toString()}`);
|
|
205
|
+
// Get the values for the missing slot keys
|
|
206
|
+
const payload = missingKeys.map((key) => ({
|
|
25
207
|
id: (jsonRpcId++).toString(),
|
|
26
208
|
jsonrpc: '2.0',
|
|
27
209
|
method: 'eth_getStorageAt',
|
|
28
|
-
params: [
|
|
29
|
-
contractAddress,
|
|
30
|
-
bignumber_1.BigNumber.from(slot).toHexString(),
|
|
31
|
-
block,
|
|
32
|
-
],
|
|
210
|
+
params: [contractAddress, key, block],
|
|
33
211
|
}));
|
|
34
212
|
const response = await axios_1.default.post(url, payload);
|
|
35
|
-
console.log(response.data);
|
|
36
213
|
if (response.data?.error?.message) {
|
|
37
|
-
throw
|
|
214
|
+
throw Error(response.data.error.message);
|
|
38
215
|
}
|
|
39
|
-
if (response.data.length !==
|
|
40
|
-
throw
|
|
216
|
+
if (response.data.length !== missingKeys.length) {
|
|
217
|
+
throw Error(`Requested ${missingKeys.length} storage slot values but only got ${response.data.length}`);
|
|
41
218
|
}
|
|
42
219
|
const responseData = response.data;
|
|
43
220
|
const sortedResponses = responseData.sort((a, b) => bignumber_1.BigNumber.from(a.id).gt(b.id) ? 1 : -1);
|
|
44
|
-
|
|
221
|
+
const missingValues = sortedResponses.map((data) => {
|
|
222
|
+
if (data.error) {
|
|
223
|
+
throw Error(`json rpc call with id ${data.id} failed to get storage values: ${data.error?.message}`);
|
|
224
|
+
}
|
|
225
|
+
return '0x' + data.result.toUpperCase().slice(2);
|
|
226
|
+
});
|
|
227
|
+
// add new values to the cache and return the merged slot values
|
|
228
|
+
return SlotValueCache_1.SlotValueCache.addSlotValues(slotKeys, missingKeys, missingValues);
|
|
45
229
|
}
|
|
46
230
|
catch (err) {
|
|
47
|
-
throw
|
|
231
|
+
throw Error(`Failed to get ${slotKeys.length} storage values for contract ${contractAddress} from ${url}`, { cause: err });
|
|
48
232
|
}
|
|
49
233
|
};
|
|
50
|
-
exports.
|
|
234
|
+
exports.getSlotValues = getSlotValues;
|
|
235
|
+
/**
|
|
236
|
+
* Get storage slot values from JSON-RPC API provider.
|
|
237
|
+
* @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy
|
|
238
|
+
* @param contractAddress Contract address to get the storage slot values from.
|
|
239
|
+
* If proxied, use proxy and not the implementation contract.
|
|
240
|
+
* @param slotKey 32 byte slot key as a BigNumber.
|
|
241
|
+
* @param blockTag block number or `latest`
|
|
242
|
+
* @return slotValue 32 byte slot value as hexadecimal string
|
|
243
|
+
*/
|
|
244
|
+
const getSlotValue = async (url, contractAddress, slotKey, blockTag) => {
|
|
245
|
+
debug(`About to get storage slot ${slotKey} value for ${contractAddress}`);
|
|
246
|
+
const values = await (0, exports.getSlotValues)(url, contractAddress, [slotKey], blockTag);
|
|
247
|
+
return values[0];
|
|
248
|
+
};
|
|
249
|
+
exports.getSlotValue = getSlotValue;
|
|
250
|
+
/**
|
|
251
|
+
* Calculates the number of string characters or bytes of a string or bytes type.
|
|
252
|
+
* See the following for how string and bytes are stored in storage slots
|
|
253
|
+
* https://docs.soliditylang.org/en/v0.8.17/internals/layout_in_storage.html#bytes-and-string
|
|
254
|
+
* @param variable the variable with the slotValue that is being sized
|
|
255
|
+
* @return bytes the number of bytes of the dynamic slot. If static, zero is return.
|
|
256
|
+
*/
|
|
257
|
+
const dynamicSlotSize = (variable) => {
|
|
258
|
+
try {
|
|
259
|
+
if (!variable?.slotValue)
|
|
260
|
+
throw Error(`Missing slot value.`);
|
|
261
|
+
const last4bits = '0x' + variable.slotValue.slice(-1);
|
|
262
|
+
const last4bitsNum = bignumber_1.BigNumber.from(last4bits).toNumber();
|
|
263
|
+
// If the last 4 bits is an even number then it's not a dynamic slot
|
|
264
|
+
if (last4bitsNum % 2 === 0)
|
|
265
|
+
return 0;
|
|
266
|
+
const sizeRaw = bignumber_1.BigNumber.from(variable.slotValue).toNumber();
|
|
267
|
+
// Adjust the size to bytes
|
|
268
|
+
return (sizeRaw - 1) / 2;
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
throw Error(`Failed to calculate dynamic slot size for variable "${variable?.name}" of type "${variable?.type}" with slot value ${variable?.slotValue}`, { cause: err });
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
exports.dynamicSlotSize = dynamicSlotSize;
|
|
275
|
+
const convert2String = (bytes) => {
|
|
276
|
+
if (bytes ===
|
|
277
|
+
'0x0000000000000000000000000000000000000000000000000000000000000000') {
|
|
278
|
+
return '';
|
|
279
|
+
}
|
|
280
|
+
const rawString = (0, utils_1.toUtf8String)(bytes);
|
|
281
|
+
return (0, exports.escapeString)(rawString);
|
|
282
|
+
};
|
|
283
|
+
exports.convert2String = convert2String;
|
|
284
|
+
const escapeString = (text) => {
|
|
285
|
+
return text.replace(/(?=[<>&"])/g, '\\');
|
|
286
|
+
};
|
|
287
|
+
exports.escapeString = escapeString;
|
|
51
288
|
//# sourceMappingURL=slotValues.js.map
|
package/lib/sol2uml.js
CHANGED
|
@@ -13,12 +13,10 @@ const writerFiles_1 = require("./writerFiles");
|
|
|
13
13
|
const path_1 = require("path");
|
|
14
14
|
const squashClasses_1 = require("./squashClasses");
|
|
15
15
|
const diff_1 = require("./diff");
|
|
16
|
+
const slotValues_1 = require("./slotValues");
|
|
17
|
+
const ethers_1 = require("ethers");
|
|
16
18
|
const clc = require('cli-color');
|
|
17
19
|
const program = new commander_1.Command();
|
|
18
|
-
const version = (0, path_1.basename)(__dirname) === 'lib'
|
|
19
|
-
? require('../package.json').version // used when run from compile js in /lib
|
|
20
|
-
: require('../../package.json').version; // used when run from TypeScript source files under src/ts via ts-node
|
|
21
|
-
program.version(version);
|
|
22
20
|
const debugControl = require('debug');
|
|
23
21
|
const debug = require('debug')('sol2uml');
|
|
24
22
|
program
|
|
@@ -42,7 +40,15 @@ The Solidity code can be pulled from verified source code on Blockchain explorer
|
|
|
42
40
|
.default('mainnet')
|
|
43
41
|
.env('ETH_NETWORK'))
|
|
44
42
|
.addOption(new commander_1.Option('-k, --apiKey <key>', 'Blockchain explorer API key. eg Etherscan, Arbiscan, Optimism, BscScan, CronoScan, FTMScan, PolygonScan or SnowTrace API key').env('SCAN_API_KEY'))
|
|
43
|
+
.option('-bc, --backColor <color>', 'Canvas background color. "none" will use a transparent canvas.', 'white')
|
|
44
|
+
.option('-sc, --shapeColor <color>', 'Basic drawing color for graphics, not text', 'black')
|
|
45
|
+
.option('-fc, --fillColor <color>', 'Color used to fill the background of a node', 'gray95')
|
|
46
|
+
.option('-tc, --textColor <color>', 'Color used for text', 'black')
|
|
45
47
|
.option('-v, --verbose', 'run with debugging statements', false);
|
|
48
|
+
const version = (0, path_1.basename)(__dirname) === 'lib'
|
|
49
|
+
? require('../package.json').version // used when run from compile js in /lib
|
|
50
|
+
: require('../../package.json').version; // used when run from TypeScript source files under src/ts via ts-node
|
|
51
|
+
program.version(version);
|
|
46
52
|
program
|
|
47
53
|
.command('class', { isDefault: true })
|
|
48
54
|
.description('Generates a UML class diagram from Solidity source code.')
|
|
@@ -103,7 +109,7 @@ If an Ethereum address with a 0x prefix is passed, the verified source code from
|
|
|
103
109
|
// Convert UML classes to Graphviz dot format.
|
|
104
110
|
const dotString = (0, converterClasses2Dot_1.convertUmlClasses2Dot)(filteredUmlClasses, combinedOptions.clusterFolders, combinedOptions);
|
|
105
111
|
// Convert Graphviz dot format to file formats. eg svg or png
|
|
106
|
-
await (0, writerFiles_1.writeOutputFiles)(dotString,
|
|
112
|
+
await (0, writerFiles_1.writeOutputFiles)(dotString, contractName || 'classDiagram', combinedOptions.outputFormat, combinedOptions.outputFileName);
|
|
107
113
|
debug(`Finished generating UML`);
|
|
108
114
|
}
|
|
109
115
|
catch (err) {
|
|
@@ -126,6 +132,7 @@ WARNING: sol2uml does not use the Solidity compiler so may differ with solc. A k
|
|
|
126
132
|
.env('NODE_URL')
|
|
127
133
|
.default('http://localhost:8545'))
|
|
128
134
|
.option('-bn, --block <number>', 'Block number to get the contract storage values from.', 'latest')
|
|
135
|
+
.option('-a, --array <number>', 'Number of slots to display at the start and end of arrays.', '2')
|
|
129
136
|
.action(async (fileFolderAddress, options, command) => {
|
|
130
137
|
try {
|
|
131
138
|
const combinedOptions = {
|
|
@@ -138,12 +145,12 @@ WARNING: sol2uml does not use the Solidity compiler so may differ with solc. A k
|
|
|
138
145
|
}
|
|
139
146
|
let { umlClasses, contractName } = await (0, parserGeneral_1.parserUmlClasses)(fileFolderAddress, combinedOptions);
|
|
140
147
|
contractName = combinedOptions.contract || contractName;
|
|
141
|
-
const
|
|
148
|
+
const arrayItems = parseInt(combinedOptions.array);
|
|
149
|
+
const storageSections = (0, converterClasses2Storage_1.convertClasses2StorageSections)(contractName, umlClasses, arrayItems, combinedOptions.contractFile);
|
|
142
150
|
if ((0, regEx_1.isAddress)(fileFolderAddress)) {
|
|
143
151
|
// The first storage is the contract
|
|
144
|
-
|
|
152
|
+
storageSections[0].address = fileFolderAddress;
|
|
145
153
|
}
|
|
146
|
-
debug(storages);
|
|
147
154
|
if (combinedOptions.data) {
|
|
148
155
|
let storageAddress = combinedOptions.storage;
|
|
149
156
|
if (storageAddress) {
|
|
@@ -157,16 +164,24 @@ WARNING: sol2uml does not use the Solidity compiler so may differ with solc. A k
|
|
|
157
164
|
}
|
|
158
165
|
storageAddress = fileFolderAddress;
|
|
159
166
|
}
|
|
160
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
let block = combinedOptions.block;
|
|
168
|
+
if (block === 'latest') {
|
|
169
|
+
const provider = new ethers_1.ethers.providers.JsonRpcProvider(combinedOptions.url);
|
|
170
|
+
block = await provider.getBlockNumber();
|
|
171
|
+
debug(`Latest block is ${block}. All storage slot values will be from this block.`);
|
|
172
|
+
}
|
|
173
|
+
// Get slot values for each storage section
|
|
174
|
+
for (const storageSection of storageSections) {
|
|
175
|
+
await (0, slotValues_1.addSlotValues)(combinedOptions.url, storageAddress, storageSection, arrayItems, block);
|
|
176
|
+
// Add storage variables for dynamic arrays, strings and bytes
|
|
177
|
+
await (0, converterClasses2Storage_1.addDynamicVariables)(storageSection, storageSections, combinedOptions.url, storageAddress, arrayItems, block);
|
|
178
|
+
}
|
|
164
179
|
}
|
|
165
|
-
const dotString = (0, converterStorage2Dot_1.convertStorages2Dot)(
|
|
166
|
-
await (0, writerFiles_1.writeOutputFiles)(dotString,
|
|
180
|
+
const dotString = (0, converterStorage2Dot_1.convertStorages2Dot)(storageSections, combinedOptions);
|
|
181
|
+
await (0, writerFiles_1.writeOutputFiles)(dotString, contractName || 'storageDiagram', combinedOptions.outputFormat, combinedOptions.outputFileName);
|
|
167
182
|
}
|
|
168
183
|
catch (err) {
|
|
169
|
-
console.error(err
|
|
184
|
+
console.error(err);
|
|
170
185
|
process.exit(2);
|
|
171
186
|
}
|
|
172
187
|
});
|
package/lib/squashClasses.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { UmlClass } from './umlClass';
|
|
2
2
|
/**
|
|
3
3
|
* Flattens the inheritance hierarchy for each base contract.
|
|
4
|
-
* @param umlClasses array of UML classes of type `UMLClass
|
|
4
|
+
* @param umlClasses array of UML classes of type `UMLClass`. The new squashed class is added to this array.
|
|
5
5
|
* @param baseContractNames array of contract names to be rendered in squashed format.
|
|
6
|
-
* @return squashUmlClasses array of UML classes of type `UMLClass`
|
|
6
|
+
* @return squashUmlClasses array of UML classes of type `UMLClass` that are to be rendered
|
|
7
7
|
*/
|
|
8
|
-
export declare const squashUmlClasses: (umlClasses: UmlClass[], baseContractNames: string[]) => UmlClass[];
|
|
8
|
+
export declare const squashUmlClasses: (umlClasses: UmlClass[], baseContractNames: readonly string[]) => UmlClass[];
|
package/lib/squashClasses.js
CHANGED
|
@@ -29,9 +29,9 @@ const crypto = __importStar(require("crypto"));
|
|
|
29
29
|
const debug = require('debug')('sol2uml');
|
|
30
30
|
/**
|
|
31
31
|
* Flattens the inheritance hierarchy for each base contract.
|
|
32
|
-
* @param umlClasses array of UML classes of type `UMLClass
|
|
32
|
+
* @param umlClasses array of UML classes of type `UMLClass`. The new squashed class is added to this array.
|
|
33
33
|
* @param baseContractNames array of contract names to be rendered in squashed format.
|
|
34
|
-
* @return squashUmlClasses array of UML classes of type `UMLClass`
|
|
34
|
+
* @return squashUmlClasses array of UML classes of type `UMLClass` that are to be rendered
|
|
35
35
|
*/
|
|
36
36
|
const squashUmlClasses = (umlClasses, baseContractNames) => {
|
|
37
37
|
let removedClassIds = [];
|
package/lib/writerFiles.d.ts
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
export type OutputFormats = 'svg' | 'png' | 'dot' | 'all';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Writes output files to the file system based on the provided input and options.
|
|
4
|
+
* @param dot The input string in DOT format.
|
|
5
|
+
* @param contractName The name of the contract.
|
|
6
|
+
* @param outputFormat The format of the output file. choices: svg, png, dot or all. default: png
|
|
7
|
+
* @param outputFilename optional filename of the output file.
|
|
8
|
+
*/
|
|
9
|
+
export declare const writeOutputFiles: (dot: string, contractName: string, outputFormat?: OutputFormats, outputFilename?: string) => Promise<void>;
|
|
3
10
|
export declare function convertDot2Svg(dot: string): any;
|
|
4
11
|
export declare function writeSolidity(code: string, filename?: string): void;
|
|
5
12
|
export declare function writeDot(dot: string, filename: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Writes an SVG file to the file system.
|
|
15
|
+
* @param svg The SVG input to be written to the file system.
|
|
16
|
+
* @param svgFilename The desired file name for the SVG file. default: classDiagram.svg
|
|
17
|
+
* @param outputFormats The format of the output file. choices: svg, png, dot or all. default: png
|
|
18
|
+
* @throws Error - If there is an error writing the SVG file.
|
|
19
|
+
*/
|
|
6
20
|
export declare function writeSVG(svg: any, svgFilename?: string, outputFormats?: OutputFormats): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Asynchronously writes a PNG file to the file system from an SVG input.
|
|
23
|
+
* @param svg - The SVG input to be converted to a PNG file.
|
|
24
|
+
* @param filename - The desired file name for the PNG file.
|
|
25
|
+
* @throws Error - If there is an error converting or writing the PNG file.
|
|
26
|
+
*/
|
|
7
27
|
export declare function writePng(svg: any, filename: string): Promise<void>;
|
package/lib/writerFiles.js
CHANGED
|
@@ -9,7 +9,14 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
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
|
+
/**
|
|
13
|
+
* Writes output files to the file system based on the provided input and options.
|
|
14
|
+
* @param dot The input string in DOT format.
|
|
15
|
+
* @param contractName The name of the contract.
|
|
16
|
+
* @param outputFormat The format of the output file. choices: svg, png, dot or all. default: png
|
|
17
|
+
* @param outputFilename optional filename of the output file.
|
|
18
|
+
*/
|
|
19
|
+
const writeOutputFiles = async (dot, contractName, outputFormat = 'svg', outputFilename) => {
|
|
13
20
|
// If all output then extension is svg
|
|
14
21
|
const outputExt = outputFormat === 'all' ? 'svg' : outputFormat;
|
|
15
22
|
if (!outputFilename) {
|
|
@@ -87,6 +94,13 @@ function writeDot(dot, filename) {
|
|
|
87
94
|
});
|
|
88
95
|
}
|
|
89
96
|
exports.writeDot = writeDot;
|
|
97
|
+
/**
|
|
98
|
+
* Writes an SVG file to the file system.
|
|
99
|
+
* @param svg The SVG input to be written to the file system.
|
|
100
|
+
* @param svgFilename The desired file name for the SVG file. default: classDiagram.svg
|
|
101
|
+
* @param outputFormats The format of the output file. choices: svg, png, dot or all. default: png
|
|
102
|
+
* @throws Error - If there is an error writing the SVG file.
|
|
103
|
+
*/
|
|
90
104
|
function writeSVG(svg, svgFilename = 'classDiagram.svg', outputFormats = 'png') {
|
|
91
105
|
debug(`About to write SVN file to ${svgFilename}`);
|
|
92
106
|
if (outputFormats === 'png') {
|
|
@@ -113,6 +127,12 @@ function writeSVG(svg, svgFilename = 'classDiagram.svg', outputFormats = 'png')
|
|
|
113
127
|
});
|
|
114
128
|
}
|
|
115
129
|
exports.writeSVG = writeSVG;
|
|
130
|
+
/**
|
|
131
|
+
* Asynchronously writes a PNG file to the file system from an SVG input.
|
|
132
|
+
* @param svg - The SVG input to be converted to a PNG file.
|
|
133
|
+
* @param filename - The desired file name for the PNG file.
|
|
134
|
+
* @throws Error - If there is an error converting or writing the PNG file.
|
|
135
|
+
*/
|
|
116
136
|
async function writePng(svg, filename) {
|
|
117
137
|
// get svg file name from png file name
|
|
118
138
|
const parsedPngFile = path_1.default.parse(filename);
|
|
@@ -142,7 +162,6 @@ async function writePng(svg, filename) {
|
|
|
142
162
|
cause: err,
|
|
143
163
|
});
|
|
144
164
|
}
|
|
145
|
-
console.log(`Generated png file ${pngFilename}`);
|
|
146
165
|
}
|
|
147
166
|
exports.writePng = writePng;
|
|
148
167
|
//# sourceMappingURL=writerFiles.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sol2uml",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Solidity contract visualisation tool.",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@openzeppelin/contracts": "4.8.0",
|
|
37
37
|
"@types/diff-match-patch": "^1.0.32",
|
|
38
|
-
"@types/jest": "^29.
|
|
38
|
+
"@types/jest": "^29.4.0",
|
|
39
39
|
"@types/klaw": "^3.0.3",
|
|
40
|
-
"jest": "^29.
|
|
41
|
-
"prettier": "^2.8.
|
|
42
|
-
"ts-jest": "^29.0.
|
|
40
|
+
"jest": "^29.4.1",
|
|
41
|
+
"prettier": "^2.8.3",
|
|
42
|
+
"ts-jest": "^29.0.5",
|
|
43
43
|
"ts-node": "^10.9.1",
|
|
44
44
|
"typescript": "^4.9.4"
|
|
45
45
|
},
|