Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

sol2uml

Package Overview
Dependencies
Maintainers
1
Versions
84
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

sol2uml - npm Package Compare versions

Comparing version 2.4.3 to 2.5.0

lib/SlotValueCache.d.ts

2

lib/associations.d.ts
import { Association, UmlClass } from './umlClass';
export declare const findAssociatedClass: (association: Association, sourceUmlClass: UmlClass, umlClasses: UmlClass[], searchedAbsolutePaths?: string[]) => UmlClass | undefined;
export declare const findAssociatedClass: (association: Association, sourceUmlClass: UmlClass, umlClasses: readonly UmlClass[], searchedAbsolutePaths?: string[]) => UmlClass | undefined;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.findAssociatedClass = void 0;
const umlClass_1 = require("./umlClass");
// Find the UML class linked to the association
const findAssociatedClass = (association, sourceUmlClass, umlClasses, searchedAbsolutePaths = []) => {
let umlClass = umlClasses.find((targetUmlClass) => {
// is the source class link via the association to the target class?
if (isAssociated(association, sourceUmlClass, targetUmlClass))
return true;
// Not linked so now try linking to target under the node_modules folder.
// eg remove node_modules from node_modules/@openzeppelin/contracts-upgradeable/proxy/Initializable.sol
// is the target class under node_modules?
if (targetUmlClass.relativePath.match(/^node_modules\//)) {
// clone the target and updated absolutePath and relativePath so it's no longer under node_modules
const clonedTargetClass = new umlClass_1.UmlClass(targetUmlClass);
clonedTargetClass.absolutePath =
targetUmlClass.absolutePath.replace(/^node_modules\//, '');
clonedTargetClass.relativePath =
targetUmlClass.relativePath.replace(/^node_modules\//, '');
// is the source class link via the association to the target class?
return isAssociated(association, sourceUmlClass, clonedTargetClass);
}
// could not find a link from the source to target via the association
return false;
});
const umlClass = umlClasses.find((targetUmlClass) => isAssociated(association, sourceUmlClass, targetUmlClass));
// If a link was found

@@ -28,0 +8,0 @@ if (umlClass)

import { ASTNode } from '@solidity-parser/parser/dist/src/ast-types';
import { UmlClass } from './umlClass';
import { Remapping } from './parserEtherscan';
/**

@@ -7,5 +8,15 @@ * Convert solidity parser output of type `ASTNode` to UML classes of type `UMLClass`

* @param relativePath relative path from the working directory to the Solidity source file
* @param remappings used to rename relative paths
* @param filesystem flag if Solidity source code was parsed from the filesystem or Etherscan
* @return umlClasses array of UML class definitions of type `UmlClass`
*/
export declare function convertAST2UmlClasses(node: ASTNode, relativePath: string, filesystem?: boolean): UmlClass[];
export declare function convertAST2UmlClasses(node: ASTNode, relativePath: string, remappings: Remapping[], filesystem?: boolean): UmlClass[];
/**
* Used to rename import file names. For example
* @openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
* to
* lib/openzeppelin-contracts/@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
* @param fileName file name in the Solidity code
* @param mappings an array of remappings from Etherscan's settings
*/
export declare const renameFile: (fileName: string, mappings: Remapping[]) => string;

@@ -26,3 +26,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.convertAST2UmlClasses = void 0;
exports.renameFile = exports.convertAST2UmlClasses = void 0;
const path = __importStar(require("path"));

@@ -38,6 +38,7 @@ const path_1 = require("path");

* @param relativePath relative path from the working directory to the Solidity source file
* @param remappings used to rename relative paths
* @param filesystem flag if Solidity source code was parsed from the filesystem or Etherscan
* @return umlClasses array of UML class definitions of type `UmlClass`
*/
function convertAST2UmlClasses(node, relativePath, filesystem = false) {
function convertAST2UmlClasses(node, relativePath, remappings, filesystem = false) {
const imports = [];

@@ -115,7 +116,8 @@ umlClasses = [];

// this has come from Etherscan
const importPath = childNode.path[0] === '.'
const remappedFile = (0, exports.renameFile)(childNode.path, remappings);
const importPath = remappedFile[0] === '.'
? // Use Linux paths, not Windows paths, to resolve Etherscan files
path_1.posix.join(codeFolder.toString(), childNode.path)
: childNode.path;
debug(`codeFolder ${codeFolder} childNode.path ${childNode.path}`);
path_1.posix.join(codeFolder.toString(), remappedFile)
: remappedFile;
debug(`codeFolder ${codeFolder} childNode.path ${childNode.path} remapped to ${remappedFile}`);
const newImport = {

@@ -132,3 +134,6 @@ absolutePath: importPath,

};
debug(`Added Etherscan import ${newImport.absolutePath} with class names: ${newImport.classNames}`);
debug(`Added Etherscan import ${newImport.absolutePath} with:`);
newImport.classNames.forEach((className) => {
debug(`\t alias ${className.className}, name ${className.className}`);
});
imports.push(newImport);

@@ -678,2 +683,23 @@ }

}
/**
* Used to rename import file names. For example
* @openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
* to
* lib/openzeppelin-contracts/@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
* @param fileName file name in the Solidity code
* @param mappings an array of remappings from Etherscan's settings
*/
const renameFile = (fileName, mappings) => {
let renamedFile = fileName;
for (const mapping of mappings) {
if (renamedFile.match(mapping.from)) {
const beforeFileName = renamedFile;
renamedFile = renamedFile.replace(mapping.from, mapping.to);
debug(`remapping ${beforeFileName} to ${renamedFile}`);
break;
}
}
return renamedFile;
};
exports.renameFile = renameFile;
//# sourceMappingURL=converterAST2Classes.js.map

@@ -16,3 +16,7 @@ import { UmlClass } from './umlClass';

hideSourceContract?: boolean;
backColor?: string;
shapeColor?: string;
fillColor?: string;
textColor?: string;
}
export declare const convertClass2Dot: (umlClass: UmlClass, options?: ClassOptions) => string;

@@ -21,5 +21,6 @@ "use strict";

rankdir=BT
color=black
arrowhead=open
node [shape=record, style=filled, fillcolor=gray95]`;
bgcolor="${classOptions.backColor}"
edge [color="${classOptions.shapeColor}"]
node [shape=record, style=filled, color="${classOptions.shapeColor}", fillcolor="${classOptions.fillColor}", fontcolor="${classOptions.textColor}"]`;
// Sort UML Classes by folder of source file

@@ -26,0 +27,0 @@ const umlClassesSortedByCodePath = sortUmlClassesByCodePath(umlClasses);

@@ -1,7 +0,9 @@

import { Attribute, UmlClass } from './umlClass';
import { Attribute, AttributeType, UmlClass } from './umlClass';
import { BigNumberish } from '@ethersproject/bignumber';
export declare enum StorageType {
export declare enum StorageSectionType {
Contract = "Contract",
Struct = "Struct",
Array = "Array"
Array = "Array",
Bytes = "Bytes",
String = "String"
}

@@ -15,18 +17,22 @@ export interface Variable {

type: string;
attributeType: AttributeType;
dynamic: boolean;
variable?: string;
name?: string;
contractName?: string;
noValue: boolean;
value?: string;
referenceStorageId?: number;
enumId?: number;
displayValue: boolean;
getValue?: boolean;
slotValue?: string;
parsedValue?: string;
referenceSectionId?: number;
enumValues?: string[];
}
export interface Storage {
export interface StorageSection {
id: number;
name: string;
address?: string;
slotKey?: string;
type: StorageType;
offset?: string;
type: StorageSectionType;
arrayLength?: number;
arrayDynamic?: boolean;
mapping: boolean;
variables: Variable[];

@@ -36,19 +42,26 @@ }

*
* @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy
* @param contractAddress Contract address to get the storage slot values from.
* If proxied, use proxy and not the implementation contract.
* @param storage is mutated with the storage values
* @param blockTag block number or `latest`
*/
export declare const addStorageValues: (url: string, contractAddress: string, storage: Storage, blockTag?: BigNumberish | 'latest') => Promise<void>;
/**
*
* @param contractName name of the contract to get storage layout.
* @param umlClasses array of UML classes of type `UMLClass`
* @param arrayItems the number of items to display at the start and end of an array
* @param contractFilename relative path of the contract in the file system
* @return array of storage objects with consecutive slots
* @return storageSections array of storageSection objects
*/
export declare const convertClasses2Storages: (contractName: string, umlClasses: UmlClass[], contractFilename?: string) => Storage[];
export declare const parseReferenceStorage: (attribute: Attribute, umlClass: UmlClass, otherClasses: UmlClass[], storages: Storage[]) => Storage | undefined;
export declare const calcStorageByteSize: (attribute: Attribute, umlClass: UmlClass, otherClasses: UmlClass[]) => {
export declare const convertClasses2StorageSections: (contractName: string, umlClasses: UmlClass[], arrayItems: number, contractFilename?: string) => StorageSection[];
/**
* Recursively adds new storage sections under a class attribute.
* also returns the allowed enum values
* @param attribute the attribute that is referencing a storage section
* @param umlClass contract or file level struct
* @param otherClasses array of all the UML Classes
* @param storageSections mutable array of storageSection objects
* @param mapping flags that the storage section is under a mapping
* @param arrayItems the number of items to display at the start and end of an array
* @return storageSection new storage section that was added or undefined if none was added.
* @return enumValues array of allowed enum values. undefined if attribute is not an enum
*/
export declare const parseStorageSectionFromAttribute: (attribute: Attribute, umlClass: UmlClass, otherClasses: readonly UmlClass[], storageSections: StorageSection[], mapping: boolean, arrayItems: number) => {
storageSection: StorageSection;
enumValues?: string[];
};
export declare const calcStorageByteSize: (attribute: Attribute, umlClass: UmlClass, otherClasses: readonly UmlClass[]) => {
size: number;

@@ -58,4 +71,13 @@ dynamic: boolean;

export declare const isElementary: (type: string) => boolean;
export declare const calcSlotKey: (variable: Variable) => string | undefined;
export declare const offsetStorageSlots: (storage: Storage, slots: number, storages: Storage[]) => void;
export declare const findDimensionLength: (umlClass: UmlClass, dimension: string) => number;
export declare const calcSectionOffset: (variable: Variable, sectionOffset?: string) => string;
export declare const findDimensionLength: (umlClass: UmlClass, dimension: string, otherClasses: readonly UmlClass[]) => number;
/**
* Recursively adds variables for dynamic string, bytes or arrays
* @param storageSection
* @param storageSections
* @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy
* @param contractAddress Contract address to get the storage slot values from.
* @param arrayItems the number of items to display at the start and end of an array
* @param blockTag block number or `latest`
*/
export declare const addDynamicVariables: (storageSection: StorageSection, storageSections: StorageSection[], url: string, contractAddress: string, arrayItems: number, blockTag: BigNumberish) => Promise<void>;

@@ -6,16 +6,18 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.findDimensionLength = exports.offsetStorageSlots = exports.calcSlotKey = exports.isElementary = exports.calcStorageByteSize = exports.parseReferenceStorage = exports.convertClasses2Storages = exports.addStorageValues = exports.StorageType = void 0;
exports.addDynamicVariables = exports.findDimensionLength = exports.calcSectionOffset = exports.isElementary = exports.calcStorageByteSize = exports.parseStorageSectionFromAttribute = exports.convertClasses2StorageSections = exports.StorageSectionType = void 0;
const umlClass_1 = require("./umlClass");
const associations_1 = require("./associations");
const slotValues_1 = require("./slotValues");
const utils_1 = require("ethers/lib/utils");
const ethers_1 = require("ethers");
const path_1 = __importDefault(require("path"));
const slotValues_1 = require("./slotValues");
const debug = require('debug')('sol2uml');
var StorageType;
(function (StorageType) {
StorageType["Contract"] = "Contract";
StorageType["Struct"] = "Struct";
StorageType["Array"] = "Array";
})(StorageType = exports.StorageType || (exports.StorageType = {}));
var StorageSectionType;
(function (StorageSectionType) {
StorageSectionType["Contract"] = "Contract";
StorageSectionType["Struct"] = "Struct";
StorageSectionType["Array"] = "Array";
StorageSectionType["Bytes"] = "Bytes";
StorageSectionType["String"] = "String";
})(StorageSectionType = exports.StorageSectionType || (exports.StorageSectionType = {}));
let storageId = 1;

@@ -25,25 +27,9 @@ let variableId = 1;

*
* @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy
* @param contractAddress Contract address to get the storage slot values from.
* If proxied, use proxy and not the implementation contract.
* @param storage is mutated with the storage values
* @param blockTag block number or `latest`
*/
const addStorageValues = async (url, contractAddress, storage, blockTag) => {
const valueVariables = storage.variables.filter((s) => !s.noValue);
const slots = valueVariables.map((s) => s.fromSlot);
const values = await (0, slotValues_1.getStorageValues)(url, contractAddress, slots, blockTag);
valueVariables.forEach((valueVariable, i) => {
valueVariable.value = values[i];
});
};
exports.addStorageValues = addStorageValues;
/**
*
* @param contractName name of the contract to get storage layout.
* @param umlClasses array of UML classes of type `UMLClass`
* @param arrayItems the number of items to display at the start and end of an array
* @param contractFilename relative path of the contract in the file system
* @return array of storage objects with consecutive slots
* @return storageSections array of storageSection objects
*/
const convertClasses2Storages = (contractName, umlClasses, contractFilename) => {
const convertClasses2StorageSections = (contractName, umlClasses, arrayItems, contractFilename) => {
// Find the base UML Class from the base contract name

@@ -66,21 +52,28 @@ const umlClass = umlClasses.find(({ name, relativePath }) => {

debug(`Found contract "${contractName}" in ${umlClass.absolutePath}`);
const storages = [];
const variables = parseVariables(umlClass, umlClasses, [], storages, []);
storages.unshift({
const storageSections = [];
const variables = parseVariables(umlClass, umlClasses, [], storageSections, [], false, arrayItems);
// Add new storage section to the beginning of the array
storageSections.unshift({
id: storageId++,
name: contractName,
type: StorageType.Contract,
type: StorageSectionType.Contract,
variables: variables,
mapping: false,
});
return storages;
adjustSlots(storageSections[0], 0, storageSections);
return storageSections;
};
exports.convertClasses2Storages = convertClasses2Storages;
exports.convertClasses2StorageSections = convertClasses2StorageSections;
/**
* Recursively parses the storage variables for a given contract.
* Recursively parse the storage variables for a given contract or struct.
* @param umlClass contract or file level struct
* @param umlClasses other contracts, structs and enums that may be a type of a storage variable.
* @param variables mutable array of storage slots that is appended to
* @param storages mutable array of storages that is appended with structs
* @param variables mutable array of storage variables that are appended to
* @param storageSections mutable array of storageSection objects
* @param inheritedContracts mutable array of contracts that have been inherited already
* @param mapping flags that the storage section is under a mapping
* @param arrayItems the number of items to display at the start and end of an array
* @return variables array of storage variables in the `umlClass`
*/
const parseVariables = (umlClass, umlClasses, variables, storages, inheritedContracts) => {
const parseVariables = (umlClass, umlClasses, variables, storageSections, inheritedContracts, mapping, arrayItems) => {
// Add storage slots from inherited contracts first.

@@ -100,3 +93,3 @@ // Get immediate parent contracts that the class inherits from

// recursively parse inherited contract
parseVariables(parentClass, umlClasses, variables, storages, inheritedContracts);
parseVariables(parentClass, umlClasses, variables, storageSections, inheritedContracts, mapping, arrayItems);
});

@@ -109,65 +102,93 @@ // Parse storage for each attribute

const { size: byteSize, dynamic } = (0, exports.calcStorageByteSize)(attribute, umlClass, umlClasses);
const noValue = attribute.attributeType === umlClass_1.AttributeType.Mapping ||
(attribute.attributeType === umlClass_1.AttributeType.Array && !dynamic);
// find any dependent storage locations
const referenceStorage = (0, exports.parseReferenceStorage)(attribute, umlClass, umlClasses, storages);
// parse any dependent storage sections or enums
const references = (0, exports.parseStorageSectionFromAttribute)(attribute, umlClass, umlClasses, storageSections, mapping || attribute.attributeType === umlClass_1.AttributeType.Mapping, arrayItems);
// should this new variable get the slot value
const displayValue = calcDisplayValue(attribute.attributeType, dynamic, mapping, references?.storageSection?.type);
const getValue = calcGetValue(attribute.attributeType, mapping);
// Get the toSlot of the last storage item
let lastToSlot = 0;
let nextOffset = 0;
if (variables.length > 0) {
const lastStorage = variables[variables.length - 1];
lastToSlot = lastStorage.toSlot;
nextOffset = lastStorage.byteOffset + lastStorage.byteSize;
}
let newVariable;
const lastVariable = variables[variables.length - 1];
let lastToSlot = lastVariable ? lastVariable.toSlot : 0;
let nextOffset = lastVariable
? lastVariable.byteOffset + lastVariable.byteSize
: 0;
let fromSlot;
let toSlot;
let byteOffset;
if (nextOffset + byteSize > 32) {
const nextFromSlot = variables.length > 0 ? lastToSlot + 1 : 0;
newVariable = {
id: variableId++,
fromSlot: nextFromSlot,
toSlot: nextFromSlot + Math.floor((byteSize - 1) / 32),
byteSize,
byteOffset: 0,
type: attribute.type,
dynamic,
noValue,
variable: attribute.name,
contractName: umlClass.name,
referenceStorageId: referenceStorage?.id,
};
fromSlot = nextFromSlot;
toSlot = nextFromSlot + Math.floor((byteSize - 1) / 32);
byteOffset = 0;
}
else {
newVariable = {
id: variableId++,
fromSlot: lastToSlot,
toSlot: lastToSlot,
byteSize,
byteOffset: nextOffset,
type: attribute.type,
dynamic,
noValue,
variable: attribute.name,
contractName: umlClass.name,
referenceStorageId: referenceStorage?.id,
};
fromSlot = lastToSlot;
toSlot = lastToSlot;
byteOffset = nextOffset;
}
if (referenceStorage) {
if (!newVariable.dynamic) {
(0, exports.offsetStorageSlots)(referenceStorage, newVariable.fromSlot, storages);
variables.push({
id: variableId++,
fromSlot,
toSlot,
byteSize,
byteOffset,
type: attribute.type,
attributeType: attribute.attributeType,
dynamic,
getValue,
displayValue,
name: attribute.name,
contractName: umlClass.name,
referenceSectionId: references?.storageSection?.id,
enumValues: references?.enumValues,
});
});
return variables;
};
/**
* Recursively adjusts the fromSlot and toSlot properties of any storage variables
* that are referenced by a static array or struct.
* Also sets the storage slot offset for dynamic arrays, strings and bytes.
* @param storageSection
* @param slotOffset
* @param storageSections
*/
const adjustSlots = (storageSection, slotOffset, storageSections) => {
storageSection.variables.forEach((variable) => {
// offset storage slots
variable.fromSlot += slotOffset;
variable.toSlot += slotOffset;
// find storage section that the variable is referencing
const referenceStorageSection = storageSections.find((ss) => ss.id === variable.referenceSectionId);
if (referenceStorageSection) {
referenceStorageSection.offset = storageSection.offset;
if (!variable.dynamic) {
adjustSlots(referenceStorageSection, variable.fromSlot, storageSections);
}
else if (attribute.attributeType === umlClass_1.AttributeType.Array) {
referenceStorage.slotKey = (0, exports.calcSlotKey)(newVariable);
else if (variable.attributeType === umlClass_1.AttributeType.Array) {
// attribute is a dynamic array
referenceStorageSection.offset = (0, exports.calcSectionOffset)(variable, storageSection.offset);
adjustSlots(referenceStorageSection, 0, storageSections);
}
}
variables.push(newVariable);
});
return variables;
};
const parseReferenceStorage = (attribute, umlClass, otherClasses, storages) => {
/**
* Recursively adds new storage sections under a class attribute.
* also returns the allowed enum values
* @param attribute the attribute that is referencing a storage section
* @param umlClass contract or file level struct
* @param otherClasses array of all the UML Classes
* @param storageSections mutable array of storageSection objects
* @param mapping flags that the storage section is under a mapping
* @param arrayItems the number of items to display at the start and end of an array
* @return storageSection new storage section that was added or undefined if none was added.
* @return enumValues array of allowed enum values. undefined if attribute is not an enum
*/
const parseStorageSectionFromAttribute = (attribute, umlClass, otherClasses, storageSections, mapping, arrayItems) => {
if (attribute.attributeType === umlClass_1.AttributeType.Array) {
// storage is dynamic if the attribute type ends in []
const result = attribute.type.match(/\[(\w*)]$/);
const result = attribute.type.match(/\[([\w$.]*)]$/);
const dynamic = result[1] === '';
const arrayLength = !dynamic
? (0, exports.findDimensionLength)(umlClass, result[1])
? (0, exports.findDimensionLength)(umlClass, result[1], otherClasses)
: undefined;

@@ -189,10 +210,20 @@ // get the type of the array items. eg

visibility: attribute.visibility,
name: baseType,
name: attribute.name,
type: baseType,
attributeType: baseAttributeType,
};
const { size: arrayItemSize } = (0, exports.calcStorageByteSize)(baseAttribute, umlClass, otherClasses);
const { size: arrayItemSize, dynamic: dynamicBase } = (0, exports.calcStorageByteSize)(baseAttribute, umlClass, otherClasses);
// If more than 16 bytes, then round up in 32 bytes increments
const arraySlotSize = arrayItemSize > 16
? 32 * Math.ceil(arrayItemSize / 32)
: arrayItemSize;
// If base type is not an Elementary type
// This can only be Array and UserDefined for base types of arrays.
let references;
if (baseAttributeType !== umlClass_1.AttributeType.Elementary) {
// recursively add storage section for Array and UserDefined types
references = (0, exports.parseStorageSectionFromAttribute)(baseAttribute, umlClass, otherClasses, storageSections, mapping, arrayItems);
}
const displayValue = calcDisplayValue(baseAttribute.attributeType, dynamicBase, mapping, references?.storageSection?.type);
const getValue = calcGetValue(attribute.attributeType, mapping);
const variables = [];

@@ -206,55 +237,60 @@ variables[0] = {

type: baseType,
dynamic,
noValue: false,
attributeType: baseAttributeType,
dynamic: dynamicBase,
getValue,
displayValue,
referenceSectionId: references?.storageSection?.id,
enumValues: references?.enumValues,
};
// If a fixed size array.
// Note dynamic arrays will have undefined arrayLength
if (arrayLength > 1) {
// For fixed length arrays. Dynamic arrays will have undefined arrayLength
for (let i = 1; i < arrayLength; i++) {
variables.push({
id: variableId++,
fromSlot: Math.floor((i * arraySlotSize) / 32),
toSlot: Math.floor(((i + 1) * arraySlotSize - 1) / 32),
byteSize: arrayItemSize,
byteOffset: (i * arraySlotSize) % 32,
type: baseType,
dynamic,
noValue: false,
});
}
// Add missing fixed array variables from index 1
addArrayVariables(arrayLength, arrayItems, variables);
// For the newly added variables
variables.forEach((variable, i) => {
if (i > 0 &&
baseAttributeType !== umlClass_1.AttributeType.Elementary &&
variable.type !== '----' // ignore any filler variables
) {
// recursively add storage section for Array and UserDefined types
references = (0, exports.parseStorageSectionFromAttribute)(baseAttribute, umlClass, otherClasses, storageSections, mapping, arrayItems);
variable.referenceSectionId = references?.storageSection?.id;
variable.enumValues = references?.enumValues;
}
});
}
// recursively add storage
if (baseAttributeType !== umlClass_1.AttributeType.Elementary) {
const referenceStorage = (0, exports.parseReferenceStorage)(baseAttribute, umlClass, otherClasses, storages);
variables[0].referenceStorageId = referenceStorage?.id;
}
const newStorage = {
const storageSection = {
id: storageId++,
name: `${attribute.type}: ${attribute.name}`,
type: StorageType.Array,
type: StorageSectionType.Array,
arrayDynamic: dynamic,
arrayLength,
variables,
mapping,
};
storages.push(newStorage);
return newStorage;
storageSections.push(storageSection);
return { storageSection };
}
if (attribute.attributeType === umlClass_1.AttributeType.UserDefined) {
// Is the user defined type linked to another Contract, Struct or Enum?
const dependentClass = otherClasses.find(({ name }) => {
return (name === attribute.type || name === attribute.type.split('.')[1]);
});
if (!dependentClass) {
throw Error(`Failed to find user defined type "${attribute.type}"`);
}
if (dependentClass.stereotype === umlClass_1.ClassStereotype.Struct) {
const variables = parseVariables(dependentClass, otherClasses, [], storages, []);
const newStorage = {
const typeClass = findTypeClass(attribute.type, attribute, otherClasses);
if (typeClass.stereotype === umlClass_1.ClassStereotype.Struct) {
const variables = parseVariables(typeClass, otherClasses, [], storageSections, [], mapping, arrayItems);
const storageSection = {
id: storageId++,
name: attribute.type,
type: StorageType.Struct,
type: StorageSectionType.Struct,
variables,
mapping,
};
storages.push(newStorage);
return newStorage;
storageSections.push(storageSection);
return { storageSection };
}
else if (typeClass.stereotype === umlClass_1.ClassStereotype.Enum) {
return {
storageSection: undefined,
enumValues: typeClass.attributes.map((a) => a.name),
};
}
return undefined;

@@ -266,20 +302,18 @@ }

// Could also be a mapping of a mapping
const result = attribute.type.match(/=\\>((?!mapping)\w*)[\\[]/);
const result = attribute.type.match(/=\\>((?!mapping)[\w$.]*)[\\[]/);
// If mapping of user defined type
if (result !== null && result[1] && !(0, exports.isElementary)(result[1])) {
// Find UserDefined type
const typeClass = otherClasses.find(({ name }) => name === result[1] || name === result[1].split('.')[1]);
if (!typeClass) {
throw Error(`Failed to find user defined type "${result[1]}" in attribute type "${attribute.type}"`);
}
// Find UserDefined type can be a contract, struct or enum
const typeClass = findTypeClass(result[1], attribute, otherClasses);
if (typeClass.stereotype === umlClass_1.ClassStereotype.Struct) {
const variables = parseVariables(typeClass, otherClasses, [], storages, []);
const newStorage = {
let variables = parseVariables(typeClass, otherClasses, [], storageSections, [], true, arrayItems);
const storageSection = {
id: storageId++,
name: typeClass.name,
type: StorageType.Struct,
type: StorageSectionType.Struct,
mapping: true,
variables,
};
storages.push(newStorage);
return newStorage;
storageSections.push(storageSection);
return { storageSection };
}

@@ -291,3 +325,79 @@ }

};
exports.parseReferenceStorage = parseReferenceStorage;
exports.parseStorageSectionFromAttribute = parseStorageSectionFromAttribute;
/**
* Adds missing storage variables to a fixed-size or dynamic array by cloning them from the first variable.
* @param arrayLength the length of the array
* @param arrayItems the number of items to display at the start and end of an array
* @param variables mutable array of storage variables that are appended to
*/
const addArrayVariables = (arrayLength, arrayItems, variables) => {
const arraySlotSize = variables[0].byteSize;
const itemsPerSlot = Math.floor(32 / arraySlotSize);
const slotsPerItem = Math.ceil(arraySlotSize / 32);
const firstFillerItem = itemsPerSlot > 0 ? arrayItems * itemsPerSlot : arrayItems;
const lastFillerItem = itemsPerSlot > 0
? arrayLength -
(arrayItems - 1) * itemsPerSlot - // the number of items in all but the last row
(arrayLength % itemsPerSlot || itemsPerSlot) - // the remaining items in the last row or all the items in a slot
1 // need the items before the last three rows
: arrayLength - arrayItems - 1;
// Add variable from index 1 for each item in the array
for (let i = 1; i < arrayLength; i++) {
const fromSlot = itemsPerSlot > 0 ? Math.floor(i / itemsPerSlot) : i * slotsPerItem;
const toSlot = itemsPerSlot > 0 ? fromSlot : fromSlot + slotsPerItem;
// add filler variable before adding the first of the last items of the array
if (i === lastFillerItem && firstFillerItem < lastFillerItem) {
const fillerFromSlot = itemsPerSlot > 0
? Math.floor(firstFillerItem / itemsPerSlot)
: firstFillerItem * slotsPerItem;
variables.push({
id: variableId++,
attributeType: umlClass_1.AttributeType.UserDefined,
type: '----',
fromSlot: fillerFromSlot,
toSlot: toSlot,
byteOffset: 0,
byteSize: (toSlot - fillerFromSlot + 1) * 32,
getValue: false,
displayValue: false,
dynamic: false,
});
}
// Add variables for the first arrayItems and last arrayItems
if (i < firstFillerItem || i > lastFillerItem) {
const byteOffset = itemsPerSlot > 0 ? (i % itemsPerSlot) * arraySlotSize : 0;
const slotValue = fromSlot === 0 ? variables[0].slotValue : undefined;
// add array variable
const newVariable = {
...variables[0],
id: variableId++,
fromSlot,
toSlot,
byteOffset,
slotValue,
// These will be added in a separate step
parsedValue: undefined,
referenceSectionId: undefined,
enumValues: undefined,
};
newVariable.parsedValue = (0, slotValues_1.parseValue)(newVariable);
variables.push(newVariable);
}
}
};
/**
* Finds an attribute's user defined type that can be a Contract, Struct or Enum
* @param userType User defined type that is being looked for. This can be the base type of an attribute.
* @param attribute the attribute in the class that is user defined. This is just used for logging purposes
* @param otherClasses
*/
const findTypeClass = (userType, attribute, otherClasses) => {
// Find associated UserDefined type
// TODO this just matches on name and doesn't take into account imports
const typeClass = otherClasses.find(({ name }) => name === userType || name === userType.split('.')[1]);
if (!typeClass) {
throw Error(`Failed to find user defined type "${userType}" in attribute "${attribute.name}" of type "${attribute.attributeType}""`);
}
return typeClass;
};
// Calculates the storage size of an attribute in bytes

@@ -303,3 +413,3 @@ const calcStorageByteSize = (attribute, umlClass, otherClasses) => {

// while address [2][] is a dynamic sized array.
const arrayDimensions = attribute.type.match(/\[\w*]/g);
const arrayDimensions = attribute.type.match(/\[[\w$.]*]/g);
// Remove first [ and last ] from each arrayDimensions

@@ -313,3 +423,3 @@ const dimensionsStr = arrayDimensions.map((a) => a.slice(1, -1));

while (dimension && dimension !== '') {
const dimensionNum = (0, exports.findDimensionLength)(umlClass, dimension);
const dimensionNum = (0, exports.findDimensionLength)(umlClass, dimension, otherClasses);
fixedDimensions.push(dimensionNum);

@@ -325,5 +435,5 @@ // read the next dimension for the next loop

}
// If a fixed sized array
let elementSize;
const type = attribute.type.substring(0, attribute.type.indexOf('['));
// If a fixed sized array
if ((0, exports.isElementary)(type)) {

@@ -358,3 +468,7 @@ const elementAttribute = {

const lastItem = fixedDimensions.length - 1;
const lastDimensionBytes = elementSize * fixedDimensions[lastItem];
const lastArrayLength = fixedDimensions[lastItem];
const itemsPerSlot = Math.floor(32 / elementSize);
const lastDimensionBytes = itemsPerSlot > 0 // if one or more array items in a slot
? Math.ceil(lastArrayLength / itemsPerSlot) * 32 // round up to include unallocated slot space
: elementSize * fixedDimensions[lastItem];
const lastDimensionSlotBytes = Math.ceil(lastDimensionBytes / 32) * 32;

@@ -369,12 +483,8 @@ const remainingDimensions = fixedDimensions

}
// If a Struct or Enum
// If a Struct, Enum or Contract reference
// TODO need to handle User Defined Value Types when they are added to Solidity
if (attribute.attributeType === umlClass_1.AttributeType.UserDefined) {
// Is the user defined type linked to another Contract, Struct or Enum?
const attributeClass = otherClasses.find(({ name }) => {
return (name === attribute.type || name === attribute.type.split('.')[1]);
});
if (!attributeClass) {
throw Error(`Failed to find user defined struct or enum "${attribute.type}"`);
}
switch (attributeClass.stereotype) {
const attributeTypeClass = findTypeClass(attribute.type, attribute, otherClasses);
switch (attributeTypeClass.stereotype) {
case umlClass_1.ClassStereotype.Enum:

@@ -389,3 +499,3 @@ return { size: 1, dynamic: false };

let structByteSize = 0;
attributeClass.attributes.forEach((structAttribute) => {
attributeTypeClass.attributes.forEach((structAttribute) => {
// If next attribute is an array, then we need to start in a new slot

@@ -399,9 +509,3 @@ if (structAttribute.attributeType === umlClass_1.AttributeType.Array) {

// UserDefined types can be a struct or enum, so we need to check if it's a struct
const userDefinedClass = otherClasses.find(({ name }) => {
return (name === structAttribute.type ||
name === structAttribute.type.split('.')[1]);
});
if (!userDefinedClass) {
throw Error(`Failed to find user defined type "${structAttribute.type}" in struct ${attributeClass.name}`);
}
const userDefinedClass = findTypeClass(structAttribute.type, structAttribute, otherClasses);
// If a struct

@@ -441,2 +545,3 @@ if (userDefinedClass.stereotype ===

case 'bytes':
return { size: 32, dynamic: true };
case 'uint':

@@ -477,3 +582,3 @@ case 'int':

default:
const result = type.match(/[u]*(int|fixed|bytes)([0-9]+)/);
const result = type.match(/^[u]?(int|fixed|bytes)([0-9]+)$/);
return result !== null;

@@ -483,28 +588,11 @@ }

exports.isElementary = isElementary;
const calcSlotKey = (variable) => {
const calcSectionOffset = (variable, sectionOffset = '0') => {
if (variable.dynamic) {
return (0, utils_1.keccak256)((0, utils_1.toUtf8Bytes)(ethers_1.BigNumber.from(variable.fromSlot).toHexString()));
const hexStringOf32Bytes = (0, utils_1.hexZeroPad)(ethers_1.BigNumber.from(variable.fromSlot).add(sectionOffset).toHexString(), 32);
return (0, utils_1.keccak256)(hexStringOf32Bytes);
}
return ethers_1.BigNumber.from(variable.fromSlot).toHexString();
return ethers_1.BigNumber.from(variable.fromSlot).add(sectionOffset).toHexString();
};
exports.calcSlotKey = calcSlotKey;
// recursively offset the slots numbers of a storage item
const offsetStorageSlots = (storage, slots, storages) => {
storage.variables.forEach((variable) => {
variable.fromSlot += slots;
variable.toSlot += slots;
if (variable.referenceStorageId) {
// recursively offset the referenced storage
const referenceStorage = storages.find((s) => s.id === variable.referenceStorageId);
if (!referenceStorage.arrayDynamic) {
(0, exports.offsetStorageSlots)(referenceStorage, slots, storages);
}
else {
referenceStorage.slotKey = (0, exports.calcSlotKey)(variable);
}
}
});
};
exports.offsetStorageSlots = offsetStorageSlots;
const findDimensionLength = (umlClass, dimension) => {
exports.calcSectionOffset = calcSectionOffset;
const findDimensionLength = (umlClass, dimension, otherClasses) => {
const dimensionNum = parseInt(dimension);

@@ -514,12 +602,186 @@ if (Number.isInteger(dimensionNum)) {

}
else {
// Try and size array dimension from declared constants
const constant = umlClass.constants.find((constant) => constant.name === dimension);
if (!constant) {
throw Error(`Could not size fixed sized array with dimension "${dimension}"`);
}
// Try and size array dimension from declared constants
const constant = umlClass.constants.find((constant) => constant.name === dimension);
if (constant) {
return constant.value;
}
// Try and size array dimension from file constants
const fileConstant = otherClasses.find((umlClass) => umlClass.name === dimension &&
umlClass.stereotype === umlClass_1.ClassStereotype.Constant);
if (fileConstant?.constants[0]?.value) {
return fileConstant.constants[0].value;
}
throw Error(`Could not size fixed sized array with dimension "${dimension}"`);
};
exports.findDimensionLength = findDimensionLength;
/**
* Calculate if the storage slot value for the attribute should be displayed in the storage section.
*
* Storage sections with true mapping should return false.
* Mapping types should return false.
* Elementary types should return true.
* Dynamic Array types should return true.
* Static Array types should return false.
* UserDefined types that are Structs should return false.
* UserDefined types that are Enums or alias to Elementary type or contract should return true.
*
* @param attributeType
* @param dynamic flags if the variable is of dynamic size
* @param mapping flags if the storage section is referenced by a mapping
* @param storageSectionType
* @return displayValue true if the slot value should be displayed.
*/
const calcDisplayValue = (attributeType, dynamic, mapping, storageSectionType) => mapping === false &&
(attributeType === umlClass_1.AttributeType.Elementary ||
(attributeType === umlClass_1.AttributeType.UserDefined &&
storageSectionType !== StorageSectionType.Struct) ||
(attributeType === umlClass_1.AttributeType.Array && dynamic));
/**
* Calculate if the storage slot value for the attribute should be retrieved from the chain.
*
* Storage sections with true mapping should return false.
* Mapping types should return false.
* Elementary types should return true.
* Array types should return true.
* UserDefined should return true.
*
* @param attributeType the type of attribute the storage variable is for.
* @param mapping flags if the storage section is referenced by a mapping
* @return getValue true if the slot value should be retrieved.
*/
const calcGetValue = (attributeType, mapping) => mapping === false && attributeType !== umlClass_1.AttributeType.Mapping;
/**
* Recursively adds variables for dynamic string, bytes or arrays
* @param storageSection
* @param storageSections
* @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy
* @param contractAddress Contract address to get the storage slot values from.
* @param arrayItems the number of items to display at the start and end of an array
* @param blockTag block number or `latest`
*/
const addDynamicVariables = async (storageSection, storageSections, url, contractAddress, arrayItems, blockTag) => {
for (const variable of storageSection.variables) {
try {
if (!variable.dynamic)
continue;
// STEP 1 - add slots for dynamic string and bytes
if (variable.type === 'string' || variable.type === 'bytes') {
if (!variable.slotValue) {
debug(`WARNING: Variable "${variable.name}" of type "${variable.type}" has no slot value`);
continue;
}
const size = (0, slotValues_1.dynamicSlotSize)(variable);
if (size > 31) {
const maxSlotNumber = Math.floor((size - 1) / 32);
const variables = [];
// For each dynamic slot
for (let i = 0; i <= maxSlotNumber; i++) {
// If the last slot then get the remaining bytes
const byteSize = i === maxSlotNumber ? size - 32 * maxSlotNumber : 32;
// Add variable for the slot
variables.push({
id: variableId++,
fromSlot: i,
toSlot: i,
byteSize,
byteOffset: 0,
type: variable.type,
contractName: variable.contractName,
attributeType: umlClass_1.AttributeType.Elementary,
dynamic: false,
getValue: true,
displayValue: true,
});
}
// add unallocated variable
const unusedBytes = 32 - (size - 32 * maxSlotNumber);
if (unusedBytes > 0) {
const lastVariable = variables[variables.length - 1];
variables.push({
...lastVariable,
byteOffset: unusedBytes,
});
variables[maxSlotNumber] = {
id: variableId++,
fromSlot: maxSlotNumber,
toSlot: maxSlotNumber,
byteSize: unusedBytes,
byteOffset: 0,
type: 'unallocated',
attributeType: umlClass_1.AttributeType.UserDefined,
contractName: variable.contractName,
name: '',
dynamic: false,
getValue: true,
displayValue: false,
};
}
const newStorageSection = {
id: storageId++,
name: `${variable.type}: ${variable.name}`,
offset: (0, exports.calcSectionOffset)(variable, storageSection.offset),
type: variable.type === 'string'
? StorageSectionType.String
: StorageSectionType.Bytes,
arrayDynamic: true,
arrayLength: size,
variables,
mapping: false,
};
variable.referenceSectionId = newStorageSection.id;
// get slot values for new referenced dynamic string or bytes
await (0, slotValues_1.addSlotValues)(url, contractAddress, newStorageSection, arrayItems, blockTag);
storageSections.push(newStorageSection);
}
continue;
}
if (variable.attributeType !== umlClass_1.AttributeType.Array)
continue;
// STEP 2 - add slots for dynamic arrays
// find storage section that the variable is referencing
const referenceStorageSection = storageSections.find((ss) => ss.id === variable.referenceSectionId);
if (!referenceStorageSection)
continue;
// recursively add dynamic variables to referenced array.
// this could be a fixed-size or dynamic array
await (0, exports.addDynamicVariables)(referenceStorageSection, storageSections, url, contractAddress, arrayItems, blockTag);
if (!variable.slotValue) {
debug(`WARNING: Dynamic array variable "${variable.name}" of type "${variable.type}" has no slot value`);
continue;
}
// Add missing dynamic array variables
const arrayLength = ethers_1.BigNumber.from(variable.slotValue).toNumber();
if (arrayLength > 1) {
// Add missing array variables to the referenced dynamic array
addArrayVariables(arrayLength, arrayItems, referenceStorageSection.variables);
// // For the newly added variables
// referenceStorageSection.variables.forEach((variable, i) => {
// if (
// referenceStorageSection.variables[0].attributeType !==
// AttributeType.Elementary &&
// i > 0
// ) {
// // recursively add storage section for Array and UserDefined types
// const references = parseStorageSectionFromAttribute(
// baseAttribute,
// umlClass,
// otherClasses,
// storageSections,
// mapping,
// arrayItems
// )
// variable.referenceSectionId = references.storageSection?.id
// variable.enumValues = references?.enumValues
// }
// })
}
// Get missing slot values to the referenced dynamic array
await (0, slotValues_1.addSlotValues)(url, contractAddress, referenceStorageSection, arrayItems, blockTag);
}
catch (err) {
throw Error(`Failed to add dynamic vars for section "${storageSection.name}", var type "${variable.type}" with value "${variable.slotValue}" from slot ${variable.fromSlot} and section offset ${storageSection.offset}`, { cause: err });
}
}
};
exports.addDynamicVariables = addDynamicVariables;
//# sourceMappingURL=converterClasses2Storage.js.map

@@ -1,7 +0,11 @@

import { Storage } from './converterClasses2Storage';
export declare const convertStorages2Dot: (storages: Storage[], options: {
import { StorageSection } from './converterClasses2Storage';
export declare const convertStorages2Dot: (storageSections: readonly StorageSection[], options: {
data: boolean;
backColor: string;
shapeColor: string;
fillColor: string;
textColor: string;
}) => string;
export declare function convertStorage2Dot(storage: Storage, dotString: string, options: {
export declare function convertStorage2Dot(storageSection: StorageSection, dotString: string, options: {
data: boolean;
}): string;

@@ -5,19 +5,21 @@ "use strict";

const converterClasses2Storage_1 = require("./converterClasses2Storage");
const umlClass_1 = require("./umlClass");
const debug = require('debug')('sol2uml');
const convertStorages2Dot = (storages, options) => {
const convertStorages2Dot = (storageSections, options) => {
let dotString = `
digraph StorageDiagram {
rankdir=LR
color=black
arrowhead=open
node [shape=record, style=filled, fillcolor=gray95 fontname="Courier New"]`;
bgcolor="${options.backColor}"
edge [color="${options.shapeColor}"]
node [shape=record, style=filled, color="${options.shapeColor}", fillcolor="${options.fillColor}", fontcolor="${options.textColor}", fontname="Courier New"]`;
// process contract and the struct storages
storages.forEach((storage) => {
storageSections.forEach((storage) => {
dotString = convertStorage2Dot(storage, dotString, options);
});
// link contract and structs to structs
storages.forEach((slot) => {
storageSections.forEach((slot) => {
slot.variables.forEach((storage) => {
if (storage.referenceStorageId) {
dotString += `\n ${slot.id}:${storage.id} -> ${storage.referenceStorageId}`;
if (storage.referenceSectionId) {
dotString += `\n ${slot.id}:${storage.id} -> ${storage.referenceSectionId}`;
}

@@ -32,15 +34,23 @@ });

exports.convertStorages2Dot = convertStorages2Dot;
function convertStorage2Dot(storage, dotString, options) {
function convertStorage2Dot(storageSection, dotString, options) {
// write storage header with name and optional address
dotString += `\n${storage.id} [label="${storage.name} \\<\\<${storage.type}\\>\\>\\n${storage.address || storage.slotKey || ''}`;
dotString += `\n${storageSection.id} [label="${storageSection.name} \\<\\<${storageSection.type}\\>\\>\\n${storageSection.address || storageSection.offset || ''}`;
dotString += ' | {';
const startingVariables = storage.variables.filter((s) => s.byteOffset === 0);
const startingVariables = storageSection.variables.filter((s) => s.byteOffset === 0);
// for each slot displayed, does is have any variables with parsed data?
const displayData = startingVariables.map((startVar) => storageSection.variables.some((variable) => variable.fromSlot === startVar.fromSlot && variable.parsedValue));
const linePad = '\\n\\ ';
// write slot numbers
dotString += '{ slot';
const dataLine = options.data ? linePad : '';
dotString +=
storageSection.offset || storageSection.mapping
? `{ offset${dataLine}`
: `{ slot${dataLine}`;
startingVariables.forEach((variable, i) => {
const dataLine = options.data && displayData[i] ? linePad : '';
if (variable.fromSlot === variable.toSlot) {
dotString += `| ${variable.fromSlot} `;
dotString += ` | ${variable.fromSlot}${dataLine}`;
}
else {
dotString += `| ${variable.fromSlot}-${variable.toSlot} `;
dotString += ` | ${variable.fromSlot}-${variable.toSlot}${dataLine}`;
}

@@ -50,13 +60,21 @@ });

if (options.data) {
dotString += '} | {value';
dotString += `} | {value${dataLine}`;
startingVariables.forEach((variable, i) => {
dotString += ` | ${variable.value || ''}`;
if (displayData[i]) {
dotString += ` | ${variable.slotValue || ''}${linePad}`;
}
else {
dotString += ` | `;
}
});
}
const contractVariablePrefix = storage.type === converterClasses2Storage_1.StorageType.Contract ? '\\<inherited contract\\>.' : '';
dotString += `} | { type: ${contractVariablePrefix}variable (bytes)`;
const contractVariablePrefix = storageSection.type === converterClasses2Storage_1.StorageSectionType.Contract
? '\\<inherited contract\\>.'
: '';
const dataLine2 = options.data ? `\\ndecoded data` : '';
dotString += `} | { type: ${contractVariablePrefix}variable (bytes)${dataLine2}`;
// For each slot
startingVariables.forEach((variable) => {
// Get all the storage variables in this slot
const slotVariables = storage.variables.filter((s) => s.fromSlot === variable.fromSlot);
const slotVariables = storageSection.variables.filter((s) => s.fromSlot === variable.fromSlot);
const usedBytes = slotVariables.reduce((acc, s) => acc + s.byteSize, 0);

@@ -72,6 +90,8 @@ if (usedBytes < 32) {

type: 'unallocated',
attributeType: umlClass_1.AttributeType.UserDefined,
dynamic: false,
noValue: true,
displayValue: false,
getValue: false,
contractName: variable.contractName,
variable: '',
name: '',
});

@@ -83,6 +103,6 @@ }

if (i === 0) {
dotString += ` | { ${dotVariable(variable, storage.name)} `;
dotString += ` | { ${dotVariable(variable, storageSection.name)} `;
}
else {
dotString += ` | ${dotVariable(variable, storage.name)} `;
dotString += ` | ${dotVariable(variable, storageSection.name)} `;
}

@@ -97,10 +117,15 @@ });

exports.convertStorage2Dot = convertStorage2Dot;
const dotVariable = (storage, contractName) => {
const port = storage.referenceStorageId !== undefined ? `<${storage.id}>` : '';
const contractNamePrefix = storage.contractName !== contractName ? `${storage.contractName}.` : '';
const variable = storage.variable
? `: ${contractNamePrefix}${storage.variable}`
const dotVariable = (variable, contractName) => {
const port = variable.referenceSectionId !== undefined ? `<${variable.id}>` : '';
const contractNamePrefix = variable.contractName !== contractName
? `${variable.contractName}.`
: '';
return `${port} ${storage.type}${variable} (${storage.byteSize})`;
const variableValue = variable.parsedValue
? `\\n\\ ${variable.parsedValue}`
: '';
const variableName = variable.name
? `: ${contractNamePrefix}${variable.name}`
: '';
return `${port} ${variable.type}${variableName} (${variable.byteSize})${variableValue}`;
};
//# sourceMappingURL=converterStorage2Dot.js.map

@@ -10,3 +10,3 @@ import { WeightedDiGraph } from 'js-graph-algorithms';

*/
export declare const filterHiddenClasses: (umlClasses: UmlClass[], options: ClassOptions) => UmlClass[];
export declare const filterHiddenClasses: (umlClasses: readonly UmlClass[], options: ClassOptions) => UmlClass[];
/**

@@ -20,3 +20,3 @@ * Finds all the UML classes that have an association with a list of base contract names.

*/
export declare const classesConnectedToBaseContracts: (umlClasses: UmlClass[], baseContractNames: string[], depth?: number) => UmlClass[];
export declare const classesConnectedToBaseContracts: (umlClasses: readonly UmlClass[], baseContractNames: readonly string[], depth?: number) => UmlClass[];
/**

@@ -31,5 +31,5 @@ * Finds all the UML classes that have an association with a base contract name.

*/
export declare const classesConnectedToBaseContract: (umlClasses: UmlClass[], baseContractName: string, weightedDirectedGraph: WeightedDiGraph, depth?: number) => {
export declare const classesConnectedToBaseContract: (umlClasses: readonly UmlClass[], baseContractName: string, weightedDirectedGraph: WeightedDiGraph, depth?: number) => {
[contractName: string]: UmlClass;
};
export declare const topologicalSortClasses: (umlClasses: UmlClass[]) => UmlClass[];
export declare const topologicalSortClasses: (umlClasses: readonly UmlClass[]) => UmlClass[];

@@ -7,2 +7,3 @@ "use strict";

const associations_1 = require("./associations");
const debug = require('debug')('sol2uml');
/**

@@ -89,3 +90,3 @@ * Filter out any UML Class types that are to be hidden.

const isTarget = umlClasses.find((u) => u.id === targetUmlClass.id);
console.log(`isTarget ${isTarget} Adding edge from ${sourceUmlClass.name} with id ${sourceUmlClass.id} to ${targetUmlClass.name} with id ${targetUmlClass.id} and type ${targetUmlClass.stereotype}`);
debug(`isTarget ${isTarget} Adding edge from ${sourceUmlClass.name} with id ${sourceUmlClass.id} to ${targetUmlClass.name} with id ${targetUmlClass.id} and type ${targetUmlClass.stereotype}`);
weightedDirectedGraph.addEdge(new js_graph_algorithms_1.Edge(sourceUmlClass.id, targetUmlClass.id, 1));

@@ -92,0 +93,0 @@ }

import { ASTNode } from '@solidity-parser/parser/dist/src/ast-types';
import { UmlClass } from './umlClass';
export interface Remapping {
from: RegExp;
to: string;
}
export declare const networks: readonly ["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"];
export type Network = typeof networks[number];
export type Network = (typeof networks)[number];
export declare class EtherscanParser {

@@ -40,3 +44,3 @@ protected apikey: string;

getSourceCode(contractAddress: string): Promise<{
files: {
files: readonly {
code: string;

@@ -47,3 +51,17 @@ filename: string;

compilerVersion: string;
remappings: Remapping[];
}>;
}
/**
* Parses Ethersan's remappings config in its API response
* @param rawMappings
*/
export declare const parseRemappings: (rawMappings: string[]) => Remapping[];
/**
* Parses a single mapping. For example
* "@openzeppelin/=lib/openzeppelin-contracts/"
* This is from Uniswap's UniversalRouter in the Settings section after the source files
* https://etherscan.io/address/0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B#code
* @param mapping
*/
export declare const parseRemapping: (mapping: string) => Remapping;

@@ -6,3 +6,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.EtherscanParser = exports.networks = void 0;
exports.parseRemapping = exports.parseRemappings = exports.EtherscanParser = exports.networks = void 0;
const axios_1 = __importDefault(require("axios"));

@@ -114,3 +114,3 @@ const parser_1 = require("@solidity-parser/parser");

async getUmlClasses(contractAddress) {
const { files, contractName } = await this.getSourceCode(contractAddress);
const { files, contractName, remappings } = await this.getSourceCode(contractAddress);
let umlClasses = [];

@@ -120,3 +120,3 @@ for (const file of files) {

const node = await this.parseSourceCode(file.code);
const umlClass = (0, converterAST2Classes_1.convertAST2UmlClasses)(node, file.filename);
const umlClass = (0, converterAST2Classes_1.convertAST2UmlClasses)(node, file.filename, remappings);
umlClasses = umlClasses.concat(umlClass);

@@ -136,3 +136,3 @@ }

async getSolidityCode(contractAddress) {
const { files, contractName, compilerVersion } = await this.getSourceCode(contractAddress);
const { files, contractName, compilerVersion, remappings } = await this.getSourceCode(contractAddress);
// Parse the UmlClasses from the Solidity code in each file

@@ -142,3 +142,3 @@ let umlClasses = [];

const node = await this.parseSourceCode(file.code);
const umlClass = (0, converterAST2Classes_1.convertAST2UmlClasses)(node, file.filename);
const umlClass = (0, converterAST2Classes_1.convertAST2UmlClasses)(node, file.filename, remappings);
umlClasses = umlClasses.concat(umlClass);

@@ -217,2 +217,3 @@ }

}
let remappings;
const results = response.data.result.map((result) => {

@@ -232,2 +233,4 @@ if (!result.SourceCode) {

const sourceCodeObject = JSON.parse(parableResultString);
// Get any remapping of filenames from the settings
remappings = (0, exports.parseRemappings)(sourceCodeObject.settings?.remappings);
// The getsource response from Etherscan is inconsistent so we need to handle both shapes

@@ -249,2 +252,4 @@ const sourceFiles = sourceCodeObject.sources

const sourceFiles = Object.values(result.SourceCode.sources);
// Get any remapping of filenames from the settings
remappings = (0, exports.parseRemappings)(result.SourceCode.settings?.remappings);
return sourceFiles.map(([filename, code]) => ({

@@ -265,2 +270,3 @@ code: code.content,

compilerVersion: response.data.result[0].CompilerVersion,
remappings,
};

@@ -280,2 +286,29 @@ }

exports.EtherscanParser = EtherscanParser;
/**
* Parses Ethersan's remappings config in its API response
* @param rawMappings
*/
const parseRemappings = (rawMappings) => {
if (!rawMappings)
return [];
return rawMappings.map((mapping) => (0, exports.parseRemapping)(mapping));
};
exports.parseRemappings = parseRemappings;
/**
* Parses a single mapping. For example
* "@openzeppelin/=lib/openzeppelin-contracts/"
* This is from Uniswap's UniversalRouter in the Settings section after the source files
* https://etherscan.io/address/0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B#code
* @param mapping
*/
const parseRemapping = (mapping) => {
const equalIndex = mapping.indexOf('=');
const from = mapping.slice(0, equalIndex);
const to = mapping.slice(equalIndex + 1);
return {
from: new RegExp('^' + from),
to,
};
};
exports.parseRemapping = parseRemapping;
//# sourceMappingURL=parserEtherscan.js.map
import { ASTNode } from '@solidity-parser/parser/dist/src/ast-types';
import { UmlClass } from './umlClass';
export declare const parseUmlClassesFromFiles: (filesOrFolders: string[], ignoreFilesOrFolders: string[], subfolders?: number) => Promise<UmlClass[]>;
export declare function getSolidityFilesFromFolderOrFiles(folderOrFilePaths: string[], ignoreFilesOrFolders: string[], subfolders?: number): Promise<string[]>;
export declare function getSolidityFilesFromFolderOrFile(folderOrFilePath: string, ignoreFilesOrFolders?: string[], depthLimit?: number): Promise<string[]>;
export declare const parseUmlClassesFromFiles: (filesOrFolders: readonly string[], ignoreFilesOrFolders: readonly string[], subfolders?: number) => Promise<UmlClass[]>;
export declare function getSolidityFilesFromFolderOrFiles(folderOrFilePaths: readonly string[], ignoreFilesOrFolders: readonly string[], subfolders?: number): Promise<string[]>;
export declare function getSolidityFilesFromFolderOrFile(folderOrFilePath: string, ignoreFilesOrFolders?: readonly string[], depthLimit?: number): Promise<string[]>;
export declare function parseSolidityFile(fileName: string): ASTNode;

@@ -19,3 +19,3 @@ "use strict";

const relativePath = (0, path_1.relative)(process.cwd(), file);
const umlClass = (0, converterAST2Classes_1.convertAST2UmlClasses)(node, relativePath, true);
const umlClass = (0, converterAST2Classes_1.convertAST2UmlClasses)(node, relativePath, [], true);
umlClasses = umlClasses.concat(umlClass);

@@ -86,3 +86,3 @@ }

}
console.error(error.stack);
console.error(error);
reject(error);

@@ -89,0 +89,0 @@ }

import { BigNumberish } from '@ethersproject/bignumber';
import { StorageSection, Variable } from './converterClasses2Storage';
/**
* Adds the slot values to the variables in the storage section.
* This can be rerun for a section as it will only get if the slot value
* does not exist.
* @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy
* @param contractAddress Contract address to get the storage slot values from.
* If contract is proxied, use proxy and not the implementation contract.
* @param storageSection is mutated with the slot values added to the variables
* @param arrayItems the number of items to display at the start and end of an array
* @param blockTag block number or `latest`
*/
export declare const addSlotValues: (url: string, contractAddress: string, storageSection: StorageSection, arrayItems: number, blockTag: BigNumberish) => Promise<void>;
export declare const parseValue: (variable: Variable) => string;
/**
* Get storage slot values from JSON-RPC API provider.

@@ -7,6 +21,7 @@ * @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy

* If proxied, use proxy and not the implementation contract.
* @param slots array of slot numbers to retrieve values for.
* @param slotKeys array of 32 byte slot keys as BigNumbers.
* @param blockTag block number or `latest`
* @return slotValues array of 32 byte slot values as hexadecimal strings
*/
export declare const getStorageValues: (url: string, contractAddress: string, slots: BigNumberish[], blockTag?: BigNumberish | 'latest') => Promise<string[]>;
export declare const getSlotValues: (url: string, contractAddress: string, slotKeys: readonly BigNumberish[], blockTag?: BigNumberish | 'latest') => Promise<string[]>;
/**

@@ -17,5 +32,20 @@ * Get storage slot values from JSON-RPC API provider.

* If proxied, use proxy and not the implementation contract.
* @param slot slot number to retrieve the value for.
* @param slotKey 32 byte slot key as a BigNumber.
* @param blockTag block number or `latest`
* @return slotValue 32 byte slot value as hexadecimal string
*/
export declare const getStorageValue: (url: string, contractAddress: string, slot: BigNumberish, blockTag?: BigNumberish | 'latest') => Promise<string>;
export declare const getSlotValue: (url: string, contractAddress: string, slotKey: BigNumberish, blockTag: BigNumberish | 'latest') => Promise<string>;
/**
* Calculates the number of string characters or bytes of a string or bytes type.
* See the following for how string and bytes are stored in storage slots
* https://docs.soliditylang.org/en/v0.8.17/internals/layout_in_storage.html#bytes-and-string
* @param variable the variable with the slotValue that is being sized
* @return bytes the number of bytes of the dynamic slot. If static, zero is return.
*/
export declare const dynamicSlotSize: (variable: {
name?: string;
type?: string;
slotValue?: string;
}) => number;
export declare const convert2String: (bytes: string) => string;
export declare const escapeString: (text: string) => string;

@@ -6,6 +6,176 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.getStorageValue = exports.getStorageValues = void 0;
exports.escapeString = exports.convert2String = exports.dynamicSlotSize = exports.getSlotValue = exports.getSlotValues = exports.parseValue = exports.addSlotValues = void 0;
const bignumber_1 = require("@ethersproject/bignumber");
const axios_1 = __importDefault(require("axios"));
const umlClass_1 = require("./umlClass");
const utils_1 = require("ethers/lib/utils");
const SlotValueCache_1 = require("./SlotValueCache");
const debug = require('debug')('sol2uml');
/**
* Adds the slot values to the variables in the storage section.
* This can be rerun for a section as it will only get if the slot value
* does not exist.
* @param url of Ethereum JSON-RPC API provider. eg Infura or Alchemy
* @param contractAddress Contract address to get the storage slot values from.
* If contract is proxied, use proxy and not the implementation contract.
* @param storageSection is mutated with the slot values added to the variables
* @param arrayItems the number of items to display at the start and end of an array
* @param blockTag block number or `latest`
*/
const addSlotValues = async (url, contractAddress, storageSection, arrayItems, blockTag) => {
const valueVariables = storageSection.variables.filter((variable) => variable.getValue && !variable.slotValue);
if (valueVariables.length === 0)
return;
// for each variable, add all the slots used by the variable.
const slots = [];
valueVariables.forEach((variable) => {
for (let i = 0; variable.fromSlot + i <= variable.toSlot; i++) {
if (variable.attributeType === umlClass_1.AttributeType.Array &&
i >= arrayItems &&
i < variable.toSlot - arrayItems) {
continue;
}
slots.push(variable.fromSlot + i);
}
});
// remove duplicate slot numbers
const uniqueFromSlots = [...new Set(slots)];
// Convert slot numbers to BigNumbers and offset dynamic arrays
let slotKeys = uniqueFromSlots.map((fromSlot) => {
if (storageSection.offset) {
return bignumber_1.BigNumber.from(storageSection.offset).add(fromSlot);
}
return bignumber_1.BigNumber.from(fromSlot);
});
// Get the contract slot values from the node provider
const values = await (0, exports.getSlotValues)(url, contractAddress, slotKeys, blockTag);
// For each slot value retrieved
values.forEach((value, i) => {
// Get the corresponding slot number for the slot value
const fromSlot = uniqueFromSlots[i];
// For each variable in the storage section
for (const variable of storageSection.variables) {
if (variable.getValue && variable.fromSlot === fromSlot) {
debug(`Set slot value ${value} for section "${storageSection.name}", var type ${variable.type}, slot ${variable.fromSlot} offset ${storageSection.offset}`);
variable.slotValue = value;
// parse variable value from slot data
if (variable.displayValue) {
variable.parsedValue = (0, exports.parseValue)(variable);
}
}
// if variable is past the slot that has the value
else if (variable.toSlot > fromSlot) {
break;
}
}
});
};
exports.addSlotValues = addSlotValues;
const parseValue = (variable) => {
if (!variable.slotValue)
return undefined;
const start = 66 - (variable.byteOffset + variable.byteSize) * 2;
const end = 66 - variable.byteOffset * 2;
const variableValue = variable.slotValue.substring(start, end);
try {
// Contracts, structs and enums
if (variable.attributeType === umlClass_1.AttributeType.UserDefined) {
return parseUserDefinedValue(variable, variableValue);
}
if (variable.attributeType === umlClass_1.AttributeType.Elementary)
return parseElementaryValue(variable, variableValue);
// dynamic arrays
if (variable.attributeType === umlClass_1.AttributeType.Array &&
variable.dynamic) {
return (0, utils_1.formatUnits)('0x' + variableValue, 0);
}
return undefined;
}
catch (err) {
throw Error(`Failed to parse variable ${variable.name} of type ${variable.type}, value "${variableValue}"`, { cause: err });
}
};
exports.parseValue = parseValue;
const parseUserDefinedValue = (variable, variableValue) => {
// TODO need to handle User Defined Value Types introduced in Solidity
// https://docs.soliditylang.org/en/v0.8.18/types.html#user-defined-value-types
// https://blog.soliditylang.org/2021/09/27/user-defined-value-types/
// using byteSize is crude and will be incorrect for aliases types like int160 or uint160
if (variable.byteSize === 20) {
return (0, utils_1.getAddress)('0x' + variableValue);
}
// this will also be wrong if the alias is to a 1 byte type. eg bytes1, int8 or uint8
if (variable.byteSize === 1) {
// assume 1 byte is an enum so convert value to enum index number
const index = bignumber_1.BigNumber.from('0x' + variableValue).toNumber();
// lookup enum value if its available
return variable?.enumValues ? variable?.enumValues[index] : undefined;
}
// we don't parse if a struct which has a size of 32 bytes
return undefined;
};
const parseElementaryValue = (variable, variableValue) => {
// Elementary types
if (variable.type === 'bool') {
if (variableValue === '00')
return 'false';
if (variableValue === '01')
return 'true';
throw Error(`Failed to parse bool variable "${variable.name}" in slot ${variable.fromSlot}, offset ${variable.byteOffset} and slot value "${variableValue}"`);
}
if (variable.type === 'string' || variable.type === 'bytes') {
if (variable.dynamic) {
const lastByte = variable.slotValue.slice(-2);
const size = bignumber_1.BigNumber.from('0x' + lastByte);
// Check if the last bit is set by AND the size with 0x01
if (size.and(1).eq(1)) {
// Return the number of chars or bytes
return bignumber_1.BigNumber.from(variable.slotValue)
.sub(1)
.div(2)
.toString();
}
// The last byte holds the length of the string or bytes in the slot
const valueHex = '0x' + variableValue.slice(0, size.toNumber());
if (variable.type === 'bytes')
return valueHex;
return `\\"${(0, exports.convert2String)(valueHex)}\\"`;
}
if (variable.type === 'bytes')
return '0x' + variableValue;
return `\\"${(0, exports.convert2String)('0x' + variableValue)}\\"`;
}
if (variable.type === 'address') {
return (0, utils_1.getAddress)('0x' + variableValue);
}
if (variable.type.match(/^uint([0-9]*)$/)) {
const parsedValue = (0, utils_1.formatUnits)('0x' + variableValue, 0);
return (0, utils_1.commify)(parsedValue);
}
if (variable.type.match(/^bytes([0-9]+)$/)) {
return '0x' + variableValue;
}
if (variable.type.match(/^int([0-9]*)/)) {
// parse variable value as an unsigned number
let rawValue = bignumber_1.BigNumber.from('0x' + variableValue);
// parse the number of bits
const result = variable.type.match(/^int([0-9]*$)/);
const bitSize = result[1] ? result[1] : 256;
// Convert the number of bits to the number of hex characters
const hexSize = bignumber_1.BigNumber.from(bitSize).div(4).toNumber();
// bit mask has a leading 1 and the rest 0. 0x8 = 1000 binary
const mask = '0x80' + '0'.repeat(hexSize - 2);
// is the first bit a 1?
const negative = rawValue.and(mask);
if (negative.gt(0)) {
// Convert unsigned number to a signed negative
const negativeOne = '0xFF' + 'F'.repeat(hexSize - 2);
rawValue = bignumber_1.BigNumber.from(negativeOne).sub(rawValue).add(1).mul(-1);
}
const parsedValue = (0, utils_1.formatUnits)(rawValue, 0);
return (0, utils_1.commify)(parsedValue);
}
// add fixed point numbers when they are supported by Solidity
return undefined;
};
let jsonRpcId = 0;

@@ -17,38 +187,51 @@ /**

* If proxied, use proxy and not the implementation contract.
* @param slots array of slot numbers to retrieve values for.
* @param slotKeys array of 32 byte slot keys as BigNumbers.
* @param blockTag block number or `latest`
* @return slotValues array of 32 byte slot values as hexadecimal strings
*/
const getStorageValues = async (url, contractAddress, slots, blockTag = 'latest') => {
const getSlotValues = async (url, contractAddress, slotKeys, blockTag = 'latest') => {
try {
debug(`About to get ${slots.length} storage values for ${contractAddress} at block ${blockTag}`);
if (slotKeys.length === 0) {
return [];
}
const block = blockTag === 'latest'
? blockTag
: bignumber_1.BigNumber.from(blockTag).toHexString();
const payload = slots.map((slot) => ({
: (0, utils_1.hexValue)(bignumber_1.BigNumber.from(blockTag));
// get cached values and missing slot keys from from cache
const { cachedValues, missingKeys } = SlotValueCache_1.SlotValueCache.readSlotValues(slotKeys);
// If all values are in the cache then just return the cached values
if (missingKeys.length === 0) {
return cachedValues;
}
debug(`About to get ${slotKeys.length} storage values for ${contractAddress} at block ${blockTag} from slot ${missingKeys[0].toString()}`);
// Get the values for the missing slot keys
const payload = missingKeys.map((key) => ({
id: (jsonRpcId++).toString(),
jsonrpc: '2.0',
method: 'eth_getStorageAt',
params: [
contractAddress,
bignumber_1.BigNumber.from(slot).toHexString(),
block,
],
params: [contractAddress, key, block],
}));
const response = await axios_1.default.post(url, payload);
console.log(response.data);
if (response.data?.error?.message) {
throw new Error(response.data.error.message);
throw Error(response.data.error.message);
}
if (response.data.length !== slots.length) {
throw new Error(`Requested ${slots.length} storage slot values but only got ${response.data.length}`);
if (response.data.length !== missingKeys.length) {
throw Error(`Requested ${missingKeys.length} storage slot values but only got ${response.data.length}`);
}
const responseData = response.data;
const sortedResponses = responseData.sort((a, b) => bignumber_1.BigNumber.from(a.id).gt(b.id) ? 1 : -1);
return sortedResponses.map((data) => '0x' + data.result.toUpperCase().slice(2));
const missingValues = sortedResponses.map((data) => {
if (data.error) {
throw Error(`json rpc call with id ${data.id} failed to get storage values: ${data.error?.message}`);
}
return '0x' + data.result.toUpperCase().slice(2);
});
// add new values to the cache and return the merged slot values
return SlotValueCache_1.SlotValueCache.addSlotValues(slotKeys, missingKeys, missingValues);
}
catch (err) {
throw new Error(`Failed to get ${slots.length} storage values for ${contractAddress} from ${url}`, { cause: err });
throw Error(`Failed to get ${slotKeys.length} storage values for contract ${contractAddress} from ${url}`, { cause: err });
}
};
exports.getStorageValues = getStorageValues;
exports.getSlotValues = getSlotValues;
/**

@@ -59,12 +242,50 @@ * Get storage slot values from JSON-RPC API provider.

* If proxied, use proxy and not the implementation contract.
* @param slot slot number to retrieve the value for.
* @param slotKey 32 byte slot key as a BigNumber.
* @param blockTag block number or `latest`
* @return slotValue 32 byte slot value as hexadecimal string
*/
const getStorageValue = async (url, contractAddress, slot, blockTag = 'latest') => {
debug(`About to get storage slot ${slot} value for ${contractAddress}`);
const values = await (0, exports.getStorageValues)(url, contractAddress, [slot], blockTag);
debug(`Got slot ${slot} value: ${values[0]}`);
const getSlotValue = async (url, contractAddress, slotKey, blockTag) => {
debug(`About to get storage slot ${slotKey} value for ${contractAddress}`);
const values = await (0, exports.getSlotValues)(url, contractAddress, [slotKey], blockTag);
return values[0];
};
exports.getStorageValue = getStorageValue;
exports.getSlotValue = getSlotValue;
/**
* Calculates the number of string characters or bytes of a string or bytes type.
* See the following for how string and bytes are stored in storage slots
* https://docs.soliditylang.org/en/v0.8.17/internals/layout_in_storage.html#bytes-and-string
* @param variable the variable with the slotValue that is being sized
* @return bytes the number of bytes of the dynamic slot. If static, zero is return.
*/
const dynamicSlotSize = (variable) => {
try {
if (!variable?.slotValue)
throw Error(`Missing slot value.`);
const last4bits = '0x' + variable.slotValue.slice(-1);
const last4bitsNum = bignumber_1.BigNumber.from(last4bits).toNumber();
// If the last 4 bits is an even number then it's not a dynamic slot
if (last4bitsNum % 2 === 0)
return 0;
const sizeRaw = bignumber_1.BigNumber.from(variable.slotValue).toNumber();
// Adjust the size to bytes
return (sizeRaw - 1) / 2;
}
catch (err) {
throw Error(`Failed to calculate dynamic slot size for variable "${variable?.name}" of type "${variable?.type}" with slot value ${variable?.slotValue}`, { cause: err });
}
};
exports.dynamicSlotSize = dynamicSlotSize;
const convert2String = (bytes) => {
if (bytes ===
'0x0000000000000000000000000000000000000000000000000000000000000000') {
return '';
}
const rawString = (0, utils_1.toUtf8String)(bytes);
return (0, exports.escapeString)(rawString);
};
exports.convert2String = convert2String;
const escapeString = (text) => {
return text.replace(/(?=[<>&"])/g, '\\');
};
exports.escapeString = escapeString;
//# sourceMappingURL=slotValues.js.map

@@ -16,8 +16,6 @@ #! /usr/bin/env node

const diff_1 = require("./diff");
const slotValues_1 = require("./slotValues");
const ethers_1 = require("ethers");
const clc = require('cli-color');
const program = new commander_1.Command();
const version = (0, path_1.basename)(__dirname) === 'lib'
? require('../package.json').version // used when run from compile js in /lib
: require('../../package.json').version; // used when run from TypeScript source files under src/ts via ts-node
program.version(version);
const debugControl = require('debug');

@@ -46,3 +44,11 @@ const debug = require('debug')('sol2uml');

.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'))
.option('-bc, --backColor <color>', 'Canvas background color. "none" will use a transparent canvas.', 'white')
.option('-sc, --shapeColor <color>', 'Basic drawing color for graphics, not text', 'black')
.option('-fc, --fillColor <color>', 'Color used to fill the background of a node', 'gray95')
.option('-tc, --textColor <color>', 'Color used for text', 'black')
.option('-v, --verbose', 'run with debugging statements', false);
const version = (0, path_1.basename)(__dirname) === 'lib'
? require('../package.json').version // used when run from compile js in /lib
: require('../../package.json').version; // used when run from TypeScript source files under src/ts via ts-node
program.version(version);
program

@@ -131,2 +137,3 @@ .command('class', { isDefault: true })

.option('-bn, --block <number>', 'Block number to get the contract storage values from.', 'latest')
.option('-a, --array <number>', 'Number of slots to display at the start and end of arrays.', '2')
.action(async (fileFolderAddress, options, command) => {

@@ -144,8 +151,8 @@ try {

contractName = combinedOptions.contract || contractName;
const storages = (0, converterClasses2Storage_1.convertClasses2Storages)(contractName, umlClasses, combinedOptions.contractFile);
const arrayItems = parseInt(combinedOptions.array);
const storageSections = (0, converterClasses2Storage_1.convertClasses2StorageSections)(contractName, umlClasses, arrayItems, combinedOptions.contractFile);
if ((0, regEx_1.isAddress)(fileFolderAddress)) {
// The first storage is the contract
storages[0].address = fileFolderAddress;
storageSections[0].address = fileFolderAddress;
}
debug(storages);
if (combinedOptions.data) {

@@ -164,12 +171,20 @@ let storageAddress = combinedOptions.storage;

}
const storage = storages.find((so) => so.name === contractName);
if (!storageAddress)
throw Error(`Could not find the "${contractName}" contract in list of parsed storages`);
await (0, converterClasses2Storage_1.addStorageValues)(combinedOptions.url, storageAddress, storage, combinedOptions.block);
let block = combinedOptions.block;
if (block === 'latest') {
const provider = new ethers_1.ethers.providers.JsonRpcProvider(combinedOptions.url);
block = await provider.getBlockNumber();
debug(`Latest block is ${block}. All storage slot values will be from this block.`);
}
// Get slot values for each storage section
for (const storageSection of storageSections) {
await (0, slotValues_1.addSlotValues)(combinedOptions.url, storageAddress, storageSection, arrayItems, block);
// Add storage variables for dynamic arrays, strings and bytes
await (0, converterClasses2Storage_1.addDynamicVariables)(storageSection, storageSections, combinedOptions.url, storageAddress, arrayItems, block);
}
}
const dotString = (0, converterStorage2Dot_1.convertStorages2Dot)(storages, combinedOptions);
const dotString = (0, converterStorage2Dot_1.convertStorages2Dot)(storageSections, combinedOptions);
await (0, writerFiles_1.writeOutputFiles)(dotString, contractName || 'storageDiagram', combinedOptions.outputFormat, combinedOptions.outputFileName);
}
catch (err) {
console.error(err.stack);
console.error(err);
process.exit(2);

@@ -176,0 +191,0 @@ }

import { UmlClass } from './umlClass';
/**
* Flattens the inheritance hierarchy for each base contract.
* @param umlClasses array of UML classes of type `UMLClass`
* @param umlClasses array of UML classes of type `UMLClass`. The new squashed class is added to this array.
* @param baseContractNames array of contract names to be rendered in squashed format.
* @return squashUmlClasses array of UML classes of type `UMLClass`
* @return squashUmlClasses array of UML classes of type `UMLClass` that are to be rendered
*/
export declare const squashUmlClasses: (umlClasses: UmlClass[], baseContractNames: string[]) => UmlClass[];
export declare const squashUmlClasses: (umlClasses: UmlClass[], baseContractNames: readonly string[]) => UmlClass[];

@@ -32,5 +32,5 @@ "use strict";

* Flattens the inheritance hierarchy for each base contract.
* @param umlClasses array of UML classes of type `UMLClass`
* @param umlClasses array of UML classes of type `UMLClass`. The new squashed class is added to this array.
* @param baseContractNames array of contract names to be rendered in squashed format.
* @return squashUmlClasses array of UML classes of type `UMLClass`
* @return squashUmlClasses array of UML classes of type `UMLClass` that are to be rendered
*/

@@ -37,0 +37,0 @@ const squashUmlClasses = (umlClasses, baseContractNames) => {

@@ -165,5 +165,4 @@ "use strict";

}
console.log(`Generated png file ${pngFilename}`);
}
exports.writePng = writePng;
//# sourceMappingURL=writerFiles.js.map
{
"name": "sol2uml",
"version": "2.4.3",
"version": "2.5.0",
"description": "Solidity contract visualisation tool.",

@@ -5,0 +5,0 @@ "main": "./lib/index.js",

@@ -17,3 +17,3 @@ # Solidity 2 UML

See more contract storage diagram examples [here](./examples/storage/README.md).
See an explanation of how storage diagrams work with lots of examples [here](./examples/storage/README.md).

@@ -58,3 +58,2 @@ # Install

Options:
-V, --version output the version number
-sf, --subfolders <value> number of subfolders that will be recursively searched for Solidity files. (default: all)

@@ -64,5 +63,11 @@ -f, --outputFormat <value> output file format. (choices: "svg", "png", "dot", "all", default: "svg")

-i, --ignoreFilesOrFolders <filesOrFolders> comma separated list of files or folders to ignore
-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)
-k, --apiKey <key> Blockchain explorer API key. eg Etherscan, Arbiscan, BscScan, CronoScan, FTMScan, PolygonScan or SnowTrace API key (env: SCAN_API_KEY)
-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)
-k, --apiKey <key> Blockchain explorer API key. eg Etherscan, Arbiscan, Optimism, BscScan, CronoScan, FTMScan, PolygonScan or SnowTrace API key (env: SCAN_API_KEY)
-bc, --backColor <color> Canvas background color. "none" will use a transparent canvas. (default: "white")
-sc, --shapeColor <color> Basic drawing color for graphics, not text (default: "black")
-fc, --fillColor <color> Color used to fill the background of a node (default: "gray95")
-tc, --textColor <color> Color used for text (default: "black")
-v, --verbose run with debugging statements (default: false)
-V, --version output the version number
-h, --help display help for command

@@ -137,2 +142,3 @@

-bn, --block <number> Block number to get the contract storage values from. (default: "latest")
-a, --array <number> Number of slots to display at the start and end of arrays. (default: "2")
-h, --help display help for command

@@ -279,2 +285,16 @@ ```

# Styling Colors
The colors use by the diagrams can be configured using the `backColor`, `shapeColor`, `fillColor` and `textColor` options.
sol2uml uses the [X11 color scheme](https://graphviz.org/doc/info/colors.html#x11) for named colors.
Other color formats like Red-Green-Blue (RGB) can also be used. For example, #ffffff for white and #000000 for black.
See [Graphviz color](https://graphviz.org/docs/attr-types/color/) documentation for more details.
Here's an example using the color options
```
sol2uml storage -sc deeppink -tc #ffffff -fc dimgrey -bc black 0xfCc00A1e250644d89AF0df661bC6f04891E21585
```
![Aave V3 Pool](./examples/storage/AaveV3PoolStorageColor.svg )
# Version 2.x changes

@@ -281,0 +301,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc