UNPKG

@truffle/codec

Version:

Library for encoding and decoding smart contract data

583 lines 24.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.findRepeatCompilationIds = exports.infoToCompilations = exports.findCompilationAndContract = exports.collectUserDefinedTypesAndTaggedOutputs = exports.simpleShimSourceMap = exports.getContractNode = exports.shimContracts = exports.shimArtifacts = exports.shimCompilation = exports.shimCompilations = void 0; const debug_1 = __importDefault(require("debug")); const debug = (0, debug_1.default)("codec:compilations:utils"); const Ast = __importStar(require("../ast")); const compile_common_1 = require("@truffle/compile-common"); const Format = __importStar(require("../format")); const errors_1 = require("../errors"); function shimCompilations(inputCompilations, shimmedCompilationIdPrefix = "shimmedcompilation") { return inputCompilations.map((compilation, compilationIndex) => shimCompilation(compilation, `${shimmedCompilationIdPrefix}Number(${compilationIndex})`)); } exports.shimCompilations = shimCompilations; function shimCompilation(inputCompilation, shimmedCompilationId = "shimmedcompilation") { return Object.assign(Object.assign({}, shimContracts(inputCompilation.contracts, { files: inputCompilation.sourceIndexes, sources: inputCompilation.sources, shimmedCompilationId, compiler: inputCompilation.compiler })), { compiler: inputCompilation.compiler }); } exports.shimCompilation = shimCompilation; /** * wrapper around shimContracts that just returns * the result in a one-element array (keeping the old name * shimArtifacts for compatibility) */ function shimArtifacts(artifacts, files, shimmedCompilationId = "shimmedcompilation") { return [shimContracts(artifacts, { files, shimmedCompilationId })]; } exports.shimArtifacts = shimArtifacts; /** * shims a bunch of contracts ("artifacts", though not necessarily) * to a compilation. usually used via one of the above functions. * Note: if you pass in options.sources, options.files will be ignored. * Note: if you pass in options.sources, sources will not have * compiler set unless you also pass in options.compiler; in this case * you should set that up separately, as in shimCompilation(). */ function shimContracts(artifacts, options = {}) { const { files, sources: inputSources } = options; const shimmedCompilationId = options.shimmedCompilationId || "shimmedcompilation"; let contracts = []; let sources = []; let unreliableSourceOrder = false; for (let artifact of artifacts) { let { contractName, bytecode, sourceMap, deployedBytecode, deployedSourceMap, immutableReferences, sourcePath, source, ast, abi, compiler, generatedSources, deployedGeneratedSources, metadata } = artifact; if (artifact.contract_name) { //just in case contractName = artifact.contract_name; //dunno what's up w/ the type of contract_name, but it needs coercing } debug("contractName: %s", contractName); let contractObject = { contractName, bytecode, sourceMap, deployedBytecode, deployedSourceMap, immutableReferences, abi, generatedSources: normalizeGeneratedSources(generatedSources, compiler), deployedGeneratedSources: normalizeGeneratedSources(deployedGeneratedSources, compiler), compiler }; let sourceObject = { sourcePath, source, ast: ast, compiler, language: inferLanguage(ast, compiler, sourcePath) }; //ast needs to be coerced because schema doesn't quite match our types here... if (metadata) { try { const parsedMetadata = JSON.parse(metadata); //sorry const settings = parsedMetadata.settings; const viaIR = settings.viaIR; contractObject.settings = { viaIR }; sourceObject.settings = { viaIR }; } catch (_a) { //if metadata doesn't parse, or we hit undefineds, ignore it } } //if files or sources was passed, trust that to determine the source index //(assuming we have a sourcePath! currently it will be absent when dealing with //Solidity versions <0.4.9; presumably we will fix this if we ever properly //support versions that old, but for now this is necessary to get debug -x to work) if ((files || inputSources) && sourcePath) { //note: we never set the unreliableSourceOrder flag in this branch; //we just trust files/sources. If this info is bad, then, uh, too bad. debug("inputSources: %O", inputSources); debug("files: %O", files); debug("sourcePath: %O", sourcePath); const index = inputSources ? inputSources.findIndex(source => source.sourcePath === sourcePath) : files.indexOf(sourcePath); if (!inputSources) { //if inputSources was passed, we'll handle this separately below sourceObject.id = index.toString(); //HACK sources[index] = sourceObject; } debug("files || inputSources; index: %d", index); contractObject.primarySourceId = index.toString(); //HACK } else { //if neither was passed, attempt to determine it from the ast let index; let needsAdding; if (sourceObject.ast) { //note: this works for both Solidity and Vyper index = sourceIndexForAst(sourceObject.ast); //sourceObject.ast for typing reasons } else if (compiler && compiler.name === "vyper") { index = 0; //if it's Vyper but there's no AST, we can //assume that it was compiled alone and therefore has index 0 } //if that didn't work, try the source map if (index === undefined && (sourceMap || deployedSourceMap)) { const sourceMapString = simpleShimSourceMap(deployedSourceMap || sourceMap); index = extractPrimarySource(sourceMapString); } //else leave undefined for now ({ index, needsAdding, unreliableSourceOrder } = getIndexToAddAt(sourceObject, index, sources, unreliableSourceOrder)); if (needsAdding) { //if we're in this case, inputSources was not passed sourceObject.id = index.toString(); //HACK sources[index] = sourceObject; debug("else; index: %d", index); } //whether needed adding or not, set the source ID on the contract object contractObject.primarySourceId = index.toString(); //HACK debug("(no index unless mentioned)"); } contracts.push(contractObject); } //now: check for id overlap with internal sources //(don't bother if inputSources or files was passed) if (!inputSources && !files) { for (let contract of contracts) { const { generatedSources, deployedGeneratedSources } = contract; for (let index in generatedSources) { if (index in sources) { unreliableSourceOrder = true; } } for (let index in deployedGeneratedSources) { if (index in sources) { unreliableSourceOrder = true; } } } } let compiler; if (options.compiler) { compiler = options.compiler; } else if (!unreliableSourceOrder && contracts.length > 0) { //if things were actually compiled together, we should just be able //to pick an arbitrary one compiler = contracts[0].compiler; } let settings; //we'll do the same thing with settings if (options.settings) { settings = options.settings; } else if (!unreliableSourceOrder && contracts.length > 0) { //if things were actually compiled together, we should just be able //to pick an arbitrary one settings = contracts[0].settings; } //if input sources was passed, set up the sources object directly :) if (inputSources) { sources = inputSources.map(({ sourcePath, contents: source, ast, language }, index) => ({ sourcePath, source, ast: ast, language, id: index.toString(), compiler //redundant but let's include it })); } return { id: shimmedCompilationId, unreliableSourceOrder, sources, contracts, compiler, settings }; } exports.shimContracts = shimContracts; //note: this works for Vyper too! function sourceIndexForAst(ast) { if (Array.isArray(ast)) { //special handling for old Vyper versions ast = ast[0]; } if (!ast) { return undefined; } if (ast.nodeType === "YulObject") { //Yul needs some special handling... ast = ast.code.block; } return parseInt(ast.src.split(":")[2]); //src is given as start:length:file. //we want just the file. } function getContractNode(contract, compilation) { const { contractName, sourceMap, deployedSourceMap, primarySourceId } = contract; const { unreliableSourceOrder, sources } = compilation; let sourcesToCheck; //we will attempt to locate the primary source; //if we can't find it, we'll just check every source in this //compilation. if (primarySourceId !== undefined) { sourcesToCheck = [ sources.find(source => source && source.id === primarySourceId) ]; } else if (!unreliableSourceOrder && (deployedSourceMap || sourceMap)) { const sourceMapString = simpleShimSourceMap(deployedSourceMap || sourceMap); let sourceId = extractPrimarySource(sourceMapString); sourcesToCheck = [sources[sourceId]]; } else { //WARNING: if we end up in this case, we could get the wrong contract! //(but we shouldn't end up here) sourcesToCheck = sources; } return sourcesToCheck.reduce((foundNode, source) => { if (foundNode || !source) { return foundNode; } if (!source.ast || source.language !== "Solidity") { //ignore non-Solidity ASTs for now, we don't support them yet return undefined; } return source.ast.nodes.find(node => node.nodeType === "ContractDefinition" && node.name === contractName); }, undefined); } exports.getContractNode = getContractNode; /** * extract the primary source from a source map * (i.e., the source for the first instruction, found * between the second and third colons) */ function extractPrimarySource(sourceMap) { if (!sourceMap) { //HACK? return 0; //in this case (e.g. a Vyper contract with an old-style //source map) we infer that it was compiled by itself } return parseInt(sourceMap.match(/^[^:]*:[^:]*:([^:]*):/)[1] || "0"); } function normalizeGeneratedSources(generatedSources, compiler) { if (!generatedSources) { return []; } if (!isGeneratedSources(generatedSources)) { return generatedSources; //if already normalizeed, leave alone } let sources = []; //output for (let source of generatedSources) { sources[source.id] = { id: source.id.toString(), sourcePath: source.name, source: source.contents, //ast needs to be coerced because schema doesn't quite match our types here... ast: source.ast, compiler: compiler, language: source.language }; } return sources; } //HACK function isGeneratedSources(sources) { //note: for some reason arr.includes(undefined) returns true on sparse arrays //if sources.length === 0, it's ambiguous; we'll exclude it as not needing normalization return (sources.length > 0 && !sources.includes(undefined) && (sources[0].contents !== undefined || sources[0].name !== undefined)); } //HACK, maybe? function inferLanguage(ast, compiler, sourcePath) { if (ast) { if (ast.nodeType === "SourceUnit") { return "Solidity"; } else if (ast.nodeType && ast.nodeType.startsWith("Yul")) { //Every Yul source I've seen has YulBlock as the root, but //I'm not sure that that's *always* the case return "Yul"; } else if (Array.isArray(ast) || ast.ast_type === "Module") { return "Vyper"; } } else if (compiler) { if (compiler.name === "vyper") { return "Vyper"; } else if (compiler.name === "solc") { //assuming sources compiled with solc without sourcePath are Solidity if (sourcePath && sourcePath.endsWith(".yul")) { return "Yul"; } else { return "Solidity"; } } else { return undefined; } } else { return undefined; } } function getIndexToAddAt(sourceObject, index, sources, unreliableSourceOrder) { debug("sourcePath: %s", sourceObject.sourcePath); debug("given index: %d", index); debug("sources: %o", sources.map(source => source.sourcePath)); //first: is this already there? only add it if it's not. //(we determine this by sourcePath if present, and the actual source //contents if not) const existingIndex = sources.findIndex(existingSource => existingSource && //findIndex treats absent as undefined, so we need this guard // (array may be sparse) (existingSource.sourcePath === sourceObject.sourcePath || (!sourceObject.sourcePath && !existingSource.sourcePath && existingSource.source === sourceObject.source))); if (existingIndex === -1) { //it's not already there, let's add it if (unreliableSourceOrder || index === undefined || index in sources) { //if we can't add it at the correct spot, set the //unreliable source order flag debug("collision!"); unreliableSourceOrder = true; } if (unreliableSourceOrder) { //in case of unreliable source order, we'll ignore what indices //things are *supposed* to have and just append things to the end index = sources.length; } //otherwise, just leave things alone return { index, needsAdding: true, unreliableSourceOrder }; } else { debug("already present, not adding"); return { index: existingIndex, needsAdding: false, unreliableSourceOrder }; } } /** * convert Vyper source maps to solidity ones * (note we won't bother handling the case where the compressed * version doesn't exist; that will have to wait for a later version) */ function simpleShimSourceMap(sourceMap) { if (sourceMap === undefined) { return undefined; //undefined case } else if (typeof sourceMap === "object") { return sourceMap.pc_pos_map_compressed; //Vyper object case } else { try { return JSON.parse(sourceMap).pc_pos_map_compressed; //Vyper JSON case } catch (_) { return sourceMap; //Solidity case } } } exports.simpleShimSourceMap = simpleShimSourceMap; /** * collects user defined types & tagged outputs for a given set of compilations, * returning both the definition nodes and (for the types) the type objects * * "Tagged outputs" means user-defined things that are output by a contract * (not input to a contract), and which are distinguished by (potentially * ambiguous) selectors. So, events and custom errors are tagged outputs. * Function arguments are not tagged outputs (they're not outputs). * Return values are not tagged outputs (they don't have a selector). * Built-in errors (Error(string) and Panic(uint))... OK I guess those could * be considered tagged outputs, but we're only looking at user-defined ones * here. */ function collectUserDefinedTypesAndTaggedOutputs(compilations) { let references = {}; let types = {}; for (const compilation of compilations) { references[compilation.id] = {}; types[compilation.id] = { compiler: compilation.compiler, types: {} }; for (const source of compilation.sources) { if (!source) { continue; //remember, sources could be empty if shimmed! } const { ast, compiler, language } = source; if (language === "Solidity" && ast) { //don't check Yul or Vyper sources! for (const node of ast.nodes) { if (node.nodeType === "StructDefinition" || node.nodeType === "EnumDefinition" || node.nodeType === "UserDefinedValueTypeDefinition" || node.nodeType === "ContractDefinition") { references[compilation.id][node.id] = node; //we don't have all the references yet, but we actually don't need them :) const dataType = Ast.Import.definitionToStoredType(node, compilation.id, compiler, references[compilation.id]); types[compilation.id].types[dataType.id] = dataType; } else if (node.nodeType === "EventDefinition" || node.nodeType === "ErrorDefinition") { references[compilation.id][node.id] = node; } if (node.nodeType === "ContractDefinition") { for (const subNode of node.nodes) { if (subNode.nodeType === "StructDefinition" || subNode.nodeType === "EnumDefinition" || subNode.nodeType === "UserDefinedValueTypeDefinition") { references[compilation.id][subNode.id] = subNode; //we don't have all the references yet, but we only need the //reference to the defining contract, which we just added above! const dataType = Ast.Import.definitionToStoredType(subNode, compilation.id, compiler, references[compilation.id]); types[compilation.id].types[dataType.id] = dataType; } else if (subNode.nodeType === "EventDefinition" || subNode.nodeType === "ErrorDefinition") { references[compilation.id][subNode.id] = subNode; } } } } } } } return { definitions: references, typesByCompilation: types, types: Format.Types.forgetCompilations(types) }; } exports.collectUserDefinedTypesAndTaggedOutputs = collectUserDefinedTypesAndTaggedOutputs; /** * Given a list of compilations, and an artifact appearing in one * of those compilations, finds the compilation and the corresponding * contract object * (these may be undefined if they can't be found) */ function findCompilationAndContract(compilations, artifact) { const deployedBytecode = compile_common_1.Shims.NewToLegacy.forBytecode(artifact.deployedBytecode); const bytecode = compile_common_1.Shims.NewToLegacy.forBytecode(artifact.bytecode); let firstNameMatch; let multipleNameMatches = false; for (const compilation of compilations) { for (const contract of compilation.contracts) { const nameMatches = contract.contractName === (artifact.contractName || artifact.contract_name); if (nameMatches) { if (bytecode) { if (compile_common_1.Shims.NewToLegacy.forBytecode(contract.bytecode) === bytecode) { return { compilation, contract }; } } else if (deployedBytecode) { if (compile_common_1.Shims.NewToLegacy.forBytecode(contract.deployedBytecode) === deployedBytecode) { return { compilation, contract }; } } else if (!firstNameMatch) { //if we have a name match, but no bytecode to go by, record this one. //if it turns out to be the only one, we'll return it later. firstNameMatch = { compilation, contract }; } else if (!multipleNameMatches) { //on the other hand, if there *is* an existing name match already, //record that we've got multiple. multipleNameMatches = true; } } } } //once the loop is done, if we haven't returned a bytecode match, //check if we've got a unique name match, and return it if so if (firstNameMatch && !multipleNameMatches) { return firstNameMatch; } //otherwise, if there's no bytecode match, and either no name match //or multiple name matches, just return a default fallback const defaultContract = { contractName: artifact.contractName || artifact.contract_name, abi: artifact.abi }; const defaultCompilation = { id: "defaultCompilation", sources: [], contracts: [defaultContract] }; return { compilation: defaultCompilation, contract: defaultContract }; } exports.findCompilationAndContract = findCompilationAndContract; function projectInfoIsCodecStyle(info) { return Boolean(info.compilations); } function projectInfoIsCommonStyle(info) { return Boolean(info.commonCompilations); } function projectInfoIsArtifacts(info) { return Boolean(info.artifacts); } function infoToCompilations(projectInfo, nonceString) { if (!projectInfo) { throw new errors_1.NoProjectInfoError(); } if (projectInfoIsCodecStyle(projectInfo)) { return projectInfo.compilations; } else if (projectInfoIsCommonStyle(projectInfo)) { return shimCompilations(projectInfo.commonCompilations, nonceString); } else if (projectInfoIsArtifacts(projectInfo)) { return shimArtifacts(projectInfo.artifacts, undefined, nonceString); } } exports.infoToCompilations = infoToCompilations; function findRepeatCompilationIds(compilations) { let repeats = new Set(); for (let i = 0; i < compilations.length; i++) { for (let j = i + 1; j < compilations.length; j++) { if (compilations[i].id === compilations[j].id) { repeats.add(compilations[i].id); } } } return repeats; } exports.findRepeatCompilationIds = findRepeatCompilationIds; //# sourceMappingURL=utils.js.map