diff --git a/configs/deployments.config.json b/configs/deployments.config.json new file mode 100644 index 0000000..edb158e --- /dev/null +++ b/configs/deployments.config.json @@ -0,0 +1,12 @@ +{ + "devnet": {}, + "sepolia": { + "HelloStarknet": { + "contract": "HelloStarknet", + "classHash": "0x659beca87b6eb6621573f9155f15f970caf513d419ba46b545b29439e0045c6", + "constructorArgs": [], + "address": "" + } + }, + "mainnet": {} +} \ No newline at end of file diff --git a/configs/scaffold.config.json b/configs/scaffold.config.json new file mode 100644 index 0000000..ae6c393 --- /dev/null +++ b/configs/scaffold.config.json @@ -0,0 +1,13 @@ +{ + "network": "sepolia", + "url": "https://free-rpc.nethermind.io/sepolia-juno/", + "feeToken": "eth", + "maxFee": 894843045483, + "account": { + "name": "scaffold", + "profile": "scaffold" + }, + "contracts": [ + "HelloStarknet" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 1f1fae8..f43928c 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,20 @@ { - "name": "create-starknet-app", - "version": "0.6.0", + "name": "test-create-starknet-app", + "version": "0.5.8", "description": "An open-source starknet development stack", "bin": "./bin/cli.mjs", "scripts": { + "prepare-account": "node scaffold_scripts/prepare-account.js", + "deploy-account": "node scaffold_scripts/deploy-account.js", + "generate-contract-names": "node scaffold_scripts/generateContractNames.mjs", "build-contracts": "cd contracts && scarb build", "test-contracts": "cd contracts && snforge test", "format-contracts": "cd contracts && scarb fmt", + "declare-contract": "node scaffold_scripts/declareContract.mjs", "verify-contracts": "cd contracts && sncast verify --contract-address ${npm_config_contract_address} --contract-name ${npm_config_contract_name} --verifier walnut --network ${npm_config_network}", "contract-scripts": "cd contracts/scripts && sncast script run ${npm_config_script} --url ${npm_config_url}", "generate-interface": "cd contracts && src5_rs parse", - "prepare-account": "cd contracts && sncast account create --url ${npm_config_url} --name ${npm_config_name} --add-profile", - "deploy-account": "cd contracts && sncast --profile ${npm_config_profile} account deploy --name ${npm_config_name} --fee-token ${npm_config_fee_token} --max-fee ${npm_config_max_fee}", "delete-account": "cd contracts && sncast --profile ${npm_config_profile} --accounts-file ${npm_config_accounts_file} account delete --name ${npm_config_name} --network ${npm_config_network}", - "declare-contract": "cd contracts && sncast --profile ${npm_config_profile} declare --contract-name ${npm_config_contract_name} --fee-token ${npm_config_fee_token}", "deploy-contract": "cd contracts && sncast --profile ${npm_config_profile} deploy --fee-token ${npm_config_fee_token} --class-hash ${npm_config_class_hash}", "initialize-dojo": "rm -rf contracts && mkdir contracts && cd contracts && sozo init ${npm_config_name}", "build-dojo": "cd contracts/${npm_config_name} && sozo build", diff --git a/scaffold_scripts/declareContract.mjs b/scaffold_scripts/declareContract.mjs new file mode 100644 index 0000000..3e09192 --- /dev/null +++ b/scaffold_scripts/declareContract.mjs @@ -0,0 +1,168 @@ +import cp, { execSync } from "child_process"; +import { readFileSync, existsSync, writeFileSync } from "fs"; +import { resolve } from "path"; +import { createInterface } from "readline"; +import { promisify } from "util"; +import ora from "ora"; + +// Resolve config path +const configPath = resolve(process.cwd(), "configs/scaffold.config.json"); +const deploymentsPath = resolve( + process.cwd(), + "configs/deployments.config.json" +); + +// convert libs to promises +const exec = promisify(cp.exec); + +// Check if the config file exists +if (!existsSync(configPath) || !existsSync(deploymentsPath)) { + console.error("Error: Config file not found. Ensure the correct setup."); + process.exit(1); +} + +// Load and parse the configuration file +let config; +try { + config = JSON.parse(readFileSync(configPath, "utf-8")); +} catch (error) { + console.error("Error reading or parsing the config file:", error.message); + process.exit(1); +} + +// Initialize readline interface +const rl = createInterface({ + input: process.stdin, + output: process.stdout, +}); + +/** + * Prompt the user with a question and return their response. + * @param {string} question - The question to ask. + * @returns {Promise} - User's input. + */ +const askQuestion = (question) => + new Promise((resolve) => rl.question(question, resolve)); + +/** + * Display contract names and prompt the user to select one. + * @returns {Promise} - The selected contract name. + */ +async function getContract() { + const contracts = config["contracts"] || []; + + if (!contracts.length) { + console.error( + "No contracts found. Please run 'npm run generate-contract-names' and try again." + ); + process.exit(1); + } + + console.log("Available Contracts:"); + contracts.forEach((contract, index) => + console.log(`${index + 1}. ${contract}`) + ); + + while (true) { + const choice = await askQuestion( + `Select the contract to declare (1-${contracts.length}): ` + ); + const selectedIndex = parseInt(choice, 10) - 1; + + if (selectedIndex >= 0 && selectedIndex < contracts.length) { + return contracts[selectedIndex]; + } + + console.log("Invalid choice. Please try again."); + } +} + +/** + * Validate if `sncast` is available in the environment. + */ +async function validateSncast() { + try { + await exec("sncast --version"); + } catch { + console.error("Error: `sncast` is not installed or not in PATH."); + process.exit(1); + } +} + +/** + * Reads a JSON configuration file and updates the deployment details. + * @param {string} network - The network of deployment. + * @param {string} contract - The name of the declared contract. + * @param {string} classHash - The declared class hash. + */ +function updateDeploymentConfig(network, contract, classHash) { + try { + const deployments = JSON.parse(readFileSync(deploymentsPath, "utf-8")); + + if (!deployments[network]) { + deployments[network] = {}; + } + + // Update and write the configuration file + deployments[network][contract] = { + contract, + classHash, + constructorArgs: [], + address: "", + }; + + // Save updated deployments + writeFileSync(deploymentsPath, JSON.stringify(deployments, null, 2)); + } catch (err) { + throw new Error(`Error updating deployments config file: ${err.message}`); + } +} + +(async () => { + const spinner = ora(); + + try { + // Validate `sncast` availability + validateSncast(); + + // Destructure config variables + const { + network, + feeToken, + account: { profile }, + } = config; + + // Get the selected contract name + const contract = await getContract(); + + // Build the command + const command = `cd contracts && sncast --profile ${profile} declare --contract-name ${contract} --fee-token ${feeToken}`; + + spinner.start("Declaring contract..."); + const output = await exec(command); + + if (output.stderr) { + throw new Error(output.stderr); + } + + const classHashMatch = output.stdout.match( + /class_hash:\s*(0x[0-9a-fA-F]+)/ + ); + const classHash = classHashMatch ? classHashMatch[1] : null; + + if (!classHash) { + throw new Error("class_hash not found in command output."); + } + + updateDeploymentConfig(network, contract, classHash); + + spinner.succeed("Contract declared successfully."); + console.log("Run 'npm deploy-contract' to deploy a contract."); + } catch (error) { + spinner.fail("Error during contract declaration."); + console.error("Error:", error.message); + process.exit(1); + } finally { + rl.close(); + } +})(); diff --git a/scaffold_scripts/deploy-account.js b/scaffold_scripts/deploy-account.js new file mode 100644 index 0000000..a008848 --- /dev/null +++ b/scaffold_scripts/deploy-account.js @@ -0,0 +1,26 @@ +const { execSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +// Load configuration +const configPath = path.resolve(__dirname, "../configs/scaffold.config.json"); +const config = JSON.parse(fs.readFileSync(configPath, "utf-8")); + +// Destructure variables +const { + feeToken, + maxFee, + account: { name, profile }, +} = config; + +// Build the command +const command = `cd contracts && sncast --profile ${profile} account deploy --name ${name} --fee-token ${feeToken} --max-fee ${maxFee}`; + +try { + console.log("Running deploy-account command..."); + execSync(command, { stdio: "inherit" }); + console.log("Account deployed successfully!"); +} catch (error) { + console.error("Error deploying account:", error.message); + process.exit(1); +} diff --git a/scaffold_scripts/generateContractNames.mjs b/scaffold_scripts/generateContractNames.mjs new file mode 100644 index 0000000..f47af5e --- /dev/null +++ b/scaffold_scripts/generateContractNames.mjs @@ -0,0 +1,63 @@ +import { readdirSync, readFileSync, writeFileSync } from "fs"; +import { resolve, join } from "path"; + +const configPath = resolve(process.cwd(), "configs/scaffold.config.json"); +const contractsDir = resolve(process.cwd(), "contracts/src"); + +/** + * Scans the contracts directory and returns an array of contract names. + * @returns {string[]} An array of contract names. + */ +function getContractNames() { + try { + // Read all files in the contracts directory with .cairo extension + const cairoFiles = readdirSync(contractsDir).filter((file) => + file.endsWith(".cairo") + ); + + // Extract contract names using regex + return cairoFiles.flatMap((file) => { + const filePath = join(contractsDir, file); + const content = readFileSync(filePath, "utf-8"); + const contractRegex = /#\[\s*starknet::contract\s*]\s*mod\s+(\w+)/g; + const matches = [...content.matchAll(contractRegex)]; + return matches.map((match) => match[1]); // Extract the contract name from each match + }); + } catch (err) { + throw new Error(`Failed to read contract files: ${err.message}`); + } +} + +/** + * Reads a JSON configuration file and updates the contract names array + * @param {string[]} contractNames - The array of contract names for updating. + */ +function updateConfigFileWithContractNames(contractNames) { + try { + const config = JSON.parse(readFileSync(configPath, "utf-8")); + + // Update and write the configuration file + config["contract-names"] = contractNames; + writeFileSync(configPath, JSON.stringify(config, null, 2)); + + console.log("Contract names added to config file successfully!"); + } catch (err) { + throw new Error(`Error updating config file: ${err.message}`); + } +} + +/** + * Main function to scan contracts and update the configuration file. + */ +function main() { + try { + const contractNames = getContractNames(); + console.log("Contracts found:", contractNames); + + updateConfigFileWithContractNames(contractNames); + } catch (err) { + console.error(err.message); + } +} + +main(); diff --git a/scaffold_scripts/prepare-account.js b/scaffold_scripts/prepare-account.js new file mode 100644 index 0000000..827bd4a --- /dev/null +++ b/scaffold_scripts/prepare-account.js @@ -0,0 +1,25 @@ +const { execSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +// Load configuration +const configPath = path.resolve(__dirname, "../configs/scaffold.config.json"); +const config = JSON.parse(fs.readFileSync(configPath, "utf-8")); + +// Destructure variables +const { + url, + account: { name, profile }, +} = config; + +// Build the command +const command = `cd contracts && sncast account create --url ${url} --name ${name} --add-profile ${profile}`; + +try { + console.log("Running prepare-account command..."); + execSync(command, { stdio: "inherit" }); + console.log("Account prepared successfully!"); +} catch (error) { + console.error("Error preparing account:", error.message); + process.exit(1); +}