hardhat-deploy
Version:
Hardhat Plugin For Replicable Deployments And Tests
658 lines (629 loc) • 20.7 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as fs from 'fs-extra';
import * as path from 'path';
import {Wallet} from '@ethersproject/wallet';
import {getAddress, isAddress} from '@ethersproject/address';
import {Interface, FunctionFragment, Fragment} from '@ethersproject/abi';
import {Artifact, HardhatRuntimeEnvironment, Network} from 'hardhat/types';
import {BigNumber} from '@ethersproject/bignumber';
import {ABI, Export, ExtendedArtifact, MultiExport} from '../types';
import {Artifacts} from 'hardhat/internal/artifacts';
import murmur128 from 'murmur-128';
import {Transaction} from '@ethersproject/transactions';
import {store} from './globalStore';
import {ERRORS} from 'hardhat/internal/core/errors-list';
import {HardhatError} from 'hardhat/internal/core/errors';
function getOldArtifactSync(
name: string,
folderPath: string
): ExtendedArtifact | undefined {
const oldArtifactPath = path.join(folderPath, name + '.json');
let artifact;
if (fs.existsSync(oldArtifactPath)) {
try {
artifact = JSON.parse(fs.readFileSync(oldArtifactPath).toString());
} catch (e) {
console.error(e);
}
}
return artifact;
}
export async function getArtifactFromFolders(
name: string,
folderPaths: string[]
): Promise<Artifact | ExtendedArtifact | undefined> {
for (const onepath of folderPaths) {
const artifacts = new Artifacts(onepath);
let artifact = getOldArtifactSync(name, onepath);
if (!artifact) {
try {
artifact = artifacts.readArtifactSync(name);
} catch (e) {
const hardhatError = e as HardhatError;
if (
hardhatError.number &&
hardhatError.number == ERRORS.ARTIFACTS.MULTIPLE_FOUND.number
) {
throw e;
}
}
}
if (artifact) {
return artifact;
}
}
}
// TODO
// const solcInputMetadataCache: Record<string,
// const buildInfoCache
// const hashCache: Record<string, string> = {};
export async function getExtendedArtifactFromFolders(
name: string,
folderPaths: string[]
): Promise<ExtendedArtifact | undefined> {
for (const folderPath of folderPaths) {
const artifacts = new Artifacts(folderPath);
let artifact = getOldArtifactSync(name, folderPath);
if (
!artifact &&
(await artifacts.artifactExists(name).catch(() => false))
) {
const hardhatArtifact: Artifact = await artifacts.readArtifact(name);
// check if name is already a fullyQualifiedName
let fullyQualifiedName = name;
let contractName = name;
if (!fullyQualifiedName.includes(':')) {
fullyQualifiedName = `${hardhatArtifact.sourceName}:${name}`;
} else {
contractName = fullyQualifiedName.split(':')[1];
}
// TODO try catch ? in case debug file is missing
const buildInfo = await artifacts.getBuildInfo(fullyQualifiedName);
if (buildInfo) {
const solcInput = JSON.stringify(buildInfo.input, null, ' ');
const solcInputHash = Buffer.from(murmur128(solcInput)).toString('hex');
artifact = {
...hardhatArtifact,
...buildInfo.output.contracts[hardhatArtifact.sourceName][
contractName
],
solcInput,
solcInputHash,
};
} else {
artifact = {
...hardhatArtifact,
};
}
}
if (artifact) {
return artifact;
}
}
}
export function loadAllDeployments(
hre: HardhatRuntimeEnvironment,
deploymentsPath: string,
onlyABIAndAddress?: boolean,
externalDeployments?: {[networkName: string]: string[]}
): MultiExport {
const networksFound: {[networkName: string]: Export} = {};
const all: MultiExport = {}; // TODO any is chainConfig
fs.readdirSync(deploymentsPath).forEach((fileName) => {
const fPath = path.resolve(deploymentsPath, fileName);
const stats = fs.statSync(fPath);
if (stats.isDirectory()) {
let chainIdFound: string;
const chainIdFilepath = path.join(fPath, '.chainId');
if (fs.existsSync(chainIdFilepath)) {
chainIdFound = fs.readFileSync(chainIdFilepath).toString().trim();
} else {
throw new Error(
`with hardhat-deploy >= 0.6 you need to rename network folder without appended chainId
You also need to create a '.chainId' file in the folder with the chainId`
);
}
if (!all[chainIdFound]) {
all[chainIdFound] = [];
}
const contracts = loadDeployments(
deploymentsPath,
fileName,
onlyABIAndAddress
);
const network = {
name: fileName,
chainId: chainIdFound,
contracts,
};
networksFound[fileName] = network;
all[chainIdFound].push(network);
}
});
if (externalDeployments) {
for (const networkName of Object.keys(externalDeployments)) {
for (const folderPath of externalDeployments[networkName]) {
const networkConfig = hre.config.networks[networkName];
if (networkConfig && networkConfig.chainId) {
const networkChainId = networkConfig.chainId.toString();
const contracts = loadDeployments(
folderPath,
'',
onlyABIAndAddress,
undefined,
networkChainId
);
const networkExist = networksFound[networkName];
if (networkExist) {
if (networkChainId !== networkExist.chainId) {
throw new Error(
`mismatch between external deployment network ${networkName} chainId: ${networkChainId} vs existing chainId: ${networkExist.chainId}`
);
}
networkExist.contracts = {...contracts, ...networkExist.contracts};
} else {
const network = {
name: networkName,
chainId: networkChainId,
contracts,
};
networksFound[networkName] = network;
all[networkChainId].push(network);
}
} else {
console.warn(
`export-all limitation: attempting to load external deployments from ${folderPath} without chainId info. Please set the chainId in the network config for ${networkName}`
);
}
}
}
}
return all;
}
export function deleteDeployments(
deploymentsPath: string,
subPath: string
): void {
const deployPath = path.join(deploymentsPath, subPath);
fs.removeSync(deployPath);
}
function loadDeployments(
deploymentsPath: string,
subPath: string,
onlyABIAndAddress?: boolean,
expectedChainId?: string,
truffleChainId?: string
) {
const deploymentsFound: {[name: string]: any} = {};
const deployPath = path.join(deploymentsPath, subPath);
let filesStats;
try {
filesStats = traverse(
deployPath,
undefined,
undefined,
(name) => !name.startsWith('.') && name !== 'solcInputs'
);
} catch (e) {
// console.log('no folder at ' + deployPath);
return {};
}
if (filesStats.length > 0) {
if (expectedChainId) {
const chainIdFilepath = path.join(deployPath, '.chainId');
if (fs.existsSync(chainIdFilepath)) {
const chainIdFound = fs.readFileSync(chainIdFilepath).toString().trim();
if (expectedChainId !== chainIdFound) {
throw new Error(
`Loading deployment in folder '${deployPath}' (with chainId: ${chainIdFound}) for a different chainId (${expectedChainId})`
);
}
} else {
throw new Error(
`with hardhat-deploy >= 0.6 you are expected to create a '.chainId' file in the deployment folder`
);
}
}
}
let fileNames = filesStats.map((a) => a.relativePath);
fileNames = fileNames.sort((a, b) => {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
});
for (const fileName of fileNames) {
if (fileName.substr(fileName.length - 5) === '.json') {
const deploymentFileName = path.join(deployPath, fileName);
let deployment = JSON.parse(
fs.readFileSync(deploymentFileName).toString()
);
if (!deployment.address && deployment.networks) {
if (truffleChainId && deployment.networks[truffleChainId]) {
// TRUFFLE support
const truffleDeployment = deployment as any; // TruffleDeployment;
deployment.address =
truffleDeployment.networks[truffleChainId].address;
deployment.transactionHash =
truffleDeployment.networks[truffleChainId].transactionHash;
}
}
if (onlyABIAndAddress) {
deployment = {
address: deployment.address,
abi: deployment.abi,
linkedData: deployment.linkedData,
};
}
const name = fileName.slice(0, fileName.length - 5);
// console.log('fetching ' + deploymentFileName + ' for ' + name);
deploymentsFound[name] = deployment;
}
}
return deploymentsFound;
}
export function addDeployments(
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
db: any,
deploymentsPath: string,
subPath: string,
expectedChainId?: string,
truffleChainId?: string
): void {
const contracts = loadDeployments(
deploymentsPath,
subPath,
false,
expectedChainId,
truffleChainId
);
for (const key of Object.keys(contracts)) {
db.deployments[key] = contracts[key];
// TODO ABIS
// db.abis[contracts[key].address] = mergeABI(
// db.abis[contracts[key].address],
// contracts[key].abi
// );
}
}
function transformNamedAccounts(
configNamedAccounts: {[name: string]: any},
chainIdGiven: string | number,
accounts: string[],
networkConfigName: string
): {
namedAccounts: {[name: string]: string};
unnamedAccounts: string[];
unknownAccounts: string[];
addressesToProtocol: {[address: string]: string};
} {
const addressesToProtocol: {[address: string]: string} = {};
const unknownAccountsDict: {[address: string]: boolean} = {};
const knownAccountsDict: {[address: string]: boolean} = {};
for (const account of accounts) {
knownAccountsDict[account.toLowerCase()] = true;
}
const namedAccounts: {[name: string]: string} = {};
const usedAccounts: {[address: string]: boolean} = {};
// TODO transform into checksum address
if (configNamedAccounts) {
const accountNames = Object.keys(configNamedAccounts);
// eslint-disable-next-line no-inner-declarations
function parseSpec(spec: any): string | undefined {
let address: string | undefined;
switch (typeof spec) {
case 'string':
// eslint-disable-next-line no-case-declarations
const protocolSplit = spec.split('://');
if (protocolSplit.length > 1) {
if (protocolSplit[0].toLowerCase() === 'external') {
address = protocolSplit[1];
addressesToProtocol[address.toLowerCase()] =
protocolSplit[0].toLowerCase();
// knownAccountsDict[address.toLowerCase()] = true; // TODO ? this would prevent auto impersonation in fork/test
} else if (
protocolSplit[0].toLowerCase() === 'trezor'
) {
address = protocolSplit[1];
addressesToProtocol[address.toLowerCase()] =
protocolSplit[0].toLowerCase();
} else if (protocolSplit[0].toLowerCase() === 'ledger') {
const addressSplit = protocolSplit[1].split(':');
if (addressSplit.length > 1) {
address = addressSplit[1];
addressesToProtocol[
address.toLowerCase()
] = `ledger://${addressSplit[0]}`;
} else {
address = protocolSplit[1];
addressesToProtocol[address.toLowerCase()] =
"ledger://m/44'/60'/0'/0/0";
}
// knownAccountsDict[address.toLowerCase()] = true; // TODO ? this would prevent auto impersonation in fork/test
} else if (protocolSplit[0].toLowerCase() === 'privatekey') {
address = new Wallet(protocolSplit[1]).address;
addressesToProtocol[address.toLowerCase()] =
'privatekey://' + protocolSplit[1];
} else {
throw new Error(
`unsupported protocol ${protocolSplit[0]}:// for named accounts`
);
}
} else {
if (spec.slice(0, 2).toLowerCase() === '0x') {
if (!isAddress(spec)) {
throw new Error(
`"${spec}" is not a valid address, if you used to put privateKey there, use the "privatekey://" prefix instead`
);
}
address = spec;
} else {
address = parseSpec(configNamedAccounts[spec]);
}
}
break;
case 'number':
if (accounts) {
address = accounts[spec];
}
break;
case 'undefined':
break;
case 'object':
if (spec) {
if (spec.type === 'object') {
address = spec;
} else {
const newSpec = chainConfig(
spec,
chainIdGiven,
networkConfigName
);
if (typeof newSpec !== 'undefined') {
address = parseSpec(newSpec);
}
}
}
break;
}
if (address) {
if (typeof address === 'string') {
address = getAddress(address);
}
}
return address;
}
for (const accountName of accountNames) {
const spec = configNamedAccounts[accountName];
const address = parseSpec(spec);
if (address) {
namedAccounts[accountName] = address;
usedAccounts[address.toLowerCase()] = true;
if (!knownAccountsDict[address.toLowerCase()]) {
unknownAccountsDict[address.toLowerCase()] = true;
}
}
}
}
const unnamedAccounts = [];
for (const address of accounts) {
if (!usedAccounts[address.toLowerCase()]) {
unnamedAccounts.push(getAddress(address));
}
}
return {
namedAccounts,
unnamedAccounts,
unknownAccounts: Object.keys(unknownAccountsDict).map(getAddress),
addressesToProtocol,
};
}
function chainConfig(
object: any,
chainIdGiven: string | number,
networkConfigName: string
) {
// TODO utility function:
let chainIdDecimal;
if (typeof chainIdGiven === 'number') {
chainIdDecimal = '' + chainIdGiven;
} else {
if (chainIdGiven.startsWith('0x')) {
chainIdDecimal = '' + parseInt(chainIdGiven.slice(2), 16);
} else {
chainIdDecimal = chainIdGiven;
}
}
if (typeof object[networkConfigName] !== 'undefined') {
return object[networkConfigName];
} else if (typeof object[chainIdGiven] !== 'undefined') {
return object[chainIdGiven];
} else if (typeof object[chainIdDecimal] !== 'undefined') {
return object[chainIdDecimal];
} else {
return object.default;
}
}
export function processNamedAccounts(
network: Network,
namedAccounts: {
[name: string]:
| string
| number
| {[network: string]: null | number | string};
},
accounts: string[],
chainIdGiven: string
): {
namedAccounts: {[name: string]: string};
unnamedAccounts: string[];
unknownAccounts: string[];
addressesToProtocol: {[address: string]: string};
} {
if (namedAccounts) {
return transformNamedAccounts(
namedAccounts,
chainIdGiven,
accounts,
process.env.HARDHAT_DEPLOY_ACCOUNTS_NETWORK || getNetworkName(network)
);
} else {
return {
namedAccounts: {},
unnamedAccounts: accounts,
unknownAccounts: [],
addressesToProtocol: {},
};
}
}
export function traverseMultipleDirectory(dirs: string[]): string[] {
const filepaths = [];
for (const dir of dirs) {
let filesStats = traverse(dir);
filesStats = filesStats.filter((v) => !v.directory);
for (const filestat of filesStats) {
filepaths.push(path.join(dir, filestat.relativePath));
}
}
return filepaths;
}
export const traverse = function (
dir: string,
result: any[] = [],
topDir?: string,
filter?: (name: string, stats: any) => boolean // TODO any is Stats
): Array<{
name: string;
path: string;
relativePath: string;
mtimeMs: number;
directory: boolean;
}> {
fs.readdirSync(dir).forEach((name) => {
const fPath = path.resolve(dir, name);
const stats = fs.statSync(fPath);
if ((!filter && !name.startsWith('.')) || (filter && filter(name, stats))) {
const fileStats = {
name,
path: fPath,
relativePath: path.relative(topDir || dir, fPath),
mtimeMs: stats.mtimeMs,
directory: stats.isDirectory(),
};
if (fileStats.directory) {
result.push(fileStats);
return traverse(fPath, result, topDir || dir, filter);
}
result.push(fileStats);
}
});
return result;
};
export function getNetworkName(network: Network): string {
if (process.env['HARDHAT_DEPLOY_FORK']) {
return process.env['HARDHAT_DEPLOY_FORK'];
}
if ('forking' in network.config && (network.config.forking as any)?.network) {
return (network.config.forking as any)?.network;
}
return network.name;
}
export function getDeployPaths(network: Network): string[] {
const networkName = getNetworkName(network);
if (networkName === network.name) {
return network.deploy || store.networks[networkName]?.deploy; // fallback to global store
} else {
return store.networks[networkName]?.deploy; // skip network.deploy
}
}
export function filterABI(
abi: ABI,
excludeSighashes: Set<string>,
): any[] {
return abi.filter(fragment => fragment.type !== 'function' || !excludeSighashes.has(Interface.getSighash(Fragment.from(fragment) as FunctionFragment)));
}
export function mergeABIs(
abis: any[][],
options: {check: boolean; skipSupportsInterface: boolean}
): any[] {
if (abis.length === 0) {
return [];
}
const result: any[] = JSON.parse(JSON.stringify(abis[0]));
for (let i = 1; i < abis.length; i++) {
const abi = abis[i];
for (const fragment of abi) {
const newEthersFragment = Fragment.from(fragment);
// TODO constructor special handling ?
const foundSameSig = result.find((v) => {
const existingEthersFragment = Fragment.from(v);
if (v.type !== fragment.type) {
return false;
}
if (!existingEthersFragment) {
return v.name === fragment.name; // TODO fallback and receive hanlding
}
if (
existingEthersFragment.type === 'constructor' ||
newEthersFragment.type === 'constructor'
) {
return existingEthersFragment.name === newEthersFragment.name;
}
if (newEthersFragment.type === 'function') {
return (
Interface.getSighash(existingEthersFragment as FunctionFragment) ===
Interface.getSighash(newEthersFragment as FunctionFragment)
);
} else if (newEthersFragment.type === 'event') {
return existingEthersFragment.format() === newEthersFragment.format();
} else {
return v.name === fragment.name; // TODO fallback and receive hanlding
}
});
if (foundSameSig) {
if (
options.check &&
!(
options.skipSupportsInterface &&
fragment.name === 'supportsInterface'
)
) {
if (fragment.type === 'function') {
throw new Error(
`function "${fragment.name}" will shadow "${foundSameSig.name}". Please update code to avoid conflict.`
);
}
}
} else {
result.push(fragment);
}
}
}
return result;
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function recode(decoded: any): Transaction {
return {
from: decoded.from,
gasPrice: decoded.gasPrice ? BigNumber.from(decoded.gasPrice) : undefined,
maxFeePerGas: decoded.maxFeePerGas
? BigNumber.from(decoded.maxFeePerGas)
: undefined,
maxPriorityFeePerGas: decoded.maxPriorityFeePerGas
? BigNumber.from(decoded.maxPriorityFeePerGas)
: undefined,
gasLimit: BigNumber.from(decoded.gasLimit),
to: decoded.to,
value: BigNumber.from(decoded.value),
nonce: decoded.nonce,
data: decoded.data,
r: decoded.r,
s: decoded.s,
v: decoded.v,
// creates: tx.creates, // TODO test
chainId: decoded.chainId,
};
}