From d3a471cb9602b207d921cc9b242422cb28dcd218 Mon Sep 17 00:00:00 2001 From: wsdt Date: Thu, 8 Aug 2024 20:35:31 +0200 Subject: [PATCH] feat: local deploy script --- contracts/.env-example | 4 +- contracts/contracts/core/HCHelper.sol | 200 +++++++++++------------ contracts/hardhat.config.ts | 18 ++- contracts/script/LocalDeploy.s.sol | 84 ++++++++++ contracts/script/deploy-local.ts | 218 ++++++++++++++++++++++---- contracts/script/deploy.s.sol | 25 ++- contracts/script/utils.ts | 133 +++++++++++++++- frontend/.env-template | 1 - 8 files changed, 532 insertions(+), 151 deletions(-) create mode 100644 contracts/script/LocalDeploy.s.sol diff --git a/contracts/.env-example b/contracts/.env-example index c9dfe89..5ba9681 100644 --- a/contracts/.env-example +++ b/contracts/.env-example @@ -4,4 +4,6 @@ TOKEN_PRICE_CONTRACT= HC_HELPER_ADDR= BACKEND_URL= PRIVATE_KEY= -RPC_URL= \ No newline at end of file +RPC_URL= +CLIENT_ADDR= +BUNDLER_RPC= \ No newline at end of file diff --git a/contracts/contracts/core/HCHelper.sol b/contracts/contracts/core/HCHelper.sol index 0c51287..cf8cbae 100644 --- a/contracts/contracts/core/HCHelper.sol +++ b/contracts/contracts/core/HCHelper.sol @@ -1,48 +1,36 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.12; - import "../interfaces/INonceManager.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - contract HCHelper { using SafeERC20 for IERC20; - // Response data is stored here by PutResponse() and then consumed by TryCallOffchain(). // The storage slot must not be changed unless the corresponding code is updated in the Bundler. - mapping(bytes32=>bytes) ResponseCache; - + mapping(bytes32 => bytes) ResponseCache; // Owner (creator) of this contract. address public owner; - // BOBA token address address public tokenAddr; - // Token amount required to purchase each prepaid credit (may be 0 for testing) uint256 public pricePerCall; - // Account which is used to insert system error responses. Currently a single // address but could be extended to a list of authorized accounts if needed. address public systemAccount; - // Data stored per RegisteredCaller struct callerInfo { - address owner; - string url; - uint256 credits; + address owner; + string url; + uint256 credits; } - // Contracts which are allowed to use Hybrid Compute. - mapping(address=>callerInfo) public RegisteredCallers; - + mapping(address => callerInfo) public RegisteredCallers; // AA EntryPoint address immutable entryPoint; - // Constructor constructor(address _entryPoint, address _tokenAddr) { - entryPoint = _entryPoint; - tokenAddr = _tokenAddr; + entryPoint = _entryPoint; + tokenAddr = _tokenAddr; } - // Initialize system addresses. Note - can be called again to change // these addresses if necessary. function initialize(address _owner, address _systemAccount) public { @@ -50,149 +38,161 @@ contract HCHelper { owner = _owner; systemAccount = _systemAccount; } - // Change the SystemAccount address (used for error responses) function SetSystemAccount(address _systemAccount) public { require(msg.sender == owner, "Only owner"); systemAccount = _systemAccount; } - // Temporary method, until an auto-registration protocol is developed. function RegisterUrl(address contract_addr, string calldata url) public { require(msg.sender == owner, "Only owner"); RegisteredCallers[contract_addr].owner = msg.sender; RegisteredCallers[contract_addr].url = url; } - // Set or change the per-call token price (0 is allowed). Does not affect // existing credit balances, only applies to new AddCredit() calls. function SetPrice(uint256 _pricePerCall) public { require(msg.sender == owner, "Only owner"); - pricePerCall = _pricePerCall; + pricePerCall = _pricePerCall; } - // Purchase credits allowing the specified contract to perform HC calls. // The token cost is (pricePerCall() * numCredits) and is non-refundable function AddCredit(address contract_addr, uint256 numCredits) public { if (pricePerCall > 0) { uint256 tokenPrice = numCredits * pricePerCall; - IERC20(tokenAddr).safeTransferFrom(msg.sender, address(this), tokenPrice); - } + IERC20(tokenAddr).safeTransferFrom( + msg.sender, + address(this), + tokenPrice + ); + } RegisteredCallers[contract_addr].credits += numCredits; } - // Allow the owner to withdraw tokens function WithdrawTokens(uint256 amount, address withdrawTo) public { require(msg.sender == owner, "Only owner"); IERC20(tokenAddr).safeTransferFrom(address(this), withdrawTo, amount); } - // Called from a HybridAccount contract, to populate the response which it will // subsequently request in TryCallOffchain() function PutResponse(bytes32 subKey, bytes calldata response) public { - require(RegisteredCallers[msg.sender].owner != address(0), "Unregistered caller"); - require(response.length >= 32*4, "Response too short"); - - (,, uint32 errCode,) = abi.decode(response,(address, uint256, uint32, bytes)); + require( + RegisteredCallers[msg.sender].owner != address(0), + "Unregistered caller" + ); + require(response.length >= 32 * 4, "Response too short"); + (, , uint32 errCode, ) = abi.decode( + response, + (address, uint256, uint32, bytes) + ); require(errCode < 2, "invalid errCode for PutResponse()"); - bytes32 mapKey = keccak256(abi.encodePacked(msg.sender, subKey)); ResponseCache[mapKey] = response; } - // Allow the system to supply an error response for unsuccessful requests. // Any such response will only be retrieved if there was nothing supplied // by PutResponse() function PutSysResponse(bytes32 subKey, bytes calldata response) public { - require(msg.sender == systemAccount, "Only systemAccount may call PutSysResponse"); - require(response.length >= 32*4, "Response too short"); - - (,, uint32 errCode,) = abi.decode(response,(address, uint256, uint32, bytes)); - require(errCode >= 2, "PutSysResponse() may only be used for error responses"); - + require( + msg.sender == systemAccount, + "Only systemAccount may call PutSysResponse" + ); + require(response.length >= 32 * 4, "Response too short"); + (, , uint32 errCode, ) = abi.decode( + response, + (address, uint256, uint32, bytes) + ); + require( + errCode >= 2, + "PutSysResponse() may only be used for error responses" + ); bytes32 mapKey = keccak256(abi.encodePacked(address(this), subKey)); ResponseCache[mapKey] = response; } - // Remove one or more map entries (only needed if response was not retrieved normally). function RemoveResponses(bytes32[] calldata mapKeys) public { - require(msg.sender == systemAccount, "Only systemAccount may call RemoveResponses"); - for (uint32 i = 0; i < mapKeys.length; i++) { - delete(ResponseCache[mapKeys[i]]); - } + require( + msg.sender == systemAccount, + "Only systemAccount may call RemoveResponses" + ); + for (uint32 i = 0; i < mapKeys.length; i++) { + delete (ResponseCache[mapKeys[i]]); + } } - // Try to retrieve an entry, also removing it from the mapping. This // function will check for stale entries by checking the nonce of the srcAccount. // Stale entries will return a "not found" condition. - function getEntry(bytes32 mapKey) internal returns (bool, uint32, bytes memory) { + function getEntry( + bytes32 mapKey + ) internal returns (bool, uint32, bytes memory) { bytes memory entry; - bool found; - uint32 errCode; - bytes memory response; - address srcAddr; - uint256 srcNonce; - - entry = ResponseCache[mapKey]; - if (entry.length == 1) { + bool found; + uint32 errCode; + bytes memory response; + address srcAddr; + uint256 srcNonce; + entry = ResponseCache[mapKey]; + if (entry.length == 1) { // Used during state simulation to verify that a trigger request actually came from this helper contract revert("_HC_VRFY"); - } else if (entry.length != 0) { - found = true; - (srcAddr, srcNonce, errCode, response) = abi.decode(entry,(address, uint256, uint32, bytes)); - uint192 nonceKey = uint192(srcNonce >> 64); - + } else if (entry.length != 0) { + found = true; + (srcAddr, srcNonce, errCode, response) = abi.decode( + entry, + (address, uint256, uint32, bytes) + ); + uint192 nonceKey = uint192(srcNonce >> 64); INonceManager NM = INonceManager(entryPoint); - uint256 actualNonce = NM.getNonce(srcAddr, nonceKey); - - if (srcNonce + 1 != actualNonce) { - // stale entry - found = false; - errCode = 0; - response = "0x"; - } - - delete(ResponseCache[mapKey]); - } - return (found, errCode, response); + uint256 actualNonce = NM.getNonce(srcAddr, nonceKey); + if (srcNonce + 1 != actualNonce) { + // stale entry + found = false; + errCode = 0; + response = "0x"; + } + delete (ResponseCache[mapKey]); + } + return (found, errCode, response); } - // Make an offchain call to a pre-registered endpoint. - function TryCallOffchain(bytes32 userKey, bytes memory req) public returns (uint32, bytes memory) { + function TryCallOffchain( + bytes32 userKey, + bytes memory req + ) public returns (uint32, bytes memory) { bool found; - uint32 errCode; + uint32 errCode; bytes memory ret; - - require(RegisteredCallers[msg.sender].owner != address(0), "Calling contract not registered"); - - if (RegisteredCallers[msg.sender].credits == 0) { - return (5, "Insufficient credit"); - } - RegisteredCallers[msg.sender].credits -= 1; - + require( + RegisteredCallers[msg.sender].owner != address(0), + "Calling contract not registered" + ); + if (RegisteredCallers[msg.sender].credits == 0) { + return (5, "Insufficient credit"); + } + RegisteredCallers[msg.sender].credits -= 1; bytes32 subKey = keccak256(abi.encodePacked(userKey, req)); bytes32 mapKey = keccak256(abi.encodePacked(msg.sender, subKey)); - (found, errCode, ret) = getEntry(mapKey); - - if (found) { - return (errCode, ret); - } else { - // If no off-chain response, check for a system error response. + if (found) { + return (errCode, ret); + } else { + // If no off-chain response, check for a system error response. bytes32 errKey = keccak256(abi.encodePacked(address(this), subKey)); - - (found, errCode, ret) = getEntry(errKey); - if (found) { - require(errCode >= 2, "invalid errCode"); - return (errCode, ret); - } else { - // Nothing found, so trigger a new request. + (found, errCode, ret) = getEntry(errKey); + if (found) { + require(errCode >= 2, "invalid errCode"); + return (errCode, ret); + } else { + // Nothing found, so trigger a new request. bytes memory prefix = "_HC_TRIG"; - bytes memory r2 = bytes.concat(prefix, abi.encodePacked(msg.sender, userKey, req)); + bytes memory r2 = bytes.concat( + prefix, + abi.encodePacked(msg.sender, userKey, req) + ); assembly { revert(add(r2, 32), mload(r2)) - } - } - } + } + } + } } } diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 9852bad..c151c7a 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -29,13 +29,17 @@ const config: HardhatUserConfig & { sourcify: { enabled: false, }, - // networks: { - // boba_sepolia: { - // url: process.env.RPC_URL, - // accounts: [privateKey], - // allowUnlimitedContractSize: true, - // }, - // }, + networks: { + boba_sepolia: { + url: process.env.RPC_URL, + accounts: [privateKey], + allowUnlimitedContractSize: true, + }, + boba_local: { + url: "http://localhost:9545", + accounts: [privateKey], + }, + }, etherscan: { apiKey: { boba_sepolia: "boba", diff --git a/contracts/script/LocalDeploy.s.sol b/contracts/script/LocalDeploy.s.sol new file mode 100644 index 0000000..f6eb989 --- /dev/null +++ b/contracts/script/LocalDeploy.s.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import "../contracts/core/EntryPoint.sol"; +import "../contracts/core/HCHelper.sol"; +import "../contracts/samples/HybridAccountFactory.sol"; +import "../contracts/samples/SimpleAccountFactory.sol"; + +contract LocalDeploy is Script { + function run() external + returns (address[5] memory) { + address deployAddr = vm.envAddress("DEPLOY_ADDR"); + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address hcSysOwner = vm.envAddress("HC_SYS_OWNER"); + require (hcSysOwner != address(0), "HC_SYS_OWNER not set"); + uint256 deploySalt = vm.envOr("DEPLOY_SALT",uint256(0)); // Change this to force redeployment of contracts + + address bobaAddr = vm.envOr("BOBA_TOKEN", 0x4200000000000000000000000000000000000023); + + EntryPoint ept; + HCHelper helper; + SimpleAccountFactory saf; + HybridAccountFactory haf; + HybridAccount ha0; + + bytes32 salt_val = bytes32(deploySalt); + uint112 min_deposit = 0.001 ether; + + vm.startBroadcast(deployerPrivateKey); + + { // Limit variable scope to avoid "stack too deep" + address entryPointAddr = vm.envOr("ENTRY_POINTS", 0x0000000000000000000000000000000000000000); // Must be a single value despite the variable name + if (entryPointAddr != address(0) && entryPointAddr.code.length > 0) { + ept = EntryPoint(payable(entryPointAddr)); + } else { + ept = new EntryPoint{salt: salt_val}(); + } + } + { + address helperAddr = vm.envOr("HC_HELPER_ADDR", 0x0000000000000000000000000000000000000000); + if (helperAddr != address(0) && helperAddr.code.length > 0) { + helper = HCHelper(helperAddr); + } else { + helper = new HCHelper{salt: salt_val}(address(ept), bobaAddr); + } + } + { + address safAddr = vm.envOr("SA_FACTORY_ADDR", 0x0000000000000000000000000000000000000000); + if (safAddr != address(0) && safAddr.code.length > 0) { + saf = SimpleAccountFactory(safAddr); + } else { + saf = new SimpleAccountFactory(ept); + } + } + { + address hafAddr = vm.envOr("HA_FACTORY_ADDR", 0x0000000000000000000000000000000000000000); + if (hafAddr != address(0) && hafAddr.code.length > 0) { + haf = HybridAccountFactory(hafAddr); + } else { + haf = new HybridAccountFactory(ept, address(helper)); + } + } + { + address ha0Addr = vm.envOr("HC_SYS_ACCOUNT", 0x0000000000000000000000000000000000000000); + if (ha0Addr != address(0) && ha0Addr.code.length > 0) { + ha0 = HybridAccount(payable(ha0Addr)); + } else { + ha0 = haf.createAccount(hcSysOwner,0); + } + } + if (helper.systemAccount() != address(ha0)) { + helper.initialize(deployAddr, address(ha0)); + } + + (uint112 bal,,,,) = ept.deposits(address(ha0)); + if (bal < min_deposit) { + ept.depositTo{value: min_deposit - bal}(address(ha0)); + } + + vm.stopBroadcast(); + return [address(ept),address(helper), address(saf), address(haf), address(ha0)]; + } +} diff --git a/contracts/script/deploy-local.ts b/contracts/script/deploy-local.ts index e3f4ec9..a260408 100644 --- a/contracts/script/deploy-local.ts +++ b/contracts/script/deploy-local.ts @@ -85,8 +85,7 @@ const execPromise = ( }); }; -const updateEnvVariable = (key: string, value: string) => { - const envPath = path.resolve(__dirname, "../.env"); +const updateEnvVariable = (key: string, value: string, envPath: string) => { let envFile = fs.readFileSync(envPath, "utf8"); const regex = new RegExp(`^${key}=.*`, "m"); if (regex.test(envFile)) { @@ -101,7 +100,7 @@ const updateEnvVariable = (key: string, value: string) => { const parseLocalDeployAddresses = () => { const jsonPath = path.resolve( __dirname, - "../broadcast/LocalDeploy.s.sol/901/run-latest.json" + "../broadcast/deploy.s.sol/901/run-latest.json" ); try { @@ -114,21 +113,21 @@ const parseLocalDeployAddresses = () => { address: transaction.contractAddress ?? "", })); - const hybridAccount = jsonData.transactions - .map((transaction: any) => { - if ( - transaction.contractName === "HybridAccountFactory" && - transaction.function === "createAccount(address,uint256)" - ) { - return { - contractName: "HybridAccount", - address: transaction.additionalContracts[0].address, - }; - } - }) - .filter((ha: any) => ha); // filter out undefined values - - contracts.push(...hybridAccount); + // const hybridAccount = jsonData.transactions + // .map((transaction: any) => { + // if ( + // transaction.contractName === "HybridAccountFactory" && + // transaction.function === "createAccount(address,uint256)" + // ) { + // return { + // contractName: "HybridAccount", + // address: transaction.additionalContracts[0].address, + // }; + // } + // }) + // .filter((ha: any) => ha); // filter out undefined values + + // contracts.push(...hybridAccount); console.log("Parsed JSON data:", contracts); return contracts; @@ -137,25 +136,55 @@ const parseLocalDeployAddresses = () => { } }; +const deleteIgnitionDeployments = () => { + const deploymentsPath = path.resolve(__dirname, "../ignition/deployments"); + if (fs.existsSync(deploymentsPath)) { + fs.rmSync(deploymentsPath, { recursive: true, force: true }); + console.log("Ignition deployments folder deleted."); + } else { + console.log( + "Ignition deployments folder does not exist. Skipping deletion." + ); + } +}; + // TODO: fix .env file loading. Currently .env needs to be in /script directory async function main() { try { - updateEnvVariable("DEPLOY_ADDR", deployAddr); - updateEnvVariable("HC_SYS_OWNER", ha0Owner); - updateEnvVariable("BUNDLER_ADDR", bundlerAddr); - //updateEnvVariable("PRIVATE_KEY", deployKey) - console.log("privkey: ", process.env.PRIVATE_KEY); - //await execPromise("node fundL2.js") - //await execPromise("forge script --json --broadcast --silent --rpc-url=http://127.0.0.1:9545 prefund.s.sol") - //const result = await execPromise("forge script --json --broadcast --silent --rpc-url=http://127.0.0.1:9545 deploy.s.sol") - //console.log(result) - //await execPromise("node fundL2.js"); - // const output = await execPromise( - // "forge script --json --broadcast --silent --rpc-url=http://127.0.0.1:9545 LocalDeploy.s.sol:LocalDeploy" - // ); - // console.log("output:", output); + await execPromise( + "pnpm install", + [], + path.resolve(__dirname, "../../boba") + ); + await execPromise( + "make devnet-hardhat-up", + [], + path.resolve(__dirname, "../../boba") + ); + + const fundL2Vars = { + ...process.env, + PRIVATE_KEY: deployKey, + }; + await execPromise("node fundL2.js", undefined, undefined, fundL2Vars); + console.log("Funding L2 done..."); + + const baseDeployVars = { + ...process.env, + PRIVATE_KEY: deployKey, + BUNDLER_ADDR: bundlerAddr, + HC_SYS_OWNER: ha1Owner, + DEPLOY_ADDR: deployAddr, + BACKEND_URL: process.env.BACKEND_URL, + }; + + await execPromise( + "forge script --json --broadcast --silent --rpc-url=http://127.0.0.1:9545 deploy.s.sol", + undefined, + undefined, + baseDeployVars + ); const contracts = parseLocalDeployAddresses(); - // HC_HELPER_ADDR=0x7d7c459eb74b9f6bd92c9605d6c77f404809bded HC_SYS_ACCOUNT=0x72f4a4fbdd19e8dee9680758c5542b1677125d27 HC_SYS_OWNER=0x2A9099A58E0830A4Ab418c2a19710022466F1ce7 HC_SYS_PRIVKEY=0x75cd983f0f4714969b152baa258d849473732905e2301467303dacf5a09fdd57 ENTRY_POINTS=0xb8fa1a952843bd53d711239b0264d90dc973ca27 BUILDER_PRIVKEY=0xf91be07ef5a01328015cae4f2e5aefe3c4577a90abb8e2e913fe071b0e3732ed NODE_HTTP=http://192.168.178.59:9545 CHAIN_ID=901 const envVars = { HC_HELPER_ADDR: contracts?.find((c) => c.contractName === "HCHelper") ?.address, @@ -169,13 +198,134 @@ async function main() { NODE_HTTP: `http://${getLocalIpAddress()}:9545`, CHAIN_ID: "901", }; + await execPromise( "docker compose up -d --build rundler-hc", [], path.resolve(__dirname, "../../rundler-hc/hybrid-compute/"), { ...process.env, ...envVars } ); - await execPromise("docker compose up -d", [], path.resolve(__dirname, "../../backend")) + + // + deleteIgnitionDeployments(); + console.log( + "HA ADDRESS: ", + contracts?.find((c) => c.contractName === "HybridAccount")?.address + ); + console.log("path: ", path.resolve(__dirname, "../")); + const ignitionOutput = await execPromise( + "npx hardhat ignition deploy ./ignition/modules/TokenPrice.ts --network boba_local", + ["y"], + path.resolve(__dirname, "../"), + { + ...process.env, + HYBRID_ACCOUNT: contracts?.find( + (c) => c.contractName === "HybridAccount" + )?.address, + } + ); + + const tokenPriceMatch = ignitionOutput.match( + /TokenPrice#TokenPrice - (0x[a-fA-F0-9]{40})/ + ); + if (!tokenPriceMatch) { + throw new Error( + "Failed to extract TokenPrice address from Ignition output" + ); + } + const tokenPriceAddress = tokenPriceMatch[1]; + console.log("TokenPrice Contract deployed to: ", tokenPriceAddress); + + // Frontend env vars + const frontendEnvPath = path.resolve(__dirname, "../../frontend/.env"); + updateEnvVariable("VITE_PRIVATE_KEY", deployKey, frontendEnvPath); + // updateEnvVariable("VITE_CLIENT_ADDR", ) // not needed i think + updateEnvVariable( + "VITE_SMART_CONTRACT", + tokenPriceAddress, + frontendEnvPath + ); + updateEnvVariable( + "VITE_ENTRY_POINT", + contracts?.find((c) => c.contractName === "EntryPoint")?.address ?? "", + frontendEnvPath + ); + updateEnvVariable( + "VITE_RPC_PROVIDER", + "http://localhost:9545", + frontendEnvPath + ); + + console.log("Frontend ENV vars set..."); + + // Backend env vars + const backendEnvPath = path.resolve(__dirname, "../../backend/.env"); + updateEnvVariable( + "OC_HYBRID_ACCOUNT", + contracts?.find((c) => c.contractName === "HybridAccount")?.address ?? "", + backendEnvPath + ); + updateEnvVariable( + "ENTRY_POINTS", + contracts?.find((c) => c.contractName === "EntryPoint")?.address ?? "", + backendEnvPath + ); + updateEnvVariable("CHAIN_ID", "901", backendEnvPath); + updateEnvVariable("OC_PRIVKEY", deployKey, backendEnvPath); + updateEnvVariable( + "HC_HELPER_ADDR", + contracts?.find((c) => c.contractName === "HCHelper")?.address ?? "", + backendEnvPath + ); + + console.log("Backend ENV vars set..."); + + // Contract env vars + const contractsEnvPath = path.resolve(__dirname, "../.env"); + updateEnvVariable( + "HYBRID_ACCOUNT", + contracts?.find((c) => c.contractName === "HybridAccount")?.address ?? "", + contractsEnvPath + ); + updateEnvVariable( + "ENTRY_POINT", + contracts?.find((c) => c.contractName === "EntryPoint")?.address ?? "", + contractsEnvPath + ); + updateEnvVariable( + "TOKEN_PRICE_CONTRACT", + tokenPriceAddress, + contractsEnvPath + ); + updateEnvVariable( + "HC_HELPER_ADDR", + contracts?.find((c) => c.contractName === "HCHelper")?.address ?? "", + contractsEnvPath + ); + updateEnvVariable("PRIVATE_KEY", deployKey, contractsEnvPath); + updateEnvVariable( + "CLIENT_ADDR", + contracts?.find((c) => c.contractName === "SimpleAccount")?.address ?? "", + contractsEnvPath + ); + + console.log("Contracts ENV vars set..."); + + await execPromise( + "docker compose up -d", + [], + path.resolve(__dirname, "../../backend") + ); + + console.log("Backend container started..."); + + await execPromise( + "docker compose up -d", + [], + path.resolve(__dirname, "../../frontend") + ); + + console.log("Frontend container started..."); } catch (error) { console.error(error); } diff --git a/contracts/script/deploy.s.sol b/contracts/script/deploy.s.sol index 636f44f..c4250b6 100644 --- a/contracts/script/deploy.s.sol +++ b/contracts/script/deploy.s.sol @@ -23,6 +23,7 @@ contract DeployExample is Script { HybridAccount public hybridAccount; TokenPrice public tokenPrice; SimpleAccount public simpleAccount; + SimpleAccountFactory public saf; function run() public { // Prepare and start Broadcast @@ -43,19 +44,29 @@ contract DeployExample is Script { function deployContracts() public { entrypoint = new EntryPoint(); - hcHelper = new HCHelper(address(entrypoint), address(0x4200000000000000000000000000000000000023)); - hybridAccount = new HybridAccount(IEntryPoint(entrypoint), address(hcHelper)); - tokenPrice = new TokenPrice(payable(address(hcHelper))); + hcHelper = new HCHelper( + address(entrypoint), + address(0x4200000000000000000000000000000000000023) + ); + hybridAccount = new HybridAccount( + IEntryPoint(entrypoint), + address(hcHelper) + ); + tokenPrice = new TokenPrice(payable(hybridAccount)); + saf = new SimpleAccountFactory(entrypoint); simpleAccount = new SimpleAccount(IEntryPoint(entrypoint)); } function configureContracts() public { if (hcHelper.systemAccount() != address(hybridAccount)) { - hcHelper.initialize(msg.sender, address(hybridAccount)); + hcHelper.initialize(deployerAddress, address(hybridAccount)); + hcHelper.SetPrice(0); } - (uint112 bal,,,,) = entrypoint.deposits(address(hybridAccount)); + (uint112 bal, , , , ) = entrypoint.deposits(address(hybridAccount)); if (bal < 0.01 ether) { - entrypoint.depositTo{value: 0.01 ether - bal}(address(address(hybridAccount))); + entrypoint.depositTo{value: 0.01 ether - bal}( + address(address(hybridAccount)) + ); } // register url, add credit @@ -78,4 +89,4 @@ contract DeployExample is Script { console.log("CLIENT_PRIVKEY=", deployerPrivateKey); console.log("HC_SYS_OWNER", address(deployerAddress)); } -} \ No newline at end of file +} diff --git a/contracts/script/utils.ts b/contracts/script/utils.ts index 479afe0..3c69025 100644 --- a/contracts/script/utils.ts +++ b/contracts/script/utils.ts @@ -1,4 +1,9 @@ import * as os from "os"; +import * as fs from "fs"; +import * as path from "path"; +import { ProcessEnvOptions, spawn } from "child_process"; +import { Readable } from "stream"; + export const getLocalIpAddress = () => { const networkInterfaces = os.networkInterfaces(); for (const interfaceName in networkInterfaces) { @@ -9,5 +14,131 @@ export const getLocalIpAddress = () => { } } } - throw new Error("No Local IP-Address found") + throw new Error("No Local IP-Address found"); +}; + +const execPromise = ( + command: string, + inputs: string[] = [], + cwd: ProcessEnvOptions["cwd"] = undefined, + env?: NodeJS.ProcessEnv +): Promise => { + return new Promise((resolve, reject) => { + const [cmd, ...args] = command.split(" "); + const child = spawn(cmd, args, { + stdio: ["pipe", "pipe", "pipe"], + cwd, + env, + }); + + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", (data) => { + stdout += data.toString(); + console.log(data.toString()); + }); + + child.stderr.on("data", (data) => { + stderr += data.toString(); + console.error(data.toString()); + }); + + const stdinStream = new Readable({ + read() { + inputs.forEach((input) => { + this.push(input + "\n"); + }); + this.push(null); + }, + }); + + stdinStream.pipe(child.stdin); + + child.on("close", (code) => { + if (code !== 0) { + reject(new Error(`Command failed with exit code ${code}`)); + } else { + resolve(stdout); + } + }); + }); +}; + +const updateEnvVariable = (key: string, value: string) => { + const envPath = path.resolve(__dirname, "../.env"); + let envFile = fs.readFileSync(envPath, "utf8"); + const regex = new RegExp(`^${key}=.*`, "m"); + if (regex.test(envFile)) { + envFile = envFile.replace(regex, `${key}=${value}`); + } else { + envFile += `\n${key}=${value}`; + } + fs.writeFileSync(envPath, envFile); +}; + +const parseLocalDeployAddresses = () => { + const jsonPath = path.resolve( + __dirname, + "../broadcast/LocalDeploy.s.sol/901/run-latest.json" + ); + + try { + const data = fs.readFileSync(jsonPath, "utf8"); + const jsonData = JSON.parse(data); + + const contracts: { contractName: string; address: string }[] = + jsonData.transactions.map((transaction: any) => ({ + contractName: transaction.contractName ?? "", + address: transaction.contractAddress ?? "", + })); + + const hybridAccount = jsonData.transactions + .map((transaction: any) => { + if ( + transaction.contractName === "HybridAccountFactory" && + transaction.function === "createAccount(address,uint256)" + ) { + return { + contractName: "HybridAccount", + address: transaction.additionalContracts[0].address, + }; + } + }) + .filter((ha: any) => ha); // filter out undefined values + + contracts.push(...hybridAccount); + + console.log("Parsed JSON data:", contracts); + return contracts; + } catch (err) { + console.error("Error reading or parsing the file:", err); + } +}; + +const readHybridAccountAddress = () => { + const jsonPath = path.resolve( + __dirname, + "../broadcast/deploy-hybrid-account.s.sol/28882/run-latest.json" + ); + const jsonContent = JSON.parse(fs.readFileSync(jsonPath, "utf8")); + const transaction = jsonContent.transactions.find( + (tx: any) => tx.transactionType === "CREATE" + ); + if (!transaction) { + throw new Error("CREATE transaction not found in the JSON file"); + } + return transaction.contractAddress; +}; + +const deleteIgnitionDeployments = () => { + const deploymentsPath = path.resolve(__dirname, "../ignition/deployments"); + if (fs.existsSync(deploymentsPath)) { + fs.rmSync(deploymentsPath, { recursive: true, force: true }); + console.log("Ignition deployments folder deleted."); + } else { + console.log( + "Ignition deployments folder does not exist. Skipping deletion." + ); + } }; diff --git a/frontend/.env-template b/frontend/.env-template index 6599d96..75dfe7c 100644 --- a/frontend/.env-template +++ b/frontend/.env-template @@ -1,5 +1,4 @@ VITE_ENTRY_POINT= VITE_PRIVATE_KEY= -VITE_CLIENT_ADDR= VITE_SMART_CONTRACT= VITE_RPC_PROVIDER= \ No newline at end of file