From 311f7ceb2c522a459a224294170774821f6a6d0e Mon Sep 17 00:00:00 2001 From: Shiv Bhonde Date: Mon, 16 Dec 2024 18:29:59 +0530 Subject: [PATCH] merge foundry imporvments changes --- .../foundry/script/DeployYourContract.s.sol | 31 ++- .../foundry/packages/foundry/Makefile | 39 +++- .../foundry/packages/foundry/foundry.toml | 5 +- .../foundry/packages/foundry/package.json | 4 +- .../foundry/script/Deploy.s.sol.template.mjs | 15 +- .../foundry/script/DeployHelpers.s.sol | 206 ++++++++--------- .../packages/foundry/script/VerifyAll.s.sol | 162 ++++++------- .../foundry/scripts-js/generateTsAbis.js | 216 ++++++++++++++---- .../packages/foundry/scripts-js/parseArgs.js | 115 ++++++++++ 9 files changed, 514 insertions(+), 279 deletions(-) create mode 100644 templates/solidity-frameworks/foundry/packages/foundry/scripts-js/parseArgs.js diff --git a/templates/example-contracts/foundry/packages/foundry/script/DeployYourContract.s.sol b/templates/example-contracts/foundry/packages/foundry/script/DeployYourContract.s.sol index 166e2df57..7f82e5afa 100644 --- a/templates/example-contracts/foundry/packages/foundry/script/DeployYourContract.s.sol +++ b/templates/example-contracts/foundry/packages/foundry/script/DeployYourContract.s.sol @@ -1,13 +1,30 @@ -//SPDX-License-Identifier: MIT +// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import "../contracts/YourContract.sol"; import "./DeployHelpers.s.sol"; +import "../contracts/YourContract.sol"; +/** + * @notice Deploy script for YourContract contract + * @dev Inherits ScaffoldETHDeploy which: + * - Includes forge-std/Script.sol for deployment + * - Includes ScaffoldEthDeployerRunner modifier + * - Provides `deployer` variable + * Example: + * yarn deploy --file DeployYourContract.s.sol # local anvil chain + * yarn deploy --file DeployYourContract.s.sol --network optimism # live network (requires keystore) + */ contract DeployYourContract is ScaffoldETHDeploy { - // use `deployer` from `ScaffoldETHDeploy` - function run() external ScaffoldEthDeployerRunner { - YourContract yourContract = new YourContract(deployer); - console.logString(string.concat("YourContract deployed at: ", vm.toString(address(yourContract)))); - } + /** + * @dev Deployer setup based on `ETH_KEYSTORE_ACCOUNT` in `.env`: + * - "scaffold-eth-default": Uses Anvil's account #9 (0xa0Ee7A142d267C1f36714E4a8F75612F20a79720), no password prompt + * - "scaffold-eth-custom": requires password used while creating keystore + * + * Note: Must use ScaffoldEthDeployerRunner modifier to: + * - Setup correct `deployer` account and fund it + * - Export contract addresses & ABIs to `nextjs` packages + */ + function run() external ScaffoldEthDeployerRunner { + new YourContract(deployer); + } } diff --git a/templates/solidity-frameworks/foundry/packages/foundry/Makefile b/templates/solidity-frameworks/foundry/packages/foundry/Makefile index 00144c4d3..c8817e64c 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/Makefile +++ b/templates/solidity-frameworks/foundry/packages/foundry/Makefile @@ -1,8 +1,11 @@ .PHONY: build deploy generate-abis verify-keystore account chain compile deploy-verify flatten fork format lint test verify +DEPLOY_SCRIPT ?= script/Deploy.s.sol + # setup wallet for anvil setup-anvil-wallet: shx rm ~/.foundry/keystores/scaffold-eth-default 2>/dev/null; \ + shx rm -rf broadcast/Deploy.s.sol/31337 cast wallet import --private-key 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 --unsafe-password 'localhost' scaffold-eth-default # Start local chain @@ -15,14 +18,22 @@ fork: setup-anvil-wallet # Build the project build: - forge build --build-info --build-info-path out/build-info/ + forge build --via-ir --build-info --build-info-path out/build-info/ -# Deploy the project +# Deploy the contracts deploy: + @if [ ! -f "$(DEPLOY_SCRIPT)" ]; then \ + echo "Error: Deploy script '$(DEPLOY_SCRIPT)' not found"; \ + exit 1; \ + fi @if [ "$(RPC_URL)" = "localhost" ]; then \ - forge script script/Deploy.s.sol --rpc-url localhost --password localhost --broadcast --legacy --ffi; \ + if [ "$(ETH_KEYSTORE_ACCOUNT)" = "scaffold-eth-default" ]; then \ + forge script $(DEPLOY_SCRIPT) --rpc-url localhost --password localhost --broadcast --legacy --ffi; \ + else \ + forge script $(DEPLOY_SCRIPT) --rpc-url localhost --broadcast --legacy --ffi; \ + fi \ else \ - forge script script/Deploy.s.sol --rpc-url $(RPC_URL) --broadcast --legacy --ffi; \ + forge script $(DEPLOY_SCRIPT) --rpc-url $(RPC_URL) --broadcast --legacy --ffi; \ fi # Build and deploy target @@ -35,9 +46,9 @@ generate-abis: verify-keystore: if grep -q "scaffold-eth-default" .env; then \ cast wallet address --password localhost; \ - else \ + else \ cast wallet address; \ - fi + fi # List account account: @@ -58,10 +69,18 @@ compile: # Deploy and verify deploy-verify: + @if [ ! -f "$(DEPLOY_SCRIPT)" ]; then \ + echo "Error: Deploy script '$(DEPLOY_SCRIPT)' not found"; \ + exit 1; \ + fi @if [ "$(RPC_URL)" = "localhost" ]; then \ - forge script script/Deploy.s.sol --rpc-url localhost --password localhost --broadcast --legacy --ffi --verify; \ + if [ "$(ETH_KEYSTORE_ACCOUNT)" = "scaffold-eth-default" ]; then \ + forge script $(DEPLOY_SCRIPT) --rpc-url localhost --password localhost --broadcast --legacy --ffi --verify; \ + else \ + forge script $(DEPLOY_SCRIPT) --rpc-url localhost --broadcast --legacy --ffi --verify; \ + fi \ else \ - forge script script/Deploy.s.sol --rpc-url $(RPC_URL) --broadcast --legacy --ffi --verify; \ + forge script $(DEPLOY_SCRIPT) --rpc-url $(RPC_URL) --broadcast --legacy --ffi --verify; \ fi node scripts-js/generateTsAbis.js @@ -77,10 +96,6 @@ format: lint: forge fmt --check && prettier --check ./script/**/*.js -# Run tests -test: - forge test - # Verify contracts verify: forge script script/VerifyAll.s.sol --ffi --rpc-url $(RPC_URL) diff --git a/templates/solidity-frameworks/foundry/packages/foundry/foundry.toml b/templates/solidity-frameworks/foundry/packages/foundry/foundry.toml index bd4de0621..d3537f6a6 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/foundry.toml +++ b/templates/solidity-frameworks/foundry/packages/foundry/foundry.toml @@ -33,9 +33,8 @@ sepolia = { key = "${ETHERSCAN_API_KEY}" } [fmt] -multiline_func_header = "params_first" -line_length = 80 -tab_width = 2 +line_length = 120 +tab_width = 4 quote_style = "double" bracket_spacing = true int_types = "long" diff --git a/templates/solidity-frameworks/foundry/packages/foundry/package.json b/templates/solidity-frameworks/foundry/packages/foundry/package.json index 6e2ec8aad..3434efa08 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/package.json +++ b/templates/solidity-frameworks/foundry/packages/foundry/package.json @@ -8,8 +8,8 @@ "account:import": "make account-import ACCOUNT_NAME=${1:-scaffold-eth-custom}", "chain": "make chain", "compile": "make compile", - "deploy": "make build-and-deploy RPC_URL=${1:-localhost}", - "deploy:verify": "make deploy-verify RPC_URL=${1:-localhost}", + "deploy": "node scripts-js/parseArgs.js", + "deploy:verify": "node scripts/parseArgs.js --verify", "flatten": "make flatten", "fork": "make fork FORK_URL=${1:-mainnet}", "format": "make format", diff --git a/templates/solidity-frameworks/foundry/packages/foundry/script/Deploy.s.sol.template.mjs b/templates/solidity-frameworks/foundry/packages/foundry/script/Deploy.s.sol.template.mjs index 018e7ce21..17324b23a 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/script/Deploy.s.sol.template.mjs +++ b/templates/solidity-frameworks/foundry/packages/foundry/script/Deploy.s.sol.template.mjs @@ -6,13 +6,22 @@ pragma solidity ^0.8.19; import "./DeployHelpers.s.sol"; ${deploymentsScriptsImports.filter(Boolean).join("\n")} +/** + * @notice Main deployment script for all contracts + * @dev Run this when you want to deploy multiple contracts at once + * + * Example: yarn deploy # runs this script(without\`--file\` flag) + */ contract DeployScript is ScaffoldETHDeploy { function run() external { + // Deploys all your contracts sequentially + // Add new deployments here when needed + ${deploymentsLogic.filter(Boolean).join("\n")} - // deploy more contracts here - // DeployMyContract deployMyContract = new DeployMyContract(); - // deployMyContract.run(); + // Deploy another contract + // DeployMyContract myContract = new DeployMyContract(); + // myContract.run(); } }`; diff --git a/templates/solidity-frameworks/foundry/packages/foundry/script/DeployHelpers.s.sol b/templates/solidity-frameworks/foundry/packages/foundry/script/DeployHelpers.s.sol index d8bacf668..2d77229c7 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/script/DeployHelpers.s.sol +++ b/templates/solidity-frameworks/foundry/packages/foundry/script/DeployHelpers.s.sol @@ -5,123 +5,117 @@ import { Script, console } from "forge-std/Script.sol"; import { Vm } from "forge-std/Vm.sol"; contract ScaffoldETHDeploy is Script { - error InvalidChain(); - error DeployerHasNoBalance(); - error InvalidPrivateKey(string); - - event AnvilSetBalance(address account, uint256 amount); - event FailedAnvilRequest(); - - struct Deployment { - string name; - address addr; - } - - string root; - string path; - Deployment[] public deployments; - uint256 constant ANVIL_BASE_BALANCE = 10000 ether; - - /// @notice The deployer address for every run - address deployer; - - /// @notice Use this modifier on your run() function on your deploy scripts - modifier ScaffoldEthDeployerRunner() { - deployer = _startBroadcast(); - if (deployer == address(0)) { - revert InvalidPrivateKey("Invalid private key"); + error InvalidChain(); + error DeployerHasNoBalance(); + error InvalidPrivateKey(string); + + event AnvilSetBalance(address account, uint256 amount); + event FailedAnvilRequest(); + + struct Deployment { + string name; + address addr; } - _; - _stopBroadcast(); - exportDeployments(); - } - - function _startBroadcast() internal returns (address) { - vm.startBroadcast(); - (, address _deployer,) = vm.readCallers(); - - if (block.chainid == 31337 && _deployer.balance == 0) { - try this.anvil_setBalance(_deployer, ANVIL_BASE_BALANCE) { - emit AnvilSetBalance(_deployer, ANVIL_BASE_BALANCE); - } catch { - emit FailedAnvilRequest(); - } + + string root; + string path; + Deployment[] public deployments; + uint256 constant ANVIL_BASE_BALANCE = 10000 ether; + + /// @notice The deployer address for every run + address deployer; + + /// @notice Use this modifier on your run() function on your deploy scripts + modifier ScaffoldEthDeployerRunner() { + deployer = _startBroadcast(); + if (deployer == address(0)) { + revert InvalidPrivateKey("Invalid private key"); + } + _; + _stopBroadcast(); + exportDeployments(); } - return _deployer; - } - function _stopBroadcast() internal { - vm.stopBroadcast(); - } + function _startBroadcast() internal returns (address) { + vm.startBroadcast(); + (, address _deployer,) = vm.readCallers(); - function exportDeployments() internal { - // fetch already existing contracts - root = vm.projectRoot(); - path = string.concat(root, "/deployments/"); - string memory chainIdStr = vm.toString(block.chainid); - path = string.concat(path, string.concat(chainIdStr, ".json")); + if (block.chainid == 31337 && _deployer.balance == 0) { + try this.anvil_setBalance(_deployer, ANVIL_BASE_BALANCE) { + emit AnvilSetBalance(_deployer, ANVIL_BASE_BALANCE); + } catch { + emit FailedAnvilRequest(); + } + } + return _deployer; + } - string memory jsonWrite; + function _stopBroadcast() internal { + vm.stopBroadcast(); + } - uint256 len = deployments.length; + function exportDeployments() internal { + // fetch already existing contracts + root = vm.projectRoot(); + path = string.concat(root, "/deployments/"); + string memory chainIdStr = vm.toString(block.chainid); + path = string.concat(path, string.concat(chainIdStr, ".json")); - for (uint256 i = 0; i < len; i++) { - vm.serializeString( - jsonWrite, vm.toString(deployments[i].addr), deployments[i].name - ); + string memory jsonWrite; + + uint256 len = deployments.length; + + for (uint256 i = 0; i < len; i++) { + vm.serializeString(jsonWrite, vm.toString(deployments[i].addr), deployments[i].name); + } + + string memory chainName; + + try this.getChain() returns (Chain memory chain) { + chainName = chain.name; + } catch { + chainName = findChainName(); + } + jsonWrite = vm.serializeString(jsonWrite, "networkName", chainName); + vm.writeJson(jsonWrite, path); } - string memory chainName; + function getChain() public returns (Chain memory) { + return getChain(block.chainid); + } - try this.getChain() returns (Chain memory chain) { - chainName = chain.name; - } catch { - chainName = findChainName(); + function anvil_setBalance(address addr, uint256 amount) public { + string memory addressString = vm.toString(addr); + string memory amountString = vm.toString(amount); + string memory requestPayload = string.concat( + '{"method":"anvil_setBalance","params":["', addressString, '","', amountString, '"],"id":1,"jsonrpc":"2.0"}' + ); + + string[] memory inputs = new string[](8); + inputs[0] = "curl"; + inputs[1] = "-X"; + inputs[2] = "POST"; + inputs[3] = "http://localhost:8545"; + inputs[4] = "-H"; + inputs[5] = "Content-Type: application/json"; + inputs[6] = "--data"; + inputs[7] = requestPayload; + + vm.ffi(inputs); } - jsonWrite = vm.serializeString(jsonWrite, "networkName", chainName); - vm.writeJson(jsonWrite, path); - } - - function getChain() public returns (Chain memory) { - return getChain(block.chainid); - } - - function anvil_setBalance(address addr, uint256 amount) public { - string memory addressString = vm.toString(addr); - string memory amountString = vm.toString(amount); - string memory requestPayload = string.concat( - '{"method":"anvil_setBalance","params":["', - addressString, - '","', - amountString, - '"],"id":1,"jsonrpc":"2.0"}' - ); - - string[] memory inputs = new string[](8); - inputs[0] = "curl"; - inputs[1] = "-X"; - inputs[2] = "POST"; - inputs[3] = "http://localhost:8545"; - inputs[4] = "-H"; - inputs[5] = "Content-Type: application/json"; - inputs[6] = "--data"; - inputs[7] = requestPayload; - - vm.ffi(inputs); - } - - function findChainName() public returns (string memory) { - uint256 thisChainId = block.chainid; - string[2][] memory allRpcUrls = vm.rpcUrls(); - for (uint256 i = 0; i < allRpcUrls.length; i++) { - try vm.createSelectFork(allRpcUrls[i][1]) { - if (block.chainid == thisChainId) { - return allRpcUrls[i][0]; + + function findChainName() public returns (string memory) { + uint256 thisChainId = block.chainid; + string[2][] memory allRpcUrls = vm.rpcUrls(); + for (uint256 i = 0; i < allRpcUrls.length; i++) { + try vm.createSelectFork(allRpcUrls[i][1]) { + if (block.chainid == thisChainId) { + return allRpcUrls[i][0]; + } + } catch { + continue; + } } - } catch { - continue; - } + revert InvalidChain(); } - revert InvalidChain(); - } } diff --git a/templates/solidity-frameworks/foundry/packages/foundry/script/VerifyAll.s.sol b/templates/solidity-frameworks/foundry/packages/foundry/script/VerifyAll.s.sol index 2b307a04d..c6e58669d 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/script/VerifyAll.s.sol +++ b/templates/solidity-frameworks/foundry/packages/foundry/script/VerifyAll.s.sol @@ -11,121 +11,91 @@ import "solidity-bytes-utils/BytesLib.sol"; * @notice will be deleted once the forge/std is updated */ struct FfiResult { - int32 exit_code; - bytes stdout; - bytes stderr; + int32 exit_code; + bytes stdout; + bytes stderr; } interface tempVm { - function tryFfi(string[] calldata) external returns (FfiResult memory); + function tryFfi(string[] calldata) external returns (FfiResult memory); } contract VerifyAll is Script { - uint96 currTransactionIdx; + uint96 currTransactionIdx; - function run() external { - string memory root = vm.projectRoot(); - string memory path = string.concat( - root, - "/broadcast/Deploy.s.sol/", - vm.toString(block.chainid), - "/run-latest.json" - ); - string memory content = vm.readFile(path); + function run() external { + string memory root = vm.projectRoot(); + string memory path = + string.concat(root, "/broadcast/Deploy.s.sol/", vm.toString(block.chainid), "/run-latest.json"); + string memory content = vm.readFile(path); - while (this.nextTransaction(content)) { - _verifyIfContractDeployment(content); - currTransactionIdx++; + while (this.nextTransaction(content)) { + _verifyIfContractDeployment(content); + currTransactionIdx++; + } } - } - function _verifyIfContractDeployment(string memory content) internal { - string memory txType = abi.decode( - vm.parseJson(content, searchStr(currTransactionIdx, "transactionType")), - (string) - ); - if (keccak256(bytes(txType)) == keccak256(bytes("CREATE"))) { - _verifyContract(content); + function _verifyIfContractDeployment(string memory content) internal { + string memory txType = + abi.decode(vm.parseJson(content, searchStr(currTransactionIdx, "transactionType")), (string)); + if (keccak256(bytes(txType)) == keccak256(bytes("CREATE"))) { + _verifyContract(content); + } } - } - function _verifyContract(string memory content) internal { - string memory contractName = abi.decode( - vm.parseJson(content, searchStr(currTransactionIdx, "contractName")), - (string) - ); - address contractAddr = abi.decode( - vm.parseJson(content, searchStr(currTransactionIdx, "contractAddress")), - (address) - ); - bytes memory deployedBytecode = abi.decode( - vm.parseJson(content, searchStr(currTransactionIdx, "transaction.input")), - (bytes) - ); - bytes memory compiledBytecode = abi.decode( - vm.parseJson(_getCompiledBytecode(contractName), ".bytecode.object"), - (bytes) - ); - bytes memory constructorArgs = BytesLib.slice( - deployedBytecode, - compiledBytecode.length, - deployedBytecode.length - compiledBytecode.length - ); + function _verifyContract(string memory content) internal { + string memory contractName = + abi.decode(vm.parseJson(content, searchStr(currTransactionIdx, "contractName")), (string)); + address contractAddr = + abi.decode(vm.parseJson(content, searchStr(currTransactionIdx, "contractAddress")), (address)); + bytes memory deployedBytecode = + abi.decode(vm.parseJson(content, searchStr(currTransactionIdx, "transaction.input")), (bytes)); + bytes memory compiledBytecode = + abi.decode(vm.parseJson(_getCompiledBytecode(contractName), ".bytecode.object"), (bytes)); + bytes memory constructorArgs = + BytesLib.slice(deployedBytecode, compiledBytecode.length, deployedBytecode.length - compiledBytecode.length); - string[] memory inputs = new string[](9); - inputs[0] = "forge"; - inputs[1] = "verify-contract"; - inputs[2] = vm.toString(contractAddr); - inputs[3] = contractName; - inputs[4] = "--chain"; - inputs[5] = vm.toString(block.chainid); - inputs[6] = "--constructor-args"; - inputs[7] = vm.toString(constructorArgs); - inputs[8] = "--watch"; + string[] memory inputs = new string[](9); + inputs[0] = "forge"; + inputs[1] = "verify-contract"; + inputs[2] = vm.toString(contractAddr); + inputs[3] = contractName; + inputs[4] = "--chain"; + inputs[5] = vm.toString(block.chainid); + inputs[6] = "--constructor-args"; + inputs[7] = vm.toString(constructorArgs); + inputs[8] = "--watch"; - FfiResult memory f = tempVm(address(vm)).tryFfi(inputs); + FfiResult memory f = tempVm(address(vm)).tryFfi(inputs); - if (f.stderr.length != 0) { - console.logString( - string.concat( - "Submitting verification for contract: ", vm.toString(contractAddr) - ) - ); - console.logString(string(f.stderr)); - } else { - console.logString(string(f.stdout)); + if (f.stderr.length != 0) { + console.logString(string.concat("Submitting verification for contract: ", vm.toString(contractAddr))); + console.logString(string(f.stderr)); + } else { + console.logString(string(f.stdout)); + } + return; } - return; - } - function nextTransaction(string memory content) external view returns (bool) { - try this.getTransactionFromRaw(content, currTransactionIdx) { - return true; - } catch { - return false; + function nextTransaction(string memory content) external view returns (bool) { + try this.getTransactionFromRaw(content, currTransactionIdx) { + return true; + } catch { + return false; + } } - } - function _getCompiledBytecode( - string memory contractName - ) internal view returns (string memory compiledBytecode) { - string memory root = vm.projectRoot(); - string memory path = - string.concat(root, "/out/", contractName, ".sol/", contractName, ".json"); - compiledBytecode = vm.readFile(path); - } + function _getCompiledBytecode(string memory contractName) internal view returns (string memory compiledBytecode) { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/out/", contractName, ".sol/", contractName, ".json"); + compiledBytecode = vm.readFile(path); + } - function getTransactionFromRaw( - string memory content, - uint96 idx - ) external pure { - abi.decode(vm.parseJson(content, searchStr(idx, "hash")), (bytes32)); - } + function getTransactionFromRaw(string memory content, uint96 idx) external pure { + abi.decode(vm.parseJson(content, searchStr(idx, "hash")), (bytes32)); + } - function searchStr( - uint96 idx, - string memory searchKey - ) internal pure returns (string memory) { - return string.concat(".transactions[", vm.toString(idx), "].", searchKey); - } + function searchStr(uint96 idx, string memory searchKey) internal pure returns (string memory) { + return string.concat(".transactions[", vm.toString(idx), "].", searchKey); + } } diff --git a/templates/solidity-frameworks/foundry/packages/foundry/scripts-js/generateTsAbis.js b/templates/solidity-frameworks/foundry/packages/foundry/scripts-js/generateTsAbis.js index 274f12cb3..0862e875e 100644 --- a/templates/solidity-frameworks/foundry/packages/foundry/scripts-js/generateTsAbis.js +++ b/templates/solidity-frameworks/foundry/packages/foundry/scripts-js/generateTsAbis.js @@ -16,25 +16,78 @@ const generatedContractComment = ` /** * This file is autogenerated by Scaffold-ETH. * You should not edit it manually or your changes might be overwritten. - */ -`; + */`; function getDirectories(path) { return readdirSync(path).filter(function (file) { - return statSync(path + "/" + file).isDirectory(); + return statSync(join(path, file)).isDirectory(); }); } + function getFiles(path) { return readdirSync(path).filter(function (file) { - return statSync(path + "/" + file).isFile(); + return statSync(join(path, file)).isFile(); }); } + +function parseTransactionRun(filePath) { + try { + const content = readFileSync(filePath, "utf8"); + const broadcastData = JSON.parse(content); + return broadcastData.transactions || []; + } catch (error) { + console.warn(`Warning: Could not parse ${filePath}:`, error.message); + return []; + } +} + +function getDeploymentHistory(broadcastPath) { + const files = getFiles(broadcastPath); + const deploymentHistory = new Map(); + + // Sort files to process them in chronological order + const runFiles = files + .filter( + (file) => + file.startsWith("run-") && + file.endsWith(".json") && + !file.includes("run-latest") + ) + .sort((a, b) => { + // Extract run numbers and compare them + const runA = parseInt(a.match(/run-(\d+)/)?.[1] || "0"); + const runB = parseInt(b.match(/run-(\d+)/)?.[1] || "0"); + return runA - runB; + }); + + for (const file of runFiles) { + const transactions = parseTransactionRun(join(broadcastPath, file)); + + for (const tx of transactions) { + if (tx.transactionType === "CREATE") { + // Store or update contract deployment info + deploymentHistory.set(tx.contractAddress, { + contractName: tx.contractName, + address: tx.contractAddress, + deploymentFile: file, + transaction: tx, + }); + } + } + } + + return Array.from(deploymentHistory.values()); +} + function getArtifactOfContract(contractName) { const current_path_to_artifacts = join( __dirname, "..", `out/${contractName}.sol` ); + + if (!existsSync(current_path_to_artifacts)) return null; + const artifactJson = JSON.parse( readFileSync(`${current_path_to_artifacts}/${contractName}.json`) ); @@ -62,32 +115,88 @@ function getInheritedFunctions(mainArtifact) { const inheritedFromContracts = getInheritedFromContracts(mainArtifact); const inheritedFunctions = {}; for (const inheritanceContractName of inheritedFromContracts) { - const { - abi, - ast: { absolutePath }, - } = getArtifactOfContract(inheritanceContractName); - for (const abiEntry of abi) { - if (abiEntry.type == "function") { - inheritedFunctions[abiEntry.name] = absolutePath; + const artifact = getArtifactOfContract(inheritanceContractName); + if (artifact) { + const { + abi, + ast: { absolutePath }, + } = artifact; + for (const abiEntry of abi) { + if (abiEntry.type == "function") { + inheritedFunctions[abiEntry.name] = absolutePath; + } } } } return inheritedFunctions; } +function processAllDeployments(broadcastPath) { + const scriptFolders = getDirectories(broadcastPath); + const allDeployments = new Map(); + + scriptFolders.forEach((scriptFolder) => { + const scriptPath = join(broadcastPath, scriptFolder); + const chainFolders = getDirectories(scriptPath); + + chainFolders.forEach((chainId) => { + const chainPath = join(scriptPath, chainId); + const deploymentHistory = getDeploymentHistory(chainPath); + + deploymentHistory.forEach((deployment) => { + const timestamp = parseInt( + deployment.deploymentFile.match(/run-(\d+)/)?.[1] || "0" + ); + const key = `${chainId}-${deployment.contractName}`; + + // Only update if this deployment is newer + if ( + !allDeployments.has(key) || + timestamp > allDeployments.get(key).timestamp + ) { + allDeployments.set(key, { + ...deployment, + timestamp, + chainId, + deploymentScript: scriptFolder, + }); + } + }); + }); + }); + + const allContracts = {}; + + allDeployments.forEach((deployment) => { + const { chainId, contractName } = deployment; + const artifact = getArtifactOfContract(contractName); + + if (artifact) { + if (!allContracts[chainId]) { + allContracts[chainId] = {}; + } + + allContracts[chainId][contractName] = { + address: deployment.address, + abi: artifact.abi, + inheritedFunctions: getInheritedFunctions(artifact), + deploymentFile: deployment.deploymentFile, + deploymentScript: deployment.deploymentScript, + }; + } + }); + + return allContracts; +} + function main() { - const current_path_to_broadcast = join( - __dirname, - "..", - "broadcast/Deploy.s.sol" - ); + const current_path_to_broadcast = join(__dirname, "..", "broadcast"); const current_path_to_deployments = join(__dirname, "..", "deployments"); - const chains = getDirectories(current_path_to_broadcast); const Deploymentchains = getFiles(current_path_to_deployments); - const deployments = {}; + // Load existing deployments from deployments directory Deploymentchains.forEach((chain) => { if (!chain.endsWith(".json")) return; chain = chain.slice(0, -5); @@ -97,31 +206,31 @@ function main() { deployments[chain] = deploymentObject; }); - const allGeneratedContracts = {}; + // Process all deployments from all script folders + const allGeneratedContracts = processAllDeployments( + current_path_to_broadcast + ); - chains.forEach((chain) => { - allGeneratedContracts[chain] = {}; - const broadCastObject = JSON.parse( - readFileSync(`${current_path_to_broadcast}/${chain}/run-latest.json`) - ); - const transactionsCreate = broadCastObject.transactions.filter( - (transaction) => transaction.transactionType == "CREATE" - ); - transactionsCreate.forEach((transaction) => { - const artifact = getArtifactOfContract(transaction.contractName); - allGeneratedContracts[chain][ - deployments[chain][transaction.contractAddress] || - transaction.contractName - ] = { - address: transaction.contractAddress, - abi: artifact.abi, - inheritedFunctions: getInheritedFunctions(artifact), - }; + // Update contract keys based on deployments if they exist + Object.entries(allGeneratedContracts).forEach(([chainId, contracts]) => { + Object.entries(contracts).forEach(([contractName, contractData]) => { + const deployedName = deployments[chainId]?.[contractData.address]; + if (deployedName) { + // If we have a deployment name, use it instead of the contract name + allGeneratedContracts[chainId][deployedName] = contractData; + delete allGeneratedContracts[chainId][contractName]; + } }); }); - const TARGET_DIR = "../nextjs/contracts/"; + const NEXTJS_TARGET_DIR = "../nextjs/contracts/"; + + // Ensure target directories exist + if (!existsSync(NEXTJS_TARGET_DIR)) { + mkdirSync(NEXTJS_TARGET_DIR, { recursive: true }); + } + // Generate the deployedContracts content const fileContent = Object.entries(allGeneratedContracts).reduce( (content, [chainId, chainConfig]) => { return `${content}${parseInt(chainId).toFixed(0)}:${JSON.stringify( @@ -133,24 +242,31 @@ function main() { "" ); - if (!existsSync(TARGET_DIR)) { - mkdirSync(TARGET_DIR); - } + // Write the files + const fileTemplate = (importPath) => ` + ${generatedContractComment} + import { GenericContractsDeclaration } from "${importPath}"; + + const deployedContracts = {${fileContent}} as const; + + export default deployedContracts satisfies GenericContractsDeclaration; + `; + writeFileSync( - `${TARGET_DIR}deployedContracts.ts`, - format( - `${generatedContractComment} import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; \n\n - const deployedContracts = {${fileContent}} as const; \n\n export default deployedContracts satisfies GenericContractsDeclaration`, - { - parser: "typescript", - } - ) + `${NEXTJS_TARGET_DIR}deployedContracts.ts`, + format(fileTemplate("~~/utils/scaffold-eth/contract"), { + parser: "typescript", + }) + ); + + console.log( + `šŸ“ Updated TypeScript contract definition file on ${NEXTJS_TARGET_DIR}deployedContracts.ts` ); } try { main(); } catch (error) { - console.error(error); + console.error("Error:", error); process.exitCode = 1; } diff --git a/templates/solidity-frameworks/foundry/packages/foundry/scripts-js/parseArgs.js b/templates/solidity-frameworks/foundry/packages/foundry/scripts-js/parseArgs.js new file mode 100644 index 000000000..a9f2fea78 --- /dev/null +++ b/templates/solidity-frameworks/foundry/packages/foundry/scripts-js/parseArgs.js @@ -0,0 +1,115 @@ +import { spawnSync } from "child_process"; +import { config } from "dotenv"; +import { join, dirname } from "path"; +import { readFileSync } from "fs"; +import { parse } from "toml"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +config(); + +// Get all arguments after the script name +const args = process.argv.slice(2); +let fileName = "Deploy.s.sol"; +let network = "localhost"; + +// Show help message if --help is provided +if (args.includes("--help") || args.includes("-h")) { + console.log(` +Usage: yarn deploy [options] +Options: + --file Specify the deployment script file (default: Deploy.s.sol) + --network Specify the network (default: localhost) + --help, -h Show this help message +Examples: + yarn deploy --file DeployYourContract.s.sol --network sepolia + yarn deploy --network sepolia + yarn deploy --file DeployYourContract.s.sol + yarn deploy + `); + process.exit(0); +} + +// Parse arguments +for (let i = 0; i < args.length; i++) { + if (args[i] === "--network" && args[i + 1]) { + network = args[i + 1]; + i++; // Skip next arg since we used it + } else if (args[i] === "--file" && args[i + 1]) { + fileName = args[i + 1]; + i++; // Skip next arg since we used it + } +} + +// Check if the network exists in rpc_endpoints +try { + const foundryTomlPath = join(__dirname, "..", "foundry.toml"); + const tomlString = readFileSync(foundryTomlPath, "utf-8"); + const parsedToml = parse(tomlString); + + if (!parsedToml.rpc_endpoints[network]) { + console.log( + `\nāŒ Error: Network '${network}' not found in foundry.toml!`, + "\nPlease check `foundry.toml` for available networks in the [rpc_endpoints] section or add a new network." + ); + process.exit(1); + } +} catch (error) { + console.error("\nāŒ Error reading or parsing foundry.toml:", error); + process.exit(1); +} + +// Check for default account on live network +if ( + process.env.ETH_KEYSTORE_ACCOUNT === "scaffold-eth-default" && + network !== "localhost" +) { + console.log(` +āŒ Error: Cannot deploy to live network using default keystore account! + +To deploy to ${network}, please follow these steps: + +1. If you haven't generated a keystore account yet: + $ yarn generate + +2. Update your .env file: + ETH_KEYSTORE_ACCOUNT='scaffold-eth-custom' + +The default account (scaffold-eth-default) can only be used for localhost deployments. +`); + process.exit(0); +} + +if ( + process.env.ETH_KEYSTORE_ACCOUNT !== "scaffold-eth-default" && + network === "localhost" +) { + console.log(` +āš ļø Warning: Using ${process.env.ETH_KEYSTORE_ACCOUNT} keystore account on localhost. + +You can either: +1. Enter the password for ${process.env.ETH_KEYSTORE_ACCOUNT} account + OR +2. Set the default keystore account in your .env and re-run the command to skip password prompt: + ETH_KEYSTORE_ACCOUNT='scaffold-eth-default' +`); +} + +// Set environment variables for the make command +process.env.DEPLOY_SCRIPT = `script/${fileName}`; +process.env.RPC_URL = network; + +const result = spawnSync( + "make", + [ + "build-and-deploy", + `DEPLOY_SCRIPT=${process.env.DEPLOY_SCRIPT}`, + `RPC_URL=${process.env.RPC_URL}`, + ], + { + stdio: "inherit", + shell: true, + } +); + +process.exit(result.status);