hardhat-deploy
Version:
Hardhat Plugin For Replicable Deployments And Tests
1,540 lines (1,424 loc) • 48.1 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
DeployFunction,
Deployment,
DeploymentsExtension,
FixtureFunc,
DeploymentSubmission,
Export,
DeterministicDeploymentInfo,
} from '../types';
import {ExtendedArtifact} from '../types';
import {PartialExtension} from './internal/types';
import fs from 'fs-extra';
import path from 'path';
import {BigNumber} from '@ethersproject/bignumber';
import debug from 'debug';
const log = debug('hardhat:wighawag:hardhat-deploy');
import {
addDeployments,
processNamedAccounts,
loadAllDeployments,
traverseMultipleDirectory,
deleteDeployments,
getExtendedArtifactFromFolders,
getArtifactFromFolders,
getNetworkName,
getDeployPaths,
} from './utils';
import {addHelpers, waitForTx} from './helpers';
import {TransactionResponse} from '@ethersproject/providers';
import {Artifact, HardhatRuntimeEnvironment, Network} from 'hardhat/types';
import {store} from './globalStore';
import {bnReplacer} from './internal/utils';
export class DeploymentsManager {
public deploymentsExtension: DeploymentsExtension;
private db: {
gasUsed: BigNumber;
accountsLoaded: boolean;
namedAccounts: {[name: string]: string};
unnamedAccounts: string[];
deploymentsLoaded: boolean;
deployments: Record<string, Deployment>;
writeDeploymentsToFiles: boolean;
fixtureCounter: number;
snapshotCounter: number;
pastFixtures: {
[name: string]: {
index: number;
data?: any;
blockHash: string;
snapshot: any;
deployments: any;
};
};
logEnabled: boolean;
pendingTransactions: {[hash: string]: any};
savePendingTx: boolean;
gasPrice?: string;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
migrations: {[id: string]: number};
onlyArtifacts?: string[];
runAsNode: boolean;
};
private env: HardhatRuntimeEnvironment;
private deploymentsPath: string;
public impersonateUnknownAccounts: boolean;
public impersonatedAccounts: string[];
public addressesToProtocol: {[address: string]: string} = {};
private network: Network;
private partialExtension: PartialExtension;
private utils: {
dealWithPendingTransactions: (
pendingTxs: {
[txHash: string]: {
name: string;
deployment?: any;
rawTx: string;
decoded: {
from: string;
gasPrice?: string;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
gasLimit: string;
to: string;
value: string;
nonce: number;
data: string;
r: string;
s: string;
v: number;
// creates: tx.creates, // TODO test
chainId: number;
};
};
},
pendingTxPath: string,
globalGasPrice: string | undefined
) => Promise<void>;
};
constructor(env: HardhatRuntimeEnvironment, network: Network) {
log('constructing DeploymentsManager');
this.network = network;
this.impersonateUnknownAccounts = true;
this.impersonatedAccounts = [];
this.db = {
gasUsed: BigNumber.from(0),
accountsLoaded: false,
namedAccounts: {},
unnamedAccounts: [],
deploymentsLoaded: false,
deployments: {},
migrations: {},
writeDeploymentsToFiles: true, // should default to true ? so we can run scripts that use `deploy` by default
fixtureCounter: 0,
snapshotCounter: 0,
pastFixtures: {},
logEnabled: process.env['HARDHAT_DEPLOY_LOG'] ? true : false,
pendingTransactions: {},
savePendingTx: false,
gasPrice: undefined,
runAsNode: false,
};
this.env = env;
this.deploymentsPath = env.config.paths.deployments;
// TODO
// this.env.artifacts = new HardhatDeployArtifacts(this.env.artifacts);
this.partialExtension = {
readDotFile: async (name: string): Promise<string> =>
this.readDotFile(name),
saveDotFile: async (name: string, content: string): Promise<void> =>
this.saveDotFile(name, content),
deleteDotFile: async (name: string): Promise<void> =>
this.deleteDotFile(name),
save: async (
name: string,
deployment: DeploymentSubmission
): Promise<void> => {
await this.saveDeployment(name, deployment);
},
delete: async (name: string): Promise<void> =>
this.deleteDeployment(name),
get: async (name: string) => {
await this.setup(false);
const deployment = this.db.deployments[name];
if (deployment === undefined) {
throw new Error(`No deployment found for: ${name}`);
}
return deployment;
},
getOrNull: async (name: string) => {
await this.setup(false);
return this.db.deployments[name];
},
getDeploymentsFromAddress: async (address: string) => {
const deployments: Deployment[] = [];
for (const deployment of Object.values(this.db.deployments)) {
if (deployment.address === address) {
deployments.push(deployment);
}
}
return deployments;
},
all: async () => {
await this.setup(false);
return this.db.deployments; // TODO copy
},
getArtifact: async (contractName: string): Promise<Artifact> => {
if (this.db.onlyArtifacts) {
const artifactFromFolder = await getArtifactFromFolders(
contractName,
this.db.onlyArtifacts
);
if (!artifactFromFolder) {
throw new Error(
`cannot find artifact "${contractName}" from folder ${this.db.onlyArtifacts}`
);
}
return artifactFromFolder as Artifact;
}
let artifact: Artifact | ExtendedArtifact | undefined =
await getArtifactFromFolders(contractName, [
this.env.config.paths.artifacts,
]);
if (artifact) {
return artifact as Artifact;
}
const importPaths = this.getImportPaths();
artifact = await getArtifactFromFolders(contractName, importPaths);
if (!artifact) {
throw new Error(`cannot find artifact "${contractName}"`);
}
return artifact as Artifact;
},
getExtendedArtifact: async (
contractName: string
): Promise<ExtendedArtifact> => {
if (this.db.onlyArtifacts) {
const artifactFromFolder = await getExtendedArtifactFromFolders(
contractName,
this.db.onlyArtifacts
);
if (!artifactFromFolder) {
throw new Error(
`cannot find artifact "${contractName}" from folder ${this.db.onlyArtifacts}`
);
}
return artifactFromFolder as ExtendedArtifact;
}
let artifact: ExtendedArtifact | undefined =
await getExtendedArtifactFromFolders(contractName, [
this.env.config.paths.artifacts,
]);
if (artifact) {
return artifact;
}
const importPaths = this.getImportPaths();
artifact = await getExtendedArtifactFromFolders(
contractName,
importPaths
);
if (artifact) {
return artifact;
}
if (!artifact) {
throw new Error(`cannot find artifact "${contractName}"`);
}
return artifact;
},
run: (
tags?: string | string[],
options: {
resetMemory?: boolean;
deletePreviousDeployments?: boolean;
writeDeploymentsToFiles?: boolean;
export?: string;
exportAll?: string;
} = {
resetMemory: true,
writeDeploymentsToFiles: false,
deletePreviousDeployments: false,
}
) => {
return this.runDeploy(tags, {
resetMemory:
options.resetMemory === undefined ? true : options.resetMemory,
deletePreviousDeployments:
options.deletePreviousDeployments === undefined
? false
: options.deletePreviousDeployments,
writeDeploymentsToFiles:
options.writeDeploymentsToFiles === undefined
? false
: options.writeDeploymentsToFiles,
export: options.export,
exportAll: options.exportAll,
log: false,
savePendingTx: false,
});
},
fixture: async (
tags?: string | string[],
options?: {
fallbackToGlobal?: boolean;
keepExistingDeployments?: boolean;
}
) => {
await this.setup(tags === undefined);
options = {fallbackToGlobal: true, ...options};
if (typeof tags === 'string') {
tags = [tags];
}
const globalKey = '::global';
const globalFixture = this.db.pastFixtures[globalKey];
let fixtureKey = globalKey;
if (tags !== undefined) {
fixtureKey = '::' + tags.join('.');
}
if (this.db.pastFixtures[fixtureKey]) {
const pastFixture = this.db.pastFixtures[fixtureKey];
const success = await this.revertSnapshot(pastFixture);
if (success) {
return this.db.deployments;
} else {
delete this.db.pastFixtures[fixtureKey];
}
}
if (globalFixture && options.fallbackToGlobal) {
const success = await this.revertSnapshot(globalFixture);
if (success) {
return this.db.deployments;
} else {
delete this.db.pastFixtures[globalKey];
}
}
await this.runDeploy(tags, {
resetMemory: !options.keepExistingDeployments,
writeDeploymentsToFiles: false,
deletePreviousDeployments: false,
log: false,
savePendingTx: false,
});
await this.saveSnapshot(fixtureKey);
return this.db.deployments;
},
createFixture: <T, O>(func: FixtureFunc<T, O>) => {
const baseId = '' + ++this.db.fixtureCounter + '::';
return async (options?: O) => {
let id = baseId;
if (options !== undefined) {
id = id + JSON.stringify(options, bnReplacer);
}
const saved = this.db.pastFixtures[id];
if (saved) {
const success = await this.revertSnapshot(saved);
if (success) {
return saved.data;
}
}
const data = await func(this.env, options);
await this.saveSnapshot(id, data);
return data;
};
},
log: (...args: any[]) => {
if (this.db.logEnabled) {
console.log(...args);
}
},
getNetworkName: () => this.getNetworkName(),
getGasUsed: () => this.db.gasUsed.toNumber(),
} as PartialExtension;
const print = (msg: string) => {
if (this.db.logEnabled) {
process.stdout.write(msg);
}
};
log('adding helpers');
const helpers = addHelpers(
this,
this.partialExtension,
this.network,
this.partialExtension.getArtifact,
async (
name: string,
deployment: DeploymentSubmission,
artifactName?: string
): Promise<void> => {
if (
artifactName &&
this.db.writeDeploymentsToFiles &&
this.network.saveDeployments
) {
// toSave (see deployments.save function)
const extendedArtifact =
await this.partialExtension.getExtendedArtifact(artifactName);
deployment = {
...deployment,
...extendedArtifact,
};
}
await this.partialExtension.save(name, deployment);
},
() => {
return this.db.writeDeploymentsToFiles && this.network.saveDeployments;
},
this.onPendingTx.bind(this),
async () => {
// TODO extraGasPrice ?
let gasPrice: BigNumber | undefined;
let maxFeePerGas: BigNumber | undefined;
let maxPriorityFeePerGas: BigNumber | undefined;
if (this.db.gasPrice) {
gasPrice = BigNumber.from(this.db.gasPrice);
} else {
if (this.db.maxFeePerGas) {
maxFeePerGas = BigNumber.from(this.db.maxFeePerGas);
}
if (this.db.maxPriorityFeePerGas) {
maxPriorityFeePerGas = BigNumber.from(this.db.maxPriorityFeePerGas);
}
}
return {gasPrice, maxFeePerGas, maxPriorityFeePerGas};
},
this.partialExtension.log,
print
);
this.deploymentsExtension = helpers.extension;
this.utils = helpers.utils;
}
private networkWasSetup = false;
public setupNetwork(): void {
if (this.networkWasSetup) {
return;
}
// reassign network variables based on fork name if any;
const networkName = this.getNetworkName();
if (networkName !== this.network.name) {
const networkObject = store.networks[networkName];
if (networkObject) {
this.env.network.live = networkObject.live;
this.env.network.tags = networkObject.tags;
this.env.network.deploy = networkObject.deploy;
}
}
this.networkWasSetup = true;
}
private _chainId: string | undefined;
public async getChainId(): Promise<string> {
if (this._chainId) {
return this._chainId;
}
this.setupNetwork();
try {
this._chainId = await this.network.provider.send('eth_chainId');
} catch (e) {
console.log('failed to get chainId, falling back on net_version...');
this._chainId = await this.network.provider.send('net_version');
}
if (!this._chainId) {
throw new Error(`could not get chainId from network`);
}
if (this._chainId.startsWith('0x')) {
this._chainId = BigNumber.from(this._chainId).toString();
}
return this._chainId;
}
public runAsNode(enabled: boolean): void {
this.db.runAsNode = enabled;
}
public async dealWithPendingTransactions(): Promise<void> {
let pendingTxs: {
[txHash: string]: {
name: string;
deployment?: any;
rawTx: string;
decoded: {
from: string;
gasPrice?: string;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
gasLimit: string;
to: string;
value: string;
nonce: number;
data: string;
r: string;
s: string;
v: number;
// creates: tx.creates, // TODO test
chainId: number;
};
};
} = {};
const pendingTxPath = path.join(
this.deploymentsPath,
this.deploymentFolder(),
'.pendingTransactions'
);
try {
pendingTxs = JSON.parse(fs.readFileSync(pendingTxPath).toString());
} catch (e) {}
await this.utils.dealWithPendingTransactions(
pendingTxs,
pendingTxPath,
this.db.gasPrice
);
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public async onPendingTx(
tx: TransactionResponse,
name?: string,
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
deployment?: any
): Promise<TransactionResponse> {
if (
this.db.writeDeploymentsToFiles &&
this.network.saveDeployments &&
this.db.savePendingTx
) {
const deployFolderPath = path.join(
this.deploymentsPath,
this.deploymentFolder()
);
// console.log("tx", tx.hash);
const pendingTxPath = path.join(deployFolderPath, '.pendingTransactions');
fs.ensureDirSync(deployFolderPath);
const rawTx = tx.raw;
const decoded = tx.raw
? undefined
: {
from: tx.from,
gasPrice: tx.gasPrice?.toString(),
maxFeePerGas: tx.maxFeePerGas?.toString(),
maxPriorityFeePerGas: tx.maxPriorityFeePerGas?.toString(),
gasLimit: tx.gasLimit.toString(),
to: tx.to,
value: tx.value.toString(),
nonce: tx.nonce,
data: tx.data,
r: tx.r,
s: tx.s,
v: tx.v,
// creates: tx.creates, // TODO test
chainId: tx.chainId,
};
this.db.pendingTransactions[tx.hash] = name
? {name, deployment, rawTx, decoded}
: {rawTx, decoded};
fs.writeFileSync(
pendingTxPath,
JSON.stringify(this.db.pendingTransactions, bnReplacer, ' ')
);
// await new Promise(r => setTimeout(r, 20000));
const wait = tx.wait.bind(tx);
tx.wait = async (confirmations?: number) => {
const receipt = await wait(confirmations);
// console.log("checking pending tx...");
delete this.db.pendingTransactions[tx.hash];
if (Object.keys(this.db.pendingTransactions).length === 0) {
fs.removeSync(pendingTxPath);
} else {
fs.writeFileSync(
pendingTxPath,
JSON.stringify(this.db.pendingTransactions, bnReplacer, ' ')
);
}
this.db.gasUsed = this.db.gasUsed.add(receipt.gasUsed);
return receipt;
};
} else {
const wait = tx.wait.bind(tx);
tx.wait = async (confirmations?: number) => {
const receipt = await wait(confirmations);
this.db.gasUsed = this.db.gasUsed.add(receipt.gasUsed);
return receipt;
};
}
return tx;
}
public async getNamedAccounts(): Promise<{[name: string]: string}> {
await this.setupAccounts();
return this.db.namedAccounts;
}
public async getUnnamedAccounts(): Promise<string[]> {
await this.setupAccounts();
return this.db.unnamedAccounts;
}
private async getDeterminisityDeploymentInfo(): Promise<
DeterministicDeploymentInfo | undefined
> {
const chainId = await this.getChainId();
const config = this.env.config.deterministicDeployment;
return typeof config == 'function' ? config(chainId) : config?.[chainId];
}
public async getDeterministicDeploymentFactoryAddress(): Promise<string> {
const info = await this.getDeterminisityDeploymentInfo();
return info?.factory || '0x4e59b44847b379578588920ca78fbf26c0b4956c';
}
public async getDeterministicDeploymentFactoryDeployer(): Promise<string> {
const info = await this.getDeterminisityDeploymentInfo();
return info?.deployer || '0x3fab184622dc19b6109349b94811493bf2a45362';
}
public async getDeterministicDeploymentFactoryFunding(): Promise<BigNumber> {
const info = await this.getDeterminisityDeploymentInfo();
return BigNumber.from(info?.funding || '10000000000000000');
}
public async getDeterministicDeploymentFactoryDeploymentTx(): Promise<string> {
const info = await this.getDeterminisityDeploymentInfo();
return (
info?.signedTx ||
'0xf8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222'
);
}
public async loadDeployments(
chainIdExpected = true
): Promise<{[name: string]: Deployment}> {
let chainId: string | undefined;
if (chainIdExpected) {
chainId = await this.getChainId();
}
let migrations = {};
try {
log('loading migrations');
migrations = JSON.parse(
fs
.readFileSync(
path.join(
this.deploymentsPath,
this.deploymentFolder(),
'.migrations.json'
)
)
.toString()
);
} catch (e) {}
this.db.migrations = migrations;
// console.log({ migrations: this.db.migrations });
const networkName = this.getDeploymentNetworkName();
addDeployments(
this.db,
this.deploymentsPath,
this.deploymentFolder(),
networkName === this.network.name ? chainId : undefined // fork mode, we do not care about chainId ?
);
const extraDeploymentPaths =
this.env.config.external &&
this.env.config.external.deployments &&
this.env.config.external.deployments[networkName];
if (extraDeploymentPaths) {
for (const deploymentFolderPath of extraDeploymentPaths) {
addDeployments(this.db, deploymentFolderPath, '', undefined, chainId);
}
}
this.db.deploymentsLoaded = true;
return this.db.deployments;
}
public async deletePreviousDeployments(folderPath?: string): Promise<void> {
folderPath = folderPath || this.deploymentFolder();
deleteDeployments(this.deploymentsPath, folderPath);
}
public getSolcInputPath(): string {
return path.join(
this.deploymentsPath,
this.deploymentFolder(),
'solcInputs'
);
}
public async deleteDotFile(name: string): Promise<void> {
const toSave =
this.db.writeDeploymentsToFiles && this.network.saveDeployments;
if (toSave) {
// do not delete if not save mode
const deployFolderpath = path.join(
this.deploymentsPath,
this.deploymentFolder()
);
const filepath = path.join(deployFolderpath, name);
try {
fs.unlinkSync(filepath);
} catch (e) {}
}
}
public async readDotFile(name: string): Promise<string> {
if (!name.startsWith('.')) {
throw new Error(
`file to save need to start with a dot to ensure it is not considered a deployment`
);
}
const deployFolderpath = path.join(
this.deploymentsPath,
this.deploymentFolder()
);
const filepath = path.join(deployFolderpath, name);
return fs.readFileSync(filepath).toString();
}
public async saveDotFile(name: string, content: string): Promise<void> {
if (!name.startsWith('.')) {
throw new Error(
`file to save need to start with a dot to ensure it is not considered a deployment`
);
}
const toSave =
this.db.writeDeploymentsToFiles && this.network.saveDeployments;
if (toSave) {
const chainId = await this.getChainId();
const deployFolderpath = path.join(
this.deploymentsPath,
this.deploymentFolder()
);
const filepath = path.join(deployFolderpath, name);
fs.ensureDirSync(deployFolderpath);
const chainIdFilepath = path.join(deployFolderpath, '.chainId');
if (!fs.existsSync(chainIdFilepath)) {
fs.writeFileSync(chainIdFilepath, chainId);
}
const folderPath = path.dirname(filepath);
fs.ensureDirSync(folderPath);
fs.writeFileSync(filepath, content);
}
}
public async deleteDeployment(name: string): Promise<void> {
delete this.db.deployments[name];
const toSave =
this.db.writeDeploymentsToFiles && this.network.saveDeployments;
if (toSave) {
// do not delete if not save mode
const filepath = path.join(
this.deploymentsPath,
this.deploymentFolder(),
name + '.json'
);
try {
fs.unlinkSync(filepath);
} catch (e) {}
}
}
public async saveDeployment(
name: string,
deployment: DeploymentSubmission
): Promise<boolean> {
if (name.includes('/') || name.includes(':')) {
throw new Error(
`deployment name must not be a path or Fully Qualified Name - for such purposes consider using the "contract" field of deployment options`
);
}
if (
typeof deployment.address === undefined &&
!deployment.receipt?.contractAddress
) {
throw new Error(
'deployment need a receipt with contractAddress or an address'
);
}
if (typeof deployment.abi === undefined) {
throw new Error('deployment need an ABI');
}
if (name.includes('/') || name.includes(':')) {
throw new Error(
`deployment name must not be a path or Fully Qualified Name - for such purposes consider using the "contract" field of deployment options`
);
}
const chainId = await this.getChainId();
const toSave =
this.db.writeDeploymentsToFiles && this.network.saveDeployments;
const filepath = path.join(
this.deploymentsPath,
this.deploymentFolder(),
name + '.json'
);
// handle ethers receipt :
const receipt = deployment.receipt;
const actualReceipt = receipt
? {
to: receipt.to,
from: receipt.from,
contractAddress: receipt.contractAddress,
transactionIndex: receipt.transactionIndex,
gasUsed:
receipt.gasUsed && (receipt.gasUsed as BigNumber)._isBigNumber
? receipt.gasUsed.toString()
: receipt.gasUsed,
logsBloom: receipt.logsBloom,
blockHash: receipt.blockHash,
transactionHash: receipt.transactionHash,
logs: receipt.logs,
events: receipt.events,
blockNumber: receipt.blockNumber,
cumulativeGasUsed:
receipt.cumulativeGasUsed &&
(receipt.cumulativeGasUsed as BigNumber)._isBigNumber
? receipt.cumulativeGasUsed.toString()
: receipt.cumulativeGasUsed,
status: receipt.status,
byzantium: receipt.byzantium,
}
: undefined;
// from : https://stackoverflow.com/a/14810722/1663971
function objectMap(object: any, mapFn: (obj: any) => any) {
return Object.keys(object).reduce(function (result: any, key) {
result[key] = mapFn(object[key]);
return result;
}, {});
}
// TODO can cause infinite loop
function transform(v: any): any {
if (v._isBigNumber) {
return v.toString();
}
if (Array.isArray(v)) {
return v.map(transform);
}
if (typeof v === 'object') {
return objectMap(v, transform);
}
return v;
}
const actualArgs = deployment.args?.map(transform);
let numDeployments = 1;
const oldDeployment = this.db.deployments[name]
? {...this.db.deployments[name]}
: undefined;
if (oldDeployment) {
numDeployments = (oldDeployment.numDeployments || 1) + 1;
if (!deployment.history) {
delete oldDeployment.history;
}
}
const obj = JSON.parse(
JSON.stringify(
{
address: deployment.address || actualReceipt?.contractAddress,
abi: deployment.abi,
transactionHash:
deployment.transactionHash || actualReceipt?.transactionHash,
receipt: actualReceipt,
args: actualArgs,
numDeployments,
linkedData: deployment.linkedData,
solcInputHash: deployment.solcInputHash,
metadata: deployment.metadata,
bytecode: deployment.bytecode,
deployedBytecode: deployment.deployedBytecode,
libraries: deployment.libraries,
facets: deployment.facets,
execute: deployment.execute,
history: deployment.history,
implementation: deployment.implementation,
devdoc: deployment.devdoc,
userdoc: deployment.userdoc,
storageLayout: deployment.storageLayout,
methodIdentifiers: deployment.methodIdentifiers,
gasEstimates: deployment.gasEstimates, // TODO double check : use evm field ?
},
bnReplacer
)
);
if (deployment.factoryDeps?.length) {
obj.factoryDeps = deployment.factoryDeps;
}
this.db.deployments[name] = obj;
if (obj.address === undefined && obj.transactionHash !== undefined) {
let receiptFetched;
try {
receiptFetched = await waitForTx(
this.network.provider,
obj.transactionHash,
true
);
// TODO add receipt ?
obj.address = receiptFetched.contractAddress;
if (!obj.address) {
throw new Error('no contractAddress in receipt');
}
} catch (e) {
console.error(e);
if (toSave) {
console.log('deleting ' + filepath);
fs.unlinkSync(filepath);
}
delete this.db.deployments[name];
return false; // TODO throw error ?
}
}
this.db.deployments[name] = obj;
// console.log({chainId, typeOfChainId: typeof chainId});
if (toSave) {
// console.log("writing " + filepath); // TODO remove
try {
fs.mkdirSync(this.deploymentsPath);
} catch (e) {}
const deployFolderpath = path.join(
this.deploymentsPath,
this.deploymentFolder()
);
try {
fs.mkdirSync(deployFolderpath);
} catch (e) {}
const chainIdFilepath = path.join(deployFolderpath, '.chainId');
if (!fs.existsSync(chainIdFilepath)) {
fs.writeFileSync(chainIdFilepath, chainId);
}
fs.writeFileSync(filepath, JSON.stringify(obj, bnReplacer, ' '));
if (deployment.solcInputHash && deployment.solcInput) {
const solcInputsFolderpath = path.join(
this.deploymentsPath,
this.deploymentFolder(),
'solcInputs'
);
const solcInputFilepath = path.join(
solcInputsFolderpath,
deployment.solcInputHash + '.json'
);
if (!fs.existsSync(solcInputFilepath)) {
try {
fs.mkdirSync(solcInputsFolderpath);
} catch (e) {}
fs.writeFileSync(solcInputFilepath, deployment.solcInput);
}
}
}
// this.spreadEvents();
return true;
}
private companionManagers: {[name: string]: DeploymentsManager} = {};
public addCompanionManager(
name: string,
networkDeploymentsManager: DeploymentsManager
): void {
this.companionManagers[name] = networkDeploymentsManager;
}
public async runDeploy(
tags?: string | string[],
options: {
deletePreviousDeployments: boolean;
log: boolean;
resetMemory: boolean;
writeDeploymentsToFiles: boolean;
savePendingTx: boolean;
export?: string;
exportAll?: string;
gasPrice?: string;
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
tagsRequireAll?: boolean;
} = {
log: false,
resetMemory: true,
deletePreviousDeployments: false,
writeDeploymentsToFiles: true,
savePendingTx: false,
}
): Promise<{[name: string]: Deployment}> {
log('runDeploy');
this.setupNetwork();
if (options.deletePreviousDeployments) {
log('deleting previous deployments');
this.db.deployments = {};
this.db.migrations = {};
await this.deletePreviousDeployments();
for (const companionNetworkName of Object.keys(this.companionManagers)) {
const companionManager = this.companionManagers[companionNetworkName];
companionManager.deletePreviousDeployments();
}
}
await this.loadDeployments();
this.db.gasUsed = BigNumber.from(0);
this.db.writeDeploymentsToFiles = options.writeDeploymentsToFiles;
this.db.savePendingTx = options.savePendingTx;
this.db.logEnabled = options.log;
this.db.gasPrice = options.gasPrice;
this.db.maxFeePerGas = options.maxFeePerGas;
this.db.maxPriorityFeePerGas = options.maxPriorityFeePerGas;
if (options.resetMemory) {
log('reseting memory');
this.db.deployments = {};
this.db.migrations = {};
}
if (!options.deletePreviousDeployments && options.savePendingTx) {
await this.dealWithPendingTransactions(); // TODO deal with reset ?
}
for (const companionNetworkName of Object.keys(this.companionManagers)) {
const companionManager = this.companionManagers[companionNetworkName];
await companionManager.loadDeployments();
companionManager.db.writeDeploymentsToFiles =
options.writeDeploymentsToFiles;
companionManager.db.savePendingTx = options.savePendingTx;
companionManager.db.logEnabled = options.log;
// companionManager.db.gasPrice = options.gasPrice;
if (options.resetMemory) {
log('reseting memory');
companionManager.db.deployments = {};
companionManager.db.migrations = {};
}
if (!options.deletePreviousDeployments && options.savePendingTx) {
await companionManager.dealWithPendingTransactions(); // TODO deal with reset ?
}
}
if (tags !== undefined && typeof tags === 'string') {
tags = [tags];
}
if (this.env.config.external?.contracts) {
for (const externalContracts of this.env.config.external.contracts) {
if (externalContracts.deploy) {
this.db.onlyArtifacts = externalContracts.artifacts;
try {
await this.executeDeployScripts([externalContracts.deploy], tags, options.tagsRequireAll);
} finally {
this.db.onlyArtifacts = undefined;
}
}
}
}
const deployPaths = getDeployPaths(this.network);
await this.executeDeployScripts(deployPaths, tags, options.tagsRequireAll);
await this.export(options);
return this.db.deployments;
}
public async executeDeployScripts(
deployScriptsPaths: string[],
tags: string[] = [],
tagsRequireAll = false,
): Promise<void> {
const wasWrittingToFiles = this.db.writeDeploymentsToFiles;
// TODO loop over companion networks ?
// This is currently posing problem for network like optimism which require a different set of artifact and hardhat currently only expose one set at a time
let filepaths;
try {
filepaths = traverseMultipleDirectory(deployScriptsPaths);
} catch (e) {
return;
}
filepaths = filepaths.sort((a: string, b: string) => {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
});
log('deploy script folder parsed');
const funcByFilePath: {[filename: string]: DeployFunction} = {};
const scriptPathBags: {[tag: string]: string[]} = {};
const scriptFilePaths: string[] = [];
for (const filepath of filepaths) {
const scriptFilePath = path.resolve(filepath);
let deployFunc: DeployFunction;
// console.log("fetching " + scriptFilePath);
try {
delete require.cache[scriptFilePath]; // ensure we reload it every time, so changes are taken in consideration
deployFunc = require(scriptFilePath);
if ((deployFunc as any).default) {
deployFunc = (deployFunc as any).default as DeployFunction;
}
funcByFilePath[scriptFilePath] = deployFunc;
} catch (e) {
// console.error("require failed", e);
throw new Error(
'ERROR processing skip func of ' +
filepath +
':\n' +
((e as any).stack || e)
);
}
// console.log("get tags if any for " + scriptFilePath);
let scriptTags = deployFunc.tags || [];
if (typeof scriptTags === 'string') {
scriptTags = [scriptTags];
}
for (const tag of scriptTags) {
if (tag.indexOf(',') >= 0) {
throw new Error('Tag cannot contain commas');
}
const bag = scriptPathBags[tag] || [];
scriptPathBags[tag] = bag;
bag.push(scriptFilePath);
}
// console.log("tags found " + scriptFilePath, scriptTags);
if (tagsRequireAll && tags.every(tag => scriptTags.includes(tag))
|| !tagsRequireAll && (tags.length == 0 || tags.some(tag => scriptTags.includes(tag)))) {
scriptFilePaths.push(scriptFilePath);
}
}
log('tag collected');
// console.log({ scriptFilePaths });
const scriptsRegisteredToRun: {[filename: string]: boolean} = {};
const scriptsToRun: Array<{
func: DeployFunction;
filePath: string;
}> = [];
const scriptsToRunAtTheEnd: Array<{
func: DeployFunction;
filePath: string;
}> = [];
function recurseDependencies(scriptFilePath: string) {
if (scriptsRegisteredToRun[scriptFilePath]) {
return;
}
const deployFunc = funcByFilePath[scriptFilePath];
if (deployFunc.dependencies) {
for (const dependency of deployFunc.dependencies) {
const scriptFilePathsToAdd = scriptPathBags[dependency];
if (scriptFilePathsToAdd) {
for (const scriptFilenameToAdd of scriptFilePathsToAdd) {
recurseDependencies(scriptFilenameToAdd);
}
}
}
}
if (!scriptsRegisteredToRun[scriptFilePath]) {
if (deployFunc.runAtTheEnd) {
scriptsToRunAtTheEnd.push({
filePath: scriptFilePath,
func: deployFunc,
});
} else {
scriptsToRun.push({
filePath: scriptFilePath,
func: deployFunc,
});
}
scriptsRegisteredToRun[scriptFilePath] = true;
}
}
for (const scriptFilePath of scriptFilePaths) {
recurseDependencies(scriptFilePath);
}
log('dependencies collected');
try {
for (const deployScript of scriptsToRun.concat(scriptsToRunAtTheEnd)) {
const filename = path.basename(deployScript.filePath);
if (deployScript.func.id && this.db.migrations[deployScript.func.id]) {
log(
`skipping ${filename} as migrations already executed and complete`
);
continue;
}
let skip = false;
if (deployScript.func.skip) {
log(`should we skip ${deployScript.filePath} ?`);
try {
skip = await deployScript.func.skip(this.env);
} catch (e) {
// console.error("skip failed", e);
throw new Error(
'ERROR processing skip func of ' +
deployScript.filePath +
':\n' +
((e as any).stack || e)
);
}
log(`checking skip for ${deployScript.filePath} complete`);
}
if (!skip) {
log(`executing ${deployScript.filePath}`);
let result;
try {
result = await deployScript.func(this.env);
} catch (e) {
// console.error("execution failed", e);
throw new Error(
'ERROR processing ' +
deployScript.filePath +
':\n' +
((e as any).stack || e)
);
}
log(`executing ${deployScript.filePath} complete`);
if (result && typeof result === 'boolean') {
if (!deployScript.func.id) {
throw new Error(
`${deployScript.filePath} return true to not be executed again, but does not provide an id. the script function needs to have the field "id" to be set`
);
}
this.db.migrations[deployScript.func.id] = Math.floor(
Date.now() / 1000
);
const deploymentFolderPath = this.deploymentFolder();
// TODO refactor to extract this whole path and folder existence stuff
const toSave =
this.db.writeDeploymentsToFiles && this.network.saveDeployments;
if (toSave) {
try {
fs.mkdirSync(this.deploymentsPath);
} catch (e) {}
try {
fs.mkdirSync(
path.join(this.deploymentsPath, deploymentFolderPath)
);
} catch (e) {}
fs.writeFileSync(
path.join(
this.deploymentsPath,
deploymentFolderPath,
'.migrations.json'
),
JSON.stringify(this.db.migrations, bnReplacer, ' ')
);
}
}
}
}
} catch (e) {
this.db.writeDeploymentsToFiles = wasWrittingToFiles;
throw e;
}
this.db.writeDeploymentsToFiles = wasWrittingToFiles;
log('deploy scripts complete');
}
public async export(options: {
exportAll?: string;
export?: string;
}): Promise<void> {
let chainId: string | undefined;
try {
chainId = fs
.readFileSync(
path.join(this.deploymentsPath, this.deploymentFolder(), '.chainId')
)
.toString()
.trim();
} catch (e) {}
if (!chainId) {
chainId = await this.getChainId();
}
if (options.exportAll !== undefined) {
log('load all deployments for export-all');
const all = loadAllDeployments(
this.env,
this.deploymentsPath,
true,
this.env.config.external && this.env.config.external.deployments
);
const currentNetworkDeployments: {
[contractName: string]: {
address: string;
abi: any[];
linkedData?: any;
};
} = {};
const currentDeployments = this.db.deployments;
for (const contractName of Object.keys(currentDeployments)) {
const deployment = currentDeployments[contractName];
currentNetworkDeployments[contractName] = {
address: deployment.address,
abi: deployment.abi,
linkedData: deployment.linkedData,
};
}
const currentNetwork = this.getDeploymentNetworkName();
if (all[chainId] === undefined) {
all[chainId] = [];
} else {
all[chainId] = all[chainId].filter((v) => v.name !== currentNetwork);
}
all[chainId].push({
name: currentNetwork,
chainId,
contracts: currentNetworkDeployments,
});
this._writeExports(options.exportAll, all);
log('export-all complete');
}
if (options.export !== undefined) {
log('single export...');
const currentNetworkDeployments: {
[contractName: string]: {
address: string;
abi: any[];
linkedData?: any;
};
} = {};
if (chainId !== undefined) {
const currentDeployments = this.db.deployments;
for (const contractName of Object.keys(currentDeployments)) {
const deployment = currentDeployments[contractName];
currentNetworkDeployments[contractName] = {
address: deployment.address,
abi: deployment.abi,
linkedData: deployment.linkedData,
};
}
} else {
throw new Error('chainId is undefined');
}
const singleExport: Export = {
name: this.getDeploymentNetworkName(),
chainId,
contracts: currentNetworkDeployments,
};
this._writeExports(options.export, singleExport);
log('single export complete');
}
}
private _writeExports(dests: string, outputObject: any) {
const output = JSON.stringify(outputObject, bnReplacer, ' '); // TODO remove bytecode ?
const splitted = dests.split(',');
for (const split of splitted) {
if (!split) {
continue;
}
if (split === '-') {
process.stdout.write(output);
} else {
fs.ensureDirSync(path.dirname(split));
if (split.endsWith('.ts')) {
fs.writeFileSync(split, `export default ${output} as const;`);
} else {
fs.writeFileSync(split, output);
}
}
}
}
private getImportPaths() {
const importPaths = [this.env.config.paths.imports];
if (this.env.config.external && this.env.config.external.contracts) {
for (const externalContracts of this.env.config.external.contracts) {
importPaths.push(...externalContracts.artifacts);
}
}
return importPaths;
}
private async setup(isRunningGlobalFixture: boolean) {
this.setupNetwork();
if (!this.db.deploymentsLoaded && !isRunningGlobalFixture) {
if (process.env.HARDHAT_DEPLOY_FIXTURE) {
if (process.env.HARDHAT_COMPILE) {
// console.log("compiling...");
await this.env.run('compile');
}
this.db.deploymentsLoaded = true;
// console.log("running global fixture....");
await this.partialExtension.fixture(undefined, {
keepExistingDeployments: true, // by default reuse the existing deployments (useful for fork testing)
});
} else {
if (process.env.HARDHAT_COMPILE) {
// console.log("compiling...");
await this.env.run('compile');
}
await this.loadDeployments();
}
}
}
private async saveSnapshot(key: string, data?: any) {
const latestBlock = await this.network.provider.send(
'eth_getBlockByNumber',
['latest', false]
);
try {
const snapshot = await this.network.provider.send('evm_snapshot', []);
this.db.pastFixtures[key] = {
index: ++this.db.snapshotCounter,
snapshot,
data,
blockHash: latestBlock.hash,
deployments: {...this.db.deployments},
};
} catch (err) {
log(`failed to create snapshot`);
}
}
private async revertSnapshot(saved: {
index: number;
snapshot: any;
blockHash: string;
deployments: any;
}): Promise<boolean> {
const snapshotToRevertIndex = saved.index;
for (const fixtureKey of Object.keys(this.db.pastFixtures)) {
const snapshotIndex = this.db.pastFixtures[fixtureKey].index;
if (snapshotIndex > snapshotToRevertIndex) {
delete this.db.pastFixtures[fixtureKey];
}
}
let success;
try {
success = await this.network.provider.send('evm_revert', [
saved.snapshot,
]);
} catch {
log(`failed to revert to snapshot`);
success = false;
}
if (success) {
const blockRetrieved = await this.network.provider.send(
'eth_getBlockByHash',
[saved.blockHash, false]
);
if (blockRetrieved) {
saved.snapshot = await this.network.provider.send('evm_snapshot', []); // it is necessary to re-snapshot it
this.db.deployments = {...saved.deployments};
} else {
// TODO or should we throw ?
return false;
}
}
return success;
}
disableAutomaticImpersonation(): void {
this.impersonateUnknownAccounts = false;
}
private getNetworkName(): string {
return getNetworkName(this.network);
}
private getDeploymentNetworkName(): string {
if (this.db.runAsNode) {
return 'localhost';
}
return getNetworkName(this.network);
}
private deploymentFolder(): string {
return this.getDeploymentNetworkName();
}
private async impersonateAccounts(unknownAccounts: string[]) {
if (
!this.impersonateUnknownAccounts ||
process.env.HARDHAT_DEPLOY_NO_IMPERSONATION
) {
return;
}
if (this.network.autoImpersonate) {
for (const address of unknownAccounts) {
if (this.network.name === 'hardhat') {
await this.network.provider.request({
method: 'hardhat_impersonateAccount',
params: [address],
});
}
this.impersonatedAccounts.push(address);
}
}
}
public async setupAccounts(): Promise<{
namedAccounts: {[name: string]: string};
unnamedAccounts: string[];
}> {
if (!this.db.accountsLoaded) {
const chainId = await this.getChainId();
const accounts = await this.network.provider.send('eth_accounts');
const {
namedAccounts,
unnamedAccounts,
unknownAccounts,
addressesToProtocol,
} = processNamedAccounts(
this.network,
this.env.config.namedAccounts,
accounts,
chainId
); // TODO pass in network name
await this.impersonateAccounts(unknownAccounts);
this.db.namedAccounts = namedAccounts;
this.db.unnamedAccounts = unnamedAccounts;
this.db.accountsLoaded = true;
this.addressesToProtocol = addressesToProtocol;
}
return {
namedAccounts: this.db.namedAccounts,
unnamedAccounts: this.db.unnamedAccounts,
};
}
}