@buildship/hardhat-ipfs-upload
Version:
Hardhat IPFS uploader via Buildship
229 lines (178 loc) • 8.2 kB
text/typescript
import fs from "fs";
import path from "path";
import { exec } from "child_process";
import { task } from "hardhat/config";
import minimist from "minimist";
import { Blob, NFTStorage } from "nft.storage";
import { getLongVersion } from "./solc/version";
const NFT_STORAGE_API_KEY = process.env.NFT_STORAGE_API_KEY;
const argv = minimist(process.argv.slice(2));
// Uploads a compiled contract to IPFS and returns its hash
task("upload", "Uploads a compiled contract to IPFS and returns deploy link")
.addPositionalParam("contract", "Contract to deploy")
.addOptionalParam("args", "Deploy arguments")
.addOptionalParam("ascii", "ASCII art file path (.txt)")
.setAction(async (taskArgs, hre) => {
try {
if (!NFT_STORAGE_API_KEY) {
console.error("Please put NFT_STORAGE_API_KEY in .env");
process.exit(-1);
}
const client = new NFTStorage({ token: NFT_STORAGE_API_KEY });
await hre.run("compile");
// console.log('process.argv', process.argv)
const { contract, args, ascii } = taskArgs;
console.log("Using contract", contract);
if (!contract) {
console.log(
`Usage: npx hardhat upload [contract name] --args '"arg1","arg2"'`
);
return;
}
if (!fs.existsSync(contract)) {
console.error(`Contract ${contract} not found`);
return;
}
// const factory = await hre.ethers.getContractFactory(contractName);
// extract filename from contract
const filename = contract.split("/").pop().replace(".sol", "");
if (!filename) {
console.log(`File has no name`);
return;
}
// read dir ./artifacts/${contract} and open the {filename}.json file
const contractArtifact = JSON.parse(
fs
.readFileSync(`./artifacts/${contract}/${filename}.json`)
.toString()
);
let solcInput, solcLongVersion;
try {
const artifactDbg = `./artifacts/${contract}/${filename}.dbg.json`
const { buildInfo } = JSON.parse(fs.readFileSync(artifactDbg).toString());
const buildInfoPath = path.join(`./artifacts/${contract}`, buildInfo);
const info = fs.readFileSync(buildInfoPath).toString();
const { solcLongVersion: version, input } = JSON.parse(info);
solcInput = input;
solcLongVersion = version;
console.log(`Build info: ${buildInfoPath}`);
} catch (err: any) {
console.log(`Build info not found:\n`, err.message);
return;
}
const { abi, bytecode, ...artifact } = contractArtifact;
// if process.argv elements contain "help"
if (argv._.find((elem) => elem.includes("help"))) {
console.log(
`Usage: npx hardhat upload [contract name] --args '"arg1","arg2"'`
);
return;
}
let flattened;
try {
// try flattening contract
const sourcePath = contract;
// create dir ./tmp
if (!fs.existsSync("./tmp")) {
fs.mkdirSync("./tmp");
}
// await run("flatten", [ sourcePath, "./tmp/Flattened.sol" ]);
// const sh = `npx truffle-flattener ${sourcePath} | awk '/SPDX-License-Identifier/&&c++>0 {next} 1' | awk '/pragma experimental ABIEncoderV2;/&&c++>0 {next} 1' > ./tmp/Flattened.sol`;
const sh = `npx hardhat flatten "${sourcePath}" > ./tmp/Flattened.sol`;
// run the flattener
console.log("\nRunning command:", sh);
await new Promise((resolve, reject) => {
exec(sh, (err, stdout, stderr) => {
if (err || stderr) {
console.log("Error flattening contract:", err);
return reject(err);
}
// pipe stdout and stderr to console
// tslint:disable-next-line
stdout &&
console.log(
`\nOutput: ${stdout.split("\n").join("\n\t")}`
);
// tslint:disable-next-line
stderr &&
console.log(
`\nErrors: ${stderr.split("\n").join("\n\t")}`
);
resolve(true);
});
});
flattened = fs.readFileSync("./tmp/Flattened.sol", "utf8");
if (!flattened) {
throw new Error("No flattened contract");
}
// from https://github.com/boringcrypto/dictator-dao/blob/a3de9f606d05852eb5cfa811a3f38870ab22800a/hardhat.config.js#L60
// Remove every line started with "// SPDX-License-Identifier:"
flattened = flattened.replace(
/SPDX-License-Identifier:/gm,
"License-Identifier:"
);
flattened = `// SPDX-License-Identifier: MIXED\n\n${flattened}`;
// Remove every line started with "pragma experimental ABIEncoderV2;" except the first one
flattened = flattened.replace(
/pragma experimental ABIEncoderV2;\n/gm,
((i) => (m: string) => (!i++ ? m : ""))(0)
);
flattened = flattened.trim();
if (ascii) {
// read file from ascii and paste in front of flattened
const art = fs.readFileSync(ascii, "utf8");
flattened = `${art}\n\n${flattened}`;
}
// write it back
fs.writeFileSync("./tmp/Flattened.sol", flattened);
} catch (err) {
// process exit with error message
console.error(`\nError:`, err);
return;
} finally {
// rm flattened file
// tslint:disable-next-line
!process.env.KEEP && fs.unlinkSync("./tmp/Flattened.sol");
// fs.rmdirSync("./tmp");
}
console.log(`\nDeploying ${contract}`);
// const longVersion = await getLongVersion(
// hre.config.solidity.compilers[0].version
// );
// const longVersionString = longVersion.replace(/^v/, "");
const contractInfo = {
name: contractArtifact.contractName,
filename,
abi,
bytecode,
artifact,
extra: {
metadata: JSON.stringify({
...hre.config.solidity.compilers[0],
compiler: {
...hre.config.solidity.compilers[0],
version: solcLongVersion,
},
}),
},
input: solcInput,
flattened,
};
const blob = new Blob([JSON.stringify(contractInfo)], {
type: "application/json",
});
const cid = await client.storeBlob(blob);
console.log(
`Metadata uploaded to https://cloudflare-ipfs.com/ipfs/${cid}\n`
);
console.log(`Deploy here:`);
const argsString = args
? `?args=%5B${encodeURIComponent(args)}%5D`
: "?args=%5B%5D";
console.log(
`https://gate-goerli.buildship.xyz/deploy/${cid}${argsString}`
);
} catch (err) {
console.error(err);
}
});