Comparing version 2.4.3 to 2.5.0
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 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
222844
45
4407
328