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/lib/slotValues.js CHANGED
@@ -3,65 +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.getStorageValue = exports.getStorageValues = void 0;
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');
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;
179
+ };
10
180
  let jsonRpcId = 0;
11
181
  /**
12
182
  * Get storage slot values from JSON-RPC API provider.
13
183
  * @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy
14
184
  * @param contractAddress Contract address to get the storage slot values from.
15
185
  * If proxied, use proxy and not the implementation contract.
16
- * @param slots array of slot numbers to retrieve values for.
186
+ * @param slotKeys array of 32 byte slot keys as BigNumbers.
17
187
  * @param blockTag block number or `latest`
188
+ * @return slotValues array of 32 byte slot values as hexadecimal strings
18
189
  */
19
- const getStorageValues = async (url, contractAddress, slots, blockTag = 'latest') => {
190
+ const getSlotValues = async (url, contractAddress, slotKeys, blockTag = 'latest') => {
20
191
  try {
21
- debug(`About to get ${slots.length} storage values for ${contractAddress} at block ${blockTag}`);
192
+ if (slotKeys.length === 0) {
193
+ return [];
194
+ }
22
195
  const block = blockTag === 'latest'
23
196
  ? blockTag
24
- : bignumber_1.BigNumber.from(blockTag).toHexString();
25
- const payload = slots.map((slot) => ({
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) => ({
26
207
  id: (jsonRpcId++).toString(),
27
208
  jsonrpc: '2.0',
28
209
  method: 'eth_getStorageAt',
29
- params: [
30
- contractAddress,
31
- bignumber_1.BigNumber.from(slot).toHexString(),
32
- block,
33
- ],
210
+ params: [contractAddress, key, block],
34
211
  }));
35
212
  const response = await axios_1.default.post(url, payload);
36
- console.log(response.data);
37
213
  if (response.data?.error?.message) {
38
- throw new Error(response.data.error.message);
214
+ throw Error(response.data.error.message);
39
215
  }
40
- if (response.data.length !== slots.length) {
41
- throw new Error(`Requested ${slots.length} storage slot values but only got ${response.data.length}`);
216
+ if (response.data.length !== missingKeys.length) {
217
+ throw Error(`Requested ${missingKeys.length} storage slot values but only got ${response.data.length}`);
42
218
  }
43
219
  const responseData = response.data;
44
220
  const sortedResponses = responseData.sort((a, b) => bignumber_1.BigNumber.from(a.id).gt(b.id) ? 1 : -1);
45
- return sortedResponses.map((data) => '0x' + data.result.toUpperCase().slice(2));
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);
46
229
  }
47
230
  catch (err) {
48
- throw new Error(`Failed to get ${slots.length} storage values for ${contractAddress} from ${url}`, { cause: err });
231
+ throw Error(`Failed to get ${slotKeys.length} storage values for contract ${contractAddress} from ${url}`, { cause: err });
49
232
  }
50
233
  };
51
- exports.getStorageValues = getStorageValues;
234
+ exports.getSlotValues = getSlotValues;
52
235
  /**
53
236
  * Get storage slot values from JSON-RPC API provider.
54
237
  * @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy
55
238
  * @param contractAddress Contract address to get the storage slot values from.
56
239
  * If proxied, use proxy and not the implementation contract.
57
- * @param slot slot number to retrieve the value for.
240
+ * @param slotKey 32 byte slot key as a BigNumber.
58
241
  * @param blockTag block number or `latest`
242
+ * @return slotValue 32 byte slot value as hexadecimal string
59
243
  */
60
- const getStorageValue = async (url, contractAddress, slot, blockTag = 'latest') => {
61
- debug(`About to get storage slot ${slot} value for ${contractAddress}`);
62
- const values = await (0, exports.getStorageValues)(url, contractAddress, [slot], blockTag);
63
- debug(`Got slot ${slot} value: ${values[0]}`);
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);
64
247
  return values[0];
65
248
  };
66
- exports.getStorageValue = getStorageValue;
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;
67
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.')
@@ -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 storages = (0, converterClasses2Storage_1.convertClasses2Storages)(contractName, umlClasses, combinedOptions.contractFile);
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
- storages[0].address = fileFolderAddress;
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
- const storage = storages.find((so) => so.name === contractName);
161
- if (!storageAddress)
162
- throw Error(`Could not find the "${contractName}" contract in list of parsed storages`);
163
- await (0, converterClasses2Storage_1.addStorageValues)(combinedOptions.url, storageAddress, storage, combinedOptions.block);
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)(storages, combinedOptions);
180
+ const dotString = (0, converterStorage2Dot_1.convertStorages2Dot)(storageSections, combinedOptions);
166
181
  await (0, writerFiles_1.writeOutputFiles)(dotString, contractName || 'storageDiagram', combinedOptions.outputFormat, combinedOptions.outputFileName);
167
182
  }
168
183
  catch (err) {
169
- console.error(err.stack);
184
+ console.error(err);
170
185
  process.exit(2);
171
186
  }
172
187
  });
@@ -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[];
@@ -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 = [];
@@ -162,7 +162,6 @@ async function writePng(svg, filename) {
162
162
  cause: err,
163
163
  });
164
164
  }
165
- console.log(`Generated png file ${pngFilename}`);
166
165
  }
167
166
  exports.writePng = writePng;
168
167
  //# sourceMappingURL=writerFiles.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sol2uml",
3
- "version": "2.4.3",
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",