Skip to content

Hardhat zip deploy with ignition #556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
27ebf04
Before running with prettier
CoveMB Feb 21, 2025
7c7828e
After running with prettier
CoveMB Feb 21, 2025
ce5fcd3
Add consistent-type-imports rule
CoveMB Feb 21, 2025
3cd0b59
Add lint step in ci action
CoveMB Feb 21, 2025
31f0c78
resolve prettier conflict
CoveMB Feb 21, 2025
365421b
After running with prettier
CoveMB Feb 21, 2025
cedaeaa
resolve prettier conflict
CoveMB Feb 21, 2025
98bd8af
Add lint step in ci action
CoveMB Feb 21, 2025
a9098d0
resolve prettier conflict
CoveMB Feb 21, 2025
6e9df26
resolve prettier conflict
CoveMB Feb 21, 2025
574a739
Remove .vscode directory from Git tracking
CoveMB Feb 21, 2025
c0e9002
move linter action in it's own job
CoveMB Feb 21, 2025
86c65dc
add lint note in readme
CoveMB Feb 21, 2025
a1111d3
Update .github/workflows/test.yml
CoveMB Feb 21, 2025
abbd5a4
Merge remote-tracking branch 'upstream/master'
CoveMB Feb 21, 2025
beffa34
Merge branch 'master' into master
ericglau Feb 21, 2025
d6bec2a
lint script files
CoveMB Feb 21, 2025
315b775
Merge branch 'master' of github.com:CoveMB/contracts-wizard
CoveMB Feb 21, 2025
6ed6e4f
Merge remote-tracking branch 'upstream/master'
CoveMB Feb 24, 2025
ea90cd1
Merge remote-tracking branch 'upstream/master'
CoveMB Feb 28, 2025
abf687a
Merge remote-tracking branch 'upstream/master'
CoveMB Mar 4, 2025
426b62d
Merge remote-tracking branch 'upstream/master'
CoveMB Mar 20, 2025
ea25cc1
Merge remote-tracking branch 'upstream/master'
CoveMB Mar 20, 2025
0911f87
Merge remote-tracking branch 'upstream/master'
CoveMB Mar 25, 2025
4914083
Merge remote-tracking branch 'upstream/master'
CoveMB Mar 27, 2025
5ce527f
Merge remote-tracking branch 'upstream/master'
CoveMB Apr 4, 2025
03a32fc
Merge remote-tracking branch 'upstream/master'
CoveMB Apr 7, 2025
b3c0347
Merge remote-tracking branch 'upstream/master'
CoveMB Apr 12, 2025
0a52a65
Merge remote-tracking branch 'upstream/master'
CoveMB May 13, 2025
9e74342
Merge remote-tracking branch 'upstream/master'
CoveMB May 19, 2025
d727f51
Merge remote-tracking branch 'upstream/master'
CoveMB May 21, 2025
1710307
Adjust build zip to use ignition instead of deploy script, adjust tests
CoveMB May 25, 2025
5c6498d
Add changeset
CoveMB May 26, 2025
734d445
Dry the extract and run function
CoveMB May 26, 2025
736ff3d
remove import, update readme
CoveMB May 26, 2025
99cf0bb
Make deploy command descriptive name
CoveMB May 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 78 additions & 32 deletions packages/core/solidity/src/zip-hardhat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ test.serial('erc20 full', async t => {
flashmint: true,
};
const c = buildERC20(opts);
await runTest(c, t, opts);
await runIgnitionTest(c, t, opts);
});

test.serial('erc721 upgradeable', async t => {
Expand All @@ -58,7 +58,7 @@ test.serial('erc721 upgradeable', async t => {
upgradeable: 'uups',
};
const c = buildERC721(opts);
await runTest(c, t, opts);
await runDeployScriptTest(c, t, opts);
});

test.serial('erc1155 basic', async t => {
Expand All @@ -68,13 +68,13 @@ test.serial('erc1155 basic', async t => {
uri: 'https://myuri/{id}',
};
const c = buildERC1155(opts);
await runTest(c, t, opts);
await runIgnitionTest(c, t, opts);
});

test.serial('custom basic', async t => {
const opts: GenericOptions = { kind: 'Custom', name: 'My Contract' };
const c = buildCustom(opts);
await runTest(c, t, opts);
await runIgnitionTest(c, t, opts);
});

test.serial('custom upgradeable', async t => {
Expand All @@ -84,18 +84,26 @@ test.serial('custom upgradeable', async t => {
upgradeable: 'transparent',
};
const c = buildCustom(opts);
await runTest(c, t, opts);
await runDeployScriptTest(c, t, opts);
});

async function runTest(c: Contract, t: ExecutionContext<Context>, opts: GenericOptions) {
async function runDeployScriptTest(c: Contract, t: ExecutionContext<Context>, opts: GenericOptions) {
const zip = await zipHardhat(c, opts);

assertLayout(zip, c, t);
await extractAndRunPackage(zip, c, t);
await assertContents(zip, c, t);
assertDeployScriptLayout(zip, c, t);
await extractAndRunDeployScriptPackage(zip, c, t);
await assertDeployScriptContents(zip, c, t);
}

function assertLayout(zip: JSZip, c: Contract, t: ExecutionContext<Context>) {
async function runIgnitionTest(c: Contract, t: ExecutionContext<Context>, opts: GenericOptions) {
const zip = await zipHardhat(c, opts);

assertIgnitionLayout(zip, c, t);
await extractAndRunIgnitionPackage(zip, c, t);
await assertIgnitionContents(zip, c, t);
}

function assertDeployScriptLayout(zip: JSZip, c: Contract, t: ExecutionContext<Context>) {
const sorted = Object.values(zip.files)
.map(f => f.name)
.sort();
Expand All @@ -115,36 +123,62 @@ function assertLayout(zip: JSZip, c: Contract, t: ExecutionContext<Context>) {
]);
}

async function extractAndRunPackage(zip: JSZip, c: Contract, t: ExecutionContext<Context>) {
const files = Object.values(zip.files);
function assertIgnitionLayout(zip: JSZip, c: Contract, t: ExecutionContext<Context>) {
const sorted = Object.values(zip.files)
.map(f => f.name)
.sort();
t.deepEqual(sorted, [
'.gitignore',
'README.md',
'contracts/',
`contracts/${c.name}.sol`,
'hardhat.config.ts',
'ignition/',
'ignition/modules/',
`ignition/modules/${c.name}.ts`,
'package-lock.json',
'package.json',
'test/',
'test/test.ts',
'tsconfig.json',
]);
}

function extractAndRun(makeCommand: (c: Contract) => string | null) {
return async (zip: JSZip, c: Contract, t: ExecutionContext<Context>) => {
const files = Object.values(zip.files);

const tempFolder = t.context.tempFolder;
const tempFolder = t.context.tempFolder;

const items = Object.values(files);
for (const item of items) {
if (item.dir) {
await fs.mkdir(path.join(tempFolder, item.name));
} else {
await fs.writeFile(path.join(tempFolder, item.name), await asString(item));
const items = Object.values(files);
for (const item of items) {
if (item.dir) {
await fs.mkdir(path.join(tempFolder, item.name));
} else {
await fs.writeFile(path.join(tempFolder, item.name), await asString(item));
}
}
}

let command = `cd "${tempFolder}" && npm install && npm test`;
if (c.constructorArgs === undefined) {
// only test deploying the contract if there are no constructor args needed
command += ' && npx hardhat run scripts/deploy.ts';
}
let command = `cd "${tempFolder}" && npm install && npm test`;
if (c.constructorArgs === undefined) {
// only test deploying the contract if there are no constructor args needed
command += ` && ${makeCommand(c)}`;
}

const exec = util.promisify(child.exec);
const result = await exec(command);
const exec = util.promisify(child.exec);
const result = await exec(command);

t.regex(result.stdout, /1 passing/);
if (c.constructorArgs === undefined) {
t.regex(result.stdout, /deployed to/);
}
t.regex(result.stdout, /1 passing/);
if (c.constructorArgs === undefined) {
t.regex(result.stdout, /deployed to/);
}
};
}

async function assertContents(zip: JSZip, c: Contract, t: ExecutionContext<Context>) {
const extractAndRunDeployScriptPackage = extractAndRun(() => 'npx hardhat run scripts/deploy.ts');
const extractAndRunIgnitionPackage = extractAndRun(c => `npx hardhat ignition deploy ignition/modules/${c.name}.ts`);

async function assertDeployScriptContents(zip: JSZip, c: Contract, t: ExecutionContext<Context>) {
const contentComparison = [
await getItemString(zip, `contracts/${c.name}.sol`),
await getItemString(zip, 'hardhat.config.ts'),
Expand All @@ -156,6 +190,18 @@ async function assertContents(zip: JSZip, c: Contract, t: ExecutionContext<Conte
t.snapshot(contentComparison);
}

async function assertIgnitionContents(zip: JSZip, c: Contract, t: ExecutionContext<Context>) {
const contentComparison = [
await getItemString(zip, `contracts/${c.name}.sol`),
await getItemString(zip, 'hardhat.config.ts'),
await getItemString(zip, 'package.json'),
await getItemString(zip, `ignition/modules/${c.name}.ts`),
await getItemString(zip, 'test/test.ts'),
];

t.snapshot(contentComparison);
}

async function getItemString(zip: JSZip, key: string) {
const obj = zip.files[key];
if (obj === undefined) {
Expand Down
51 changes: 12 additions & 39 deletions packages/core/solidity/src/zip-hardhat.test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,23 +100,14 @@ Generated by [AVA](https://avajs.dev).
"hardhat": "^2.16.1"␊
}␊
}`,
`import { ethers } from "hardhat";␊
`import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";␊
async function main() {␊
const ContractFactory = await ethers.getContractFactory("MyToken");␊
export default buildModule("MyTokenModule", (m) => {␊
// TODO: Set addresses for the contract arguments below␊
const instance = await ContractFactory.deploy(recipient, defaultAdmin, pauser, minter);␊
await instance.waitForDeployment();␊
console.log(\`Contract deployed to ${await instance.getAddress()}\`);␊
}␊
const myToken = m.contract("MyToken", [recipient, defaultAdmin, pauser, minter]);␊
// We recommend this pattern to be able to use async/await everywhere␊
// and properly handle errors.␊
main().catch((error) => {␊
console.error(error);␊
process.exitCode = 1;␊
return { myToken };␊
});␊
`,
`import { expect } from "chai";␊
Expand Down Expand Up @@ -301,23 +292,14 @@ Generated by [AVA](https://avajs.dev).
"hardhat": "^2.16.1"␊
}␊
}`,
`import { ethers } from "hardhat";␊
`import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";␊
async function main() {␊
const ContractFactory = await ethers.getContractFactory("MyToken");␊
export default buildModule("MyTokenModule", (m) => {␊
// TODO: Set addresses for the contract arguments below␊
const instance = await ContractFactory.deploy(initialOwner);␊
await instance.waitForDeployment();␊
console.log(\`Contract deployed to ${await instance.getAddress()}\`);␊
}␊
const myToken = m.contract("MyToken", [initialOwner]);␊
// We recommend this pattern to be able to use async/await everywhere␊
// and properly handle errors.␊
main().catch((error) => {␊
console.error(error);␊
process.exitCode = 1;␊
return { myToken };␊
});␊
`,
`import { expect } from "chai";␊
Expand Down Expand Up @@ -383,23 +365,14 @@ Generated by [AVA](https://avajs.dev).
"hardhat": "^2.16.1"␊
}␊
}`,
`import { ethers } from "hardhat";␊
`import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";␊
async function main() {␊
const ContractFactory = await ethers.getContractFactory("MyContract");␊
export default buildModule("MyContractModule", (m) => {␊
const instance = await ContractFactory.deploy();␊
await instance.waitForDeployment();␊
const myContract = m.contract("MyContract", []);␊
console.log(\`Contract deployed to ${await instance.getAddress()}\`);␊
}␊
// We recommend this pattern to be able to use async/await everywhere␊
// and properly handle errors.␊
main().catch((error) => {␊
console.error(error);␊
process.exitCode = 1;␊
return { myContract };␊
});␊
`,
`import { expect } from "chai";␊
Expand Down
Binary file modified packages/core/solidity/src/zip-hardhat.test.ts.snap
Binary file not shown.
34 changes: 29 additions & 5 deletions packages/core/solidity/src/zip-hardhat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,28 @@ main().catch((error) => {
`;
};

const readme = `\
const lowerFirstCharacter = (str: string) => str.charAt(0).toLowerCase() + str.slice(1);

const ignitionModule = (c: Contract) => {
const deployArguments = getAddressArgs(c);
const contractVariableName = lowerFirstCharacter(c.name);

return `import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

export default buildModule("${c.name}Module", (m) => {

${deployArguments.length > 0 ? '// TODO: Set addresses for the contract arguments below' : ''}
const ${contractVariableName} = m.contract("${c.name}", [${deployArguments.join(', ')}]);

return { ${contractVariableName} };
});
`;
};

const readme = (c: Contract) => `\
# Sample Hardhat Project

This project demonstrates a basic Hardhat use case. It comes with a contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a test for that contract, and a script that deploys that contract.
This project demonstrates a basic Hardhat use case. It comes with a contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a test for that contract, ${c.upgradeable ? 'and a script that deploys that contract' : 'and a Hardhat Ignition module that deploys that contract'}.

## Installing dependencies

Expand All @@ -169,7 +187,7 @@ npm test
You can target any network from your Hardhat config using:

\`\`\`
npx hardhat run --network <network-name> scripts/deploy.ts
${c.upgradeable ? 'npx hardhat run --network <network-name> scripts/deploy.ts' : `npx hardhat ignition deploy ignition/modules/${c.name}.ts --network <network-name>`}
\`\`\`
`;

Expand All @@ -196,12 +214,18 @@ export async function zipHardhat(c: Contract, opts?: GenericOptions) {

zip.file(`contracts/${c.name}.sol`, printContract(c));
zip.file('test/test.ts', test(c, opts));
zip.file('scripts/deploy.ts', script(c));

if (c.upgradeable) {
zip.file('scripts/deploy.ts', script(c));
} else {
zip.file(`ignition/modules/${c.name}.ts`, ignitionModule(c));
}

zip.file('.gitignore', gitIgnore);
zip.file('hardhat.config.ts', hardhatConfig(c.upgradeable));
zip.file('package.json', JSON.stringify(packageJson, null, 2));
zip.file(`package-lock.json`, JSON.stringify(packageLock, null, 2));
zip.file('README.md', readme);
zip.file('README.md', readme(c));
zip.file('tsconfig.json', tsConfig);

return zip;
Expand Down
Loading