sol2uml
Version:
Solidity contract visualisation tool.
825 lines • 40.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.addDynamicVariables = exports.findDimensionLength = exports.calcSectionOffset = exports.isElementary = exports.calcStorageByteSize = exports.parseStorageSectionFromAttribute = exports.optionStorageVariables = exports.convertClasses2StorageSections = exports.StorageSectionType = void 0;
const umlClass_1 = require("./umlClass");
const associations_1 = require("./associations");
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 StorageSectionType;
(function (StorageSectionType) {
StorageSectionType["Contract"] = "Contract";
StorageSectionType["Struct"] = "Struct";
StorageSectionType["Array"] = "Array";
StorageSectionType["Bytes"] = "Bytes";
StorageSectionType["String"] = "String";
})(StorageSectionType || (exports.StorageSectionType = StorageSectionType = {}));
let storageId = 1;
let variableId = 1;
/**
*
* @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 storageSections array of storageSection objects
*/
const convertClasses2StorageSections = (contractName, umlClasses, arrayItems, contractFilename, noExpandVariables = []) => {
// Find the base UML Class from the base contract name
const umlClass = umlClasses.find(({ name, relativePath }) => {
if (!contractFilename) {
return name === contractName;
}
return (name === contractName &&
(relativePath == path_1.default.normalize(contractFilename) ||
path_1.default.basename(relativePath) ===
path_1.default.normalize(contractFilename)));
});
if (!umlClass) {
const contractFilenameError = contractFilename
? ` in filename "${contractFilename}"`
: '';
throw Error(`Failed to find contract with name "${contractName}"${contractFilenameError}.\nIs the \`-c --contract <name>\` option correct?`);
}
debug(`Found contract "${contractName}" in ${umlClass.absolutePath}`);
const storageSections = [];
const variables = parseVariables(umlClass, umlClasses, [], storageSections, [], false, arrayItems, noExpandVariables);
// Add new storage section to the beginning of the array
storageSections.unshift({
id: storageId++,
name: contractName,
type: StorageSectionType.Contract,
variables: variables,
mapping: false,
});
adjustSlots(storageSections[0], 0, storageSections);
return storageSections;
};
exports.convertClasses2StorageSections = convertClasses2StorageSections;
const optionStorageVariables = (contractName, slotNames, slotTypes) => {
// If no slot names
if (!slotNames?.length) {
return [];
}
// The slotTypes default should mean this never happens
if (!slotTypes.length) {
throw Error(`The slotTypes option must be used with the slotNames option`);
}
if (slotNames.length > 1 && slotTypes.length === 1) {
slotTypes = Array(slotNames.length).fill(slotTypes[0]);
// slotTypes = slotTypes.fill(slotTypes[0], 1, slotNames.length - 1)
}
const variables = [];
slotNames.forEach((slotName, i) => {
const { size: byteSize, dynamic } = calcElementaryTypeSize(slotTypes[i]);
variables.push({
id: variableId++,
fromSlot: undefined,
toSlot: undefined,
offset: slotName.offset,
byteSize,
byteOffset: 0,
type: slotTypes[i],
attributeType: umlClass_1.AttributeType.Elementary,
dynamic,
getValue: true,
displayValue: true,
name: slotName.name,
contractName,
referenceSectionId: undefined,
enumValues: undefined,
});
});
// Sort variables by offset hash
const sortedVariables = variables.sort((a, b) => {
if (a.offset < b.offset) {
return -1;
}
if (a.offset > b.offset) {
return 1;
}
return 0;
});
return sortedVariables;
};
exports.optionStorageVariables = optionStorageVariables;
/**
* 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 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, storageSections, inheritedContracts, mapping, arrayItems, noExpandVariables) => {
// Add storage slots from inherited contracts first.
// Get immediate parent contracts that the class inherits from
const parentContracts = umlClass.getParentContracts();
// Filter out any already inherited contracts
const newInheritedContracts = parentContracts.filter((parentContract) => !inheritedContracts.includes(parentContract.targetUmlClassName));
// Mutate inheritedContracts to include the new inherited contracts
inheritedContracts.push(...newInheritedContracts.map((c) => c.targetUmlClassName));
// Recursively parse each new inherited contract
newInheritedContracts.forEach((parent) => {
const parentClass = (0, associations_1.findAssociatedClass)(parent, umlClass, umlClasses);
if (!parentClass) {
throw Error(`Failed to find inherited contract "${parent.targetUmlClassName}" sourced from "${umlClass.name}" with path "${umlClass.absolutePath}"`);
}
// recursively parse inherited contract
parseVariables(parentClass, umlClasses, variables, storageSections, inheritedContracts, mapping, arrayItems, noExpandVariables);
});
// Parse storage for each attribute
umlClass.attributes.forEach((attribute) => {
// Ignore any attributes that are constants or immutable
if (attribute.compiled)
return;
const { size: byteSize, dynamic } = (0, exports.calcStorageByteSize)(attribute, umlClass, umlClasses);
// parse any dependent storage sections or enums
const references = noExpandVariables.includes(attribute.name)
? undefined
: (0, exports.parseStorageSectionFromAttribute)(attribute, umlClass, umlClasses, storageSections, mapping || attribute.attributeType === umlClass_1.AttributeType.Mapping, arrayItems, noExpandVariables);
// 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
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;
fromSlot = nextFromSlot;
toSlot = nextFromSlot + Math.floor((byteSize - 1) / 32);
byteOffset = 0;
}
else {
fromSlot = lastToSlot;
toSlot = lastToSlot;
byteOffset = nextOffset;
}
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 (variable.attributeType === umlClass_1.AttributeType.Array) {
// attribute is a dynamic array
referenceStorageSection.offset = (0, exports.calcSectionOffset)(variable, storageSection.offset);
adjustSlots(referenceStorageSection, 0, storageSections);
}
}
});
};
/**
* 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, noExpandVariables) => {
if (attribute.attributeType === umlClass_1.AttributeType.Array) {
// storage is dynamic if the attribute type ends in []
const result = attribute.type.match(/\[([\w$.]*)]$/);
const dynamic = result[1] === '';
const arrayLength = !dynamic
? (0, exports.findDimensionLength)(umlClass, result[1], otherClasses)
: undefined;
// get the type of the array items. eg
// address[][4][2] will have base type address[][4]
const baseType = attribute.type.substring(0, attribute.type.lastIndexOf('['));
let baseAttributeType;
if ((0, exports.isElementary)(baseType)) {
baseAttributeType = umlClass_1.AttributeType.Elementary;
}
else if (baseType[baseType.length - 1] === ']') {
baseAttributeType = umlClass_1.AttributeType.Array;
}
else {
baseAttributeType = umlClass_1.AttributeType.UserDefined;
}
const baseAttribute = {
visibility: attribute.visibility,
name: attribute.name,
type: baseType,
attributeType: baseAttributeType,
};
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, noExpandVariables);
}
const displayValue = calcDisplayValue(baseAttribute.attributeType, dynamicBase, mapping, references?.storageSection?.type);
const getValue = calcGetValue(attribute.attributeType, mapping);
const variables = [];
variables[0] = {
id: variableId++,
fromSlot: 0,
toSlot: Math.floor((arraySlotSize - 1) / 32),
byteSize: arrayItemSize,
byteOffset: 0,
type: baseType,
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) {
// 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, noExpandVariables);
variable.referenceSectionId = references?.storageSection?.id;
variable.enumValues = references?.enumValues;
}
});
}
const storageSection = {
id: storageId++,
name: `${attribute.type}: ${attribute.name}`,
type: StorageSectionType.Array,
arrayDynamic: dynamic,
arrayLength,
variables,
mapping,
};
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 typeClass = findTypeClass(attribute.type, attribute, umlClass, otherClasses);
if (typeClass.stereotype === umlClass_1.ClassStereotype.Struct) {
const variables = parseVariables(typeClass, otherClasses, [], storageSections, [], mapping, arrayItems, noExpandVariables);
const storageSection = {
id: storageId++,
name: attribute.type,
type: StorageSectionType.Struct,
variables,
mapping,
};
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;
}
if (attribute.attributeType === umlClass_1.AttributeType.Mapping) {
// get the UserDefined type from the mapping
// note the mapping could be an array of Structs
// Could also be a mapping of a mapping
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 can be a contract, struct or enum
const typeClass = findTypeClass(result[1], attribute, umlClass, otherClasses);
if (typeClass.stereotype === umlClass_1.ClassStereotype.Struct) {
let variables = parseVariables(typeClass, otherClasses, [], storageSections, [], true, arrayItems, noExpandVariables);
const storageSection = {
id: storageId++,
name: typeClass.name,
type: StorageSectionType.Struct,
mapping: true,
variables,
};
storageSections.push(storageSection);
return { storageSection };
}
}
return undefined;
}
return undefined;
};
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 umlClass the attribute is part of.
* @param otherClasses
*/
const findTypeClass = (userType, attribute, umlClass, otherClasses) => {
// Find associated UserDefined type
const types = userType.split('.');
const association = {
referenceType: umlClass_1.ReferenceType.Memory,
targetUmlClassName: types.length === 1 ? types[0] : types[1],
parentUmlClassName: types.length === 1 ? undefined : types[0],
};
const typeClass = (0, associations_1.findAssociatedClass)(association, umlClass, otherClasses);
if (!typeClass) {
throw Error(`Failed to find user defined type "${userType}" in attribute "${attribute.name}" of from class "${umlClass.name}" with path "${umlClass.absolutePath}"`);
}
return typeClass;
};
// Calculates the storage size of an attribute in bytes
const calcStorageByteSize = (attribute, umlClass, otherClasses) => {
if (attribute.attributeType === umlClass_1.AttributeType.Mapping ||
attribute.attributeType === umlClass_1.AttributeType.Function) {
return { size: 32, dynamic: true };
}
if (attribute.attributeType === umlClass_1.AttributeType.Array) {
// Fixed sized arrays are read from right to left until there is a dynamic dimension
// eg address[][3][2] is a fixed size array that uses 6 slots.
// while address [2][] is a dynamic sized array.
const arrayDimensions = attribute.type.match(/\[[\w$.]*]/g);
// Remove first [ and last ] from each arrayDimensions
const dimensionsStr = arrayDimensions.map((a) => a.slice(1, -1));
// fixed-sized arrays are read from right to left so reverse the dimensions
const dimensionsStrReversed = dimensionsStr.reverse();
// read fixed-size dimensions until we get a dynamic array with no dimension
let dimension = dimensionsStrReversed.shift();
const fixedDimensions = [];
while (dimension && dimension !== '') {
const dimensionNum = (0, exports.findDimensionLength)(umlClass, dimension, otherClasses);
fixedDimensions.push(dimensionNum);
// read the next dimension for the next loop
dimension = dimensionsStrReversed.shift();
}
// If the first dimension is dynamic, ie []
if (fixedDimensions.length === 0) {
// dynamic arrays start at the keccak256 of the slot number
// the array length is stored in the 32 byte slot
return { size: 32, dynamic: true };
}
// If a fixed sized array
let elementSize;
const type = attribute.type.substring(0, attribute.type.indexOf('['));
if ((0, exports.isElementary)(type)) {
const elementAttribute = {
attributeType: umlClass_1.AttributeType.Elementary,
type,
name: 'element',
};
({ size: elementSize } = (0, exports.calcStorageByteSize)(elementAttribute, umlClass, otherClasses));
}
else {
const elementAttribute = {
attributeType: umlClass_1.AttributeType.UserDefined,
type,
name: 'userDefined',
};
({ size: elementSize } = (0, exports.calcStorageByteSize)(elementAttribute, umlClass, otherClasses));
}
// Anything over 16 bytes, like an address, will take a whole 32 byte slot
if (elementSize > 16 && elementSize < 32) {
elementSize = 32;
}
// If multi dimension, then the first element is 32 bytes
if (fixedDimensions.length < arrayDimensions.length) {
const totalDimensions = fixedDimensions.reduce((total, dimension) => total * dimension, 1);
return {
size: 32 * totalDimensions,
dynamic: false,
};
}
const lastItem = fixedDimensions.length - 1;
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;
const remainingDimensions = fixedDimensions
.slice(0, lastItem)
.reduce((total, dimension) => total * dimension, 1);
return {
size: lastDimensionSlotBytes * remainingDimensions,
dynamic: false,
};
}
// 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 attributeTypeClass = findTypeClass(attribute.type, attribute, umlClass, otherClasses);
switch (attributeTypeClass.stereotype) {
case umlClass_1.ClassStereotype.Enum:
return { size: 1, dynamic: false };
case umlClass_1.ClassStereotype.Contract:
case umlClass_1.ClassStereotype.Abstract:
case umlClass_1.ClassStereotype.Interface:
case umlClass_1.ClassStereotype.Library:
return { size: 20, dynamic: false };
case umlClass_1.ClassStereotype.Struct:
let structByteSize = 0;
attributeTypeClass.attributes.forEach((structAttribute) => {
// If next attribute is an array, then we need to start in a new slot
if (structAttribute.attributeType === umlClass_1.AttributeType.Array) {
structByteSize = Math.ceil(structByteSize / 32) * 32;
}
// If next attribute is an struct, then we need to start in a new slot
else if (structAttribute.attributeType ===
umlClass_1.AttributeType.UserDefined) {
// UserDefined types can be a struct or enum, so we need to check if it's a struct
const userDefinedClass = findTypeClass(structAttribute.type, structAttribute, umlClass, otherClasses);
// If a struct
if (userDefinedClass.stereotype ===
umlClass_1.ClassStereotype.Struct) {
structByteSize = Math.ceil(structByteSize / 32) * 32;
}
}
const { size: attributeSize } = (0, exports.calcStorageByteSize)(structAttribute, umlClass, otherClasses);
// check if attribute will fit into the remaining slot
const endCurrentSlot = Math.ceil(structByteSize / 32) * 32;
const spaceLeftInSlot = endCurrentSlot - structByteSize;
if (attributeSize <= spaceLeftInSlot) {
structByteSize += attributeSize;
}
else {
structByteSize = endCurrentSlot + attributeSize;
}
});
// structs take whole 32 byte slots so round up to the nearest 32 sized slots
return {
size: Math.ceil(structByteSize / 32) * 32,
dynamic: false,
};
default:
return { size: 20, dynamic: false };
}
}
if (attribute.attributeType === umlClass_1.AttributeType.Elementary) {
return calcElementaryTypeSize(attribute.type);
}
throw new Error(`Failed to calc bytes size of attribute with name "${attribute.name}" and type ${attribute.type}`);
};
exports.calcStorageByteSize = calcStorageByteSize;
const calcElementaryTypeSize = (type) => {
switch (type) {
case 'bool':
return { size: 1, dynamic: false };
case 'address':
return { size: 20, dynamic: false };
case 'string':
case 'bytes':
return { size: 32, dynamic: true };
case 'uint':
case 'int':
case 'ufixed':
case 'fixed':
return { size: 32, dynamic: false };
default:
const result = type.match(/[u]*(int|fixed|bytes)([0-9]+)/);
if (result === null || !result[2]) {
throw Error(`Failed size elementary type "${type}"`);
}
// If bytes
if (result[1] === 'bytes') {
return { size: parseInt(result[2]), dynamic: false };
}
// TODO need to handle fixed types when they are supported
// If an int
const bitSize = parseInt(result[2]);
return { size: bitSize / 8, dynamic: false };
}
};
const isElementary = (type) => {
switch (type) {
case 'bool':
case 'address':
case 'string':
case 'bytes':
case 'uint':
case 'int':
case 'ufixed':
case 'fixed':
return true;
default:
const result = type.match(/^[u]?(int|fixed|bytes)([0-9]+)$/);
return result !== null;
}
};
exports.isElementary = isElementary;
const calcSectionOffset = (variable, sectionOffset = '0') => {
if (variable.dynamic) {
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).add(sectionOffset).toHexString();
};
exports.calcSectionOffset = calcSectionOffset;
const findDimensionLength = (umlClass, dimension, otherClasses) => {
const dimensionNum = parseInt(dimension);
if (Number.isInteger(dimensionNum)) {
return dimensionNum;
}
// 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