@solarity/hardhat-migrate
Version:
The simplest way to deploy smart contracts
260 lines (198 loc) • 9.09 kB
text/typescript
import { Interface, isAddress, resolveAddress } from "ethers";
import { Artifact, HardhatRuntimeEnvironment, Libraries } from "hardhat/types";
import { MinimalContract } from "./MinimalContract";
import { MigrateError } from "../errors";
import { catchError } from "../utils";
import { ArtifactExtended, Link, NeededLibrary } from "../types/deployer";
import { Reporter } from "../tools/reporters/Reporter";
import { ArtifactProcessor } from "../tools/storage/ArtifactProcessor";
import { TransactionProcessor } from "../tools/storage/TransactionProcessor";
class LinkerHelper {
constructor(private _hre: HardhatRuntimeEnvironment) {}
public isBytecodeNeedsLinking(bytecode: string): boolean {
return bytecode.indexOf("__") === -1;
}
public async tryLinkBytecode(contractName: string, bytecode: string, libraries: Libraries): Promise<string> {
const artifact: ArtifactExtended = this._mustGetContractArtifact(contractName, bytecode);
const neededLibraries = this._cleanNeededLibraries(bytecode, artifact, artifact.neededLibraries);
let linksToApply: Map<string, Link> = new Map();
for (const [linkedLibraryName, linkedLibraryAddress] of Object.entries(libraries)) {
const neededLibrary = this._mustGetNeededLibrary(neededLibraries, linkedLibraryName, linksToApply);
const neededLibraryFQN = `${neededLibrary.sourceName}:${neededLibrary.libName}`;
linksToApply.set(neededLibraryFQN, <Link>{
sourceName: neededLibrary.sourceName,
libraryName: neededLibrary.libName,
address: await resolveAddress(linkedLibraryAddress),
});
}
if (linksToApply.size < neededLibraries.length) {
const separatelyDeployedLibraries = await this._findMissingLibraries(
neededLibraries.filter((lib) => !linksToApply.has(`${lib.sourceName}:${lib.libName}`)),
);
linksToApply = new Map([...linksToApply.entries(), ...separatelyDeployedLibraries.entries()]);
this._validateLibrariesToLink(linksToApply, neededLibraries);
}
return this._linkBytecode(bytecode, artifact, [...linksToApply.values()]);
}
private _mustGetNeededLibrary(
neededLibraries: NeededLibrary[],
libraryName: string,
linksToApply: Map<string, Link>,
): NeededLibrary {
const matchingNeededLibraries = neededLibraries.filter(
(lib) => lib.libName === libraryName || `${lib.sourceName}:${lib.libName}` === libraryName,
);
return this._validateMatchedNeededLibraries(neededLibraries, matchingNeededLibraries, linksToApply);
}
private _validateMatchedNeededLibraries(
neededLibraries: NeededLibrary[],
matchingNeededLibraries: NeededLibrary[],
linksToApply: Map<string, Link>,
): NeededLibrary {
if (matchingNeededLibraries.length === 0) {
if (neededLibraries.length > 0) {
const libraryFQNames = neededLibraries
.map((lib) => `${lib.sourceName}:${lib.libName}`)
.map((x) => `* ${x}`)
.join("\n");
throw new MigrateError(`The libraries needed are:\n${libraryFQNames}`);
} else {
throw new MigrateError("This contract doesn't need linking any libraries.");
}
}
if (matchingNeededLibraries.length > 1) {
const matchingNeededLibrariesFQNs = matchingNeededLibraries
.map(({ sourceName, libName }) => `${sourceName}:${libName}`)
.map((x) => `* ${x}`)
.join("\n");
throw new MigrateError(
`The library name is ambiguous.\nIt may resolve to one of the following libraries:\n${matchingNeededLibrariesFQNs}\n\nTo fix this, choose one of these fully qualified library names and replace where appropriate.`,
);
}
const neededLibrary = matchingNeededLibraries[0];
const neededLibraryFQN = `${neededLibrary.sourceName}:${neededLibrary.libName}`;
if (linksToApply.has(neededLibraryFQN)) {
throw new MigrateError(
`The library names ${neededLibrary.libName} and ${neededLibraryFQN} refer to the same library and were given as two separate library links.\nRemove one of them and review your library links before proceeding.`,
);
}
return neededLibrary;
}
private async _findMissingLibraries(
missingLibraries: { sourceName: string; libName: string }[],
): Promise<Map<string, Link>> {
const missingLibrariesMap: Map<string, Link> = new Map();
for (const missingLibrary of missingLibraries) {
const lib = `${missingLibrary.sourceName}:${missingLibrary.libName}`;
const address = await this._getOrDeployLibrary(lib);
if (isAddress(address)) {
missingLibrariesMap.set(lib, {
sourceName: missingLibrary.sourceName,
libraryName: missingLibrary.libName,
address: address,
});
}
}
return missingLibrariesMap;
}
private _validateLibrariesToLink(linksToApply: Map<string, Link>, neededLibraries: NeededLibrary[]): void {
if (linksToApply.size < neededLibraries.length) {
const missingLibraries = neededLibraries
.map((lib) => `${lib.sourceName}:${lib.libName}`)
.filter((libFQName) => !linksToApply.has(libFQName))
.map((x) => `* ${x}`)
.join("\n");
throw new MigrateError(`The contract is missing links for the following libraries:\n${missingLibraries}`);
}
}
private _cleanNeededLibraries(bytecode: string, artifact: Artifact, libraries: NeededLibrary[]): NeededLibrary[] {
const actuallyNeededLibs: Map<string, NeededLibrary> = new Map();
for (const { sourceName, libName } of libraries) {
const linkReferences = artifact.linkReferences[sourceName][libName];
for (const { start, length } of linkReferences) {
if (!this._isLinkedLibrary(bytecode, start, length)) {
actuallyNeededLibs.set(`${sourceName}:${libName}`, { sourceName, libName });
}
}
}
return [...actuallyNeededLibs.values()];
}
/**
* The address of the linked library can be extracted like this: bytecode.slice(prefixLength + 2, suffixStart + 2)
*/
private _isLinkedLibrary(bytecode: string, start: number, length: number): boolean {
const prefixLength = start * 2;
const prefix = bytecode.slice(prefixLength + 2, prefixLength + 5);
const suffixStart = (start + length) * 2;
const suffix = bytecode.slice(suffixStart - 1, suffixStart + 2);
return `${prefix}${suffix}` !== "__$$__";
}
private _linkBytecode(bytecode: string, artifact: Artifact, libraries: Link[]): string {
for (const { sourceName, libraryName, address } of libraries) {
const linkReferences = artifact.linkReferences[sourceName][libraryName];
for (const { start, length } of linkReferences) {
const prefixLength = 2 + start * 2;
const prefix = bytecode.slice(0, prefixLength);
const suffixStart = 2 + (start + length) * 2;
const suffix = bytecode.slice(suffixStart);
bytecode = prefix + address.slice(2) + suffix;
}
}
return bytecode;
}
private async _getOrDeployLibrary(libraryName: string) {
try {
return (await TransactionProcessor?.tryRestoreContractAddressByName(libraryName))!;
} catch {
const artifact = this._mustGetLibraryArtifact(libraryName);
// https://github.com/ethers-io/ethers.js/issues/2431
// https://github.com/ethers-io/ethers.js/issues/1126
const core = new MinimalContract(this._hre, artifact.bytecode, new Interface(artifact.abi), libraryName);
Reporter!.notifyDeploymentOfMissingLibrary(libraryName);
return core.deploy();
}
}
private _mustGetContractArtifact(contractName: string, contractBytecode: string): ArtifactExtended {
const artifact = this._getContractFromStorage(contractName, contractBytecode);
if (!artifact) {
throw new MigrateError(`Contract artifact of ${contractName} not found. Linking cannot be performed.`);
}
return artifact;
}
private _mustGetLibraryArtifact(libraryName: string): ArtifactExtended {
const artifact = this._getContractFromStorage(libraryName);
if (!artifact) {
throw new MigrateError(`Library artifact of ${libraryName} not found. Linking cannot be performed.`);
}
return artifact;
}
private _getContractFromStorage(contractName: string, contractBytecode?: string): ArtifactExtended | null {
try {
return ArtifactProcessor.tryGetArtifactByName(contractName);
} catch {
/* ignore */
}
try {
const txData = TransactionProcessor?.tryRestoreSavedDeployedTxByContractName(contractName);
return ArtifactProcessor.tryGetArtifactByName(txData!.metadata.fullyQualifiedContractName!);
} catch {
/* ignore */
}
if (contractBytecode) {
try {
return ArtifactProcessor.tryGetArtifactByBytecode(contractBytecode);
} catch {
/* ignore */
}
}
return null;
}
}
export let Linker: LinkerHelper | null = null;
export function createLinker(hre: HardhatRuntimeEnvironment) {
if (Linker) {
return;
}
Linker = new LinkerHelper(hre);
}