Skip to content

Commit

Permalink
add import cloned contract tests and fix bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
Troublor committed Aug 24, 2024
1 parent 7aeab85 commit 0990569
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 54 deletions.
9 changes: 5 additions & 4 deletions src/clone/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Address, Chain } from 'viem';
import { CloneMetadata } from './meta';
import { instanceToPlain, plainToInstance } from 'class-transformer';
import assert from 'node:assert';
import { FileOverriddenError, UnsupportedError } from '../error';
import { FileCollisionError, UnsupportedError } from '../error';

/**
* Clone a contract from a chain into the current project.
Expand Down Expand Up @@ -43,9 +43,10 @@ export async function cloneContract(
plainToInstance(CloneMetadata, meta),
);
}
if (metas.length >= 1) {

if (destination === hre.config.paths.sources) {
throw new UnsupportedError(
'cloning multiple contracts in the same project is not yet supported',
'Cannot clone contracts into Hardhat source folder',
);
}

Expand Down Expand Up @@ -91,7 +92,7 @@ export async function cloneContract(
console.error(`\t${path.relative(hre.config.paths.root, file)}`);
}
}
throw new FileOverriddenError(overrides);
throw new FileCollisionError(overrides);
}
source_meta.sourceTree.dump(dumpDir);

Expand Down
65 changes: 48 additions & 17 deletions src/config-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ extendConfig((config: HardhatConfig) => {
const cloneMetas = loadCloneMetaSet(config);

// We need to override SolcConfig for the cloned contracts.
const allCompilerVersions: string[] = [];
for (const cloneMeta of cloneMetas) {
for (const clonedFile of Object.values(cloneMeta.clonedFiles)) {
let remappings = getRemappings(cloneMeta);

Check failure on line 33 in src/config-extensions.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'remappings' is never reassigned. Use 'const' instead

Check failure on line 33 in src/config-extensions.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'remappings' is never reassigned. Use 'const' instead

Check failure on line 33 in src/config-extensions.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'remappings' is never reassigned. Use 'const' instead
const solcConfig = cloneMeta.solcConfig;
// delete remappings since hardhat currently does not support solc remappings
// the remappings should have been process in the import resolution phase.
Expand All @@ -39,12 +39,13 @@ extendConfig((config: HardhatConfig) => {
config.solidity.overrides[
path.join(cloneMeta.folder, clonedFile)
] = solcConfig;
allCompilerVersions.push(solcConfig.version);
for (const [from, _] of Object.entries(remappings)) {

Check failure on line 42 in src/config-extensions.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'_' is assigned a value but never used

Check failure on line 42 in src/config-extensions.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'_' is assigned a value but never used

Check failure on line 42 in src/config-extensions.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'_' is assigned a value but never used
if (from.endsWith('.sol')) {
config.solidity.overrides[from] = solcConfig;
}
}
}
}
for (const compilerVersion of allCompilerVersions) {
config.solidity.compilers.push({ version: compilerVersion, settings: {} });
}
});

function loadCloneMetaSet(config: HardhatConfig): CloneMetadata[] {
Expand All @@ -69,27 +70,57 @@ function loadCloneMetaSet(config: HardhatConfig): CloneMetadata[] {
);
}

/**
* Get remappings of a cloned contract.
* All the remappings returned are guaranteed to be file-to-file remappings.
*/
function getRemappings(meta: CloneMetadata): Record<string, string> {
const remappings: Record<string, string> = {};

// original remappings of the cloned contract
for (const remapping of meta.solcConfig.settings.remappings ?? []) {
const [from, to] = (remapping as string).split('=');
if (from === to) {
continue;
}
for (const [_, actualPath] of Object.entries(meta.clonedFiles)) {

Check failure on line 86 in src/config-extensions.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'_' is assigned a value but never used

Check failure on line 86 in src/config-extensions.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'_' is assigned a value but never used

Check failure on line 86 in src/config-extensions.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'_' is assigned a value but never used
if (actualPath.startsWith(to)) {
const suffix = actualPath.substring(to.length);
remappings[from + suffix] = path.join(meta.folder, actualPath);
}
}
}

for (const [sourceName, actualPath] of Object.entries(meta.clonedFiles)) {
remappings[sourceName] = path.join(meta.folder, actualPath);
}

return remappings;
}

/**
* This task returns a Record<string, string> representing remappings to be used
* by the resolver.
*/
subtask(TASK_COMPILE_GET_REMAPPINGS).setAction(
async (_, { config }): Promise<Record<string, string>> => {
const remappings: Record<string, string> = {};

const remappings = {};
const cloneMetas = loadCloneMetaSet(config);
for (const meta of cloneMetas) {
// original remappings of the cloned contract
for (const remapping of meta.solcConfig.settings.remappings ?? []) {
const [from, to] = (remapping as string).split('=');
remappings[from] = path.join(meta.folder, to);
}

for (const [sourceName, actualPath] of Object.entries(meta.clonedFiles)) {
remappings[sourceName] = path.join(meta.folder, actualPath);
}
Object.assign(remappings, getRemappings(meta));
}

return remappings;
},
);

/**
* Returns a list of absolute paths to all the solidity files in the project.
* This list doesn't include dependencies, for example solidity files inside
* node_modules.
*
* This is the right task to override to change how the solidity files of the
* project are obtained.
*/
subtask(TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS)
.addOptionalParam('sp', undefined, undefined, types.string)
.setAction(
Expand Down
4 changes: 2 additions & 2 deletions src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ export class HttpError extends CloneError {
}
}

export class FileOverriddenError extends CloneError {
export class FileCollisionError extends CloneError {
constructor(public files: string[], cause?: unknown) {
super('FileOverriddenError', 'files will be overridden', {
super('FileOverriddenError', `files will be overridden: ${files}`, {
cause,
stack: new Error().stack,
});
Expand Down
2 changes: 1 addition & 1 deletion test/fixture-projects/hardhat-project/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HardhatUserConfig } from 'hardhat/types';
import '@medga/hardhat-clone';

const config: HardhatUserConfig = {
solidity: '0.7.3',
solidity: '0.8.9',
defaultNetwork: 'hardhat',
};

Expand Down
2 changes: 2 additions & 0 deletions test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import path from 'node:path';
import os from 'node:os';
import child_process from 'node:child_process';

export const TIMEOUT = 60 * 1000;

export const ETHERSCAN_API_KEYS = [
'MCAUM7WPE9XP5UQMZPCKIBUJHPM1C24FP6',
'QYKNT5RHASZ7PGQE68FNQWH99IXVTVVD2I',
Expand Down
35 changes: 35 additions & 0 deletions test/import.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { cloneContract, createTestFixture, TIMEOUT } from './helpers';

Check failure on line 1 in test/import.e2e.test.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'TIMEOUT' is defined but never used

Check failure on line 1 in test/import.e2e.test.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'TIMEOUT' is defined but never used

Check failure on line 1 in test/import.e2e.test.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'TIMEOUT' is defined but never used
import child_process from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';

describe('Import cloned contracts as dependency', () => {
it('import Kyber Meta Aggregation Rounter V2', async () => {
const tmp = await createTestFixture();

await cloneContract(
tmp,
'0x6131B5fae19EA4f9D964eAc0408E4408b66337b5',
'kyber',
);

const contract = `
import 'kyber/contracts/MetaAggregationRouterV2.sol';
contract C { MetaAggregationRouterV2 router; }
`;
const file = path.join(tmp, 'contracts', 'contract.sol');
await fs.promises.mkdir(path.dirname(file), { recursive: true });
await fs.promises.writeFile(file, contract);

child_process.execSync(`pnpm hardhat compile --force`, {
encoding: 'utf-8',
stdio: ['inherit', 'ignore', 'ignore'],
cwd: tmp,
});
expect(
fs.existsSync(path.join(tmp, 'artifacts', 'contracts', 'contract.sol')),
).toBeTruthy();

await fs.promises.rm(tmp, { recursive: true, force: true });
});
});
60 changes: 60 additions & 0 deletions test/multiple.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { cloneContract, createTestFixture, TIMEOUT } from './helpers';
import child_process from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';

describe('hardhat-clone clones multiple contracts', () => {
it.concurrent.each([
[
[
['0xdAC17F958D2ee523a2206206994597C13D831ec7', 'Tether'],
['0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 'USDC Proxy'],
],
],
[
[
['0x43506849D7C04F9138D1A2050bbF3A0c054402dd', 'USDC'],
['0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 'WETH'],
['0xDb53f47aC61FE54F456A4eb3E09832D08Dd7BEec', 'withlibraries'],
],
],
])(
`should clone %j`,
async (
contracts: string[][],
opts?: {
etherscanApiKey?: string;
debug?: boolean; // whether to print logs and preserve the temp project for debugging
},
) => {
const tmp = await createTestFixture(opts);

for (const [contractAddress, contractName] of contracts) {
await cloneContract(tmp, contractAddress, contractName, opts);
}

const output = child_process.execSync(
`pnpm hardhat compile --force ${opts?.debug ? '' : '--quiet'}`,
{
encoding: 'utf-8',
stdio: [
'inherit',
opts?.debug ? 'inherit' : 'ignore',
opts?.debug ? 'inherit' : 'ignore',
],
cwd: tmp,
},
);
if (opts?.debug) console.log('Compilation output:\n', output);
for (const [_, contractName] of contracts) {

Check failure on line 49 in test/multiple.e2e.test.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'_' is assigned a value but never used

Check failure on line 49 in test/multiple.e2e.test.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'_' is assigned a value but never used

Check failure on line 49 in test/multiple.e2e.test.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'_' is assigned a value but never used
expect(
fs.existsSync(path.join(tmp, 'artifacts', contractName)),
).toBeTruthy();
}

if (!opts?.debug)
await fs.promises.rm(tmp, { recursive: true, force: true });
},
TIMEOUT,
);
});
28 changes: 28 additions & 0 deletions test/unverified.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { cloneContract, createTestFixture, TIMEOUT } from './helpers';
import fs from 'node:fs';

describe('hardhat-clone clones unverified contracts', () => {
it.concurrent.each([
['0x123', 'Invalid address'],
['0x4675C7e5BaAFBFFbca748158bEcBA61ef3b0a263', 'NotVerified'],
['0xFf23e40ac05D30Df46c250Dd4d784f6496A79CE9', 'Vyper contracts'],
])(
`should clone %s (%s)`,
async (
contractAddress: string,
errorMsg: string,
opts?: {
etherscanApiKey?: string;
debug?: boolean; // whether to print logs and preserve the temp project for debugging
},
) => {
const tmp = await createTestFixture(opts);
const task = cloneContract(tmp, contractAddress, 'unverified', opts);
await expect(task).rejects.toThrow(errorMsg);

if (!opts?.debug)
await fs.promises.rm(tmp, { recursive: true, force: true });
},
TIMEOUT,
);
});
34 changes: 4 additions & 30 deletions test/verified.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { cloneContract, createTestFixture } from './helpers';
import { cloneContract, createTestFixture, TIMEOUT } from './helpers';
import fs from 'node:fs';
import path from 'node:path';
import child_process from 'node:child_process';

const TIMEOUT = 60 * 1000;

describe('hardhat-clone clones verified contracts', () => {
it.concurrent.each([
['0xdAC17F958D2ee523a2206206994597C13D831ec7', 'Tether'],
Expand Down Expand Up @@ -46,33 +44,9 @@ describe('hardhat-clone clones verified contracts', () => {
},
);
if (opts?.debug) console.log('Compilation output:\n', output);
expect(fs.existsSync(path.join(tmp, 'artifacts', contractName)));

if (!opts?.debug)
await fs.promises.rm(tmp, { recursive: true, force: true });
},
TIMEOUT,
);
});

describe('hardhat-clone clones unverified contracts', () => {
it.concurrent.each([
['0x123', 'Invalid address'],
['0x4675C7e5BaAFBFFbca748158bEcBA61ef3b0a263', 'NotVerified'],
['0xFf23e40ac05D30Df46c250Dd4d784f6496A79CE9', 'Vyper contracts'],
])(
`should clone %s (%s)`,
async (
contractAddress: string,
errorMsg: string,
opts?: {
etherscanApiKey?: string;
debug?: boolean; // whether to print logs and preserve the temp project for debugging
},
) => {
const tmp = await createTestFixture(opts);
const task = cloneContract(tmp, contractAddress, 'unverified', opts);
await expect(task).rejects.toThrow(errorMsg);
expect(
fs.existsSync(path.join(tmp, 'artifacts', contractName)),
).toBeTruthy();

if (!opts?.debug)
await fs.promises.rm(tmp, { recursive: true, force: true });
Expand Down

0 comments on commit 0990569

Please sign in to comment.