diff --git a/bin/cli.mjs b/bin/cli.mjs index 396ab94..23b87be 100755 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -6,7 +6,8 @@ import fs from "fs"; import path from "path"; import ora from "ora"; -const git_repo = "https://github.com/horuslabsio/Starknet-Scaffold"; +const git_repo = "https://github.com/horuslabsio/Starknet-Scaffold.git"; +const dojo_starter = "https://github.com/dojoengine/dojo-starter.git"; // convert libs to promises const exec = promisify(cp.exec); @@ -15,7 +16,7 @@ const rm = promisify(fs.rm); // Initialize readline interface const rl = readline.createInterface({ input: process.stdin, - output: process.stdout + output: process.stdout, }); // Function to ask questions in the terminal @@ -28,19 +29,29 @@ let projectPath; const installPackage = async () => { try { // Ask for package name - const packageName = await askQuestion('Enter your package name: '); + const packageName = await askQuestion("Enter your package name: "); // Ask for package type - const packageTypeChoices = ['contract_only', 'fullstack', 'dojo', 'debugger']; - console.log('Available package types:'); - packageTypeChoices.forEach((type, index) => console.log(`${index + 1}. ${type}`)); + const packageTypeChoices = [ + "contract_only", + "fullstack", + "debugger", + "dojo", + "kakarot", + ]; + console.log("Available package types:"); + packageTypeChoices.forEach((type, index) => + console.log(`${index + 1}. ${type}`) + ); let packageType; while (!packageType) { - const packageTypeChoice = await askQuestion('Select the package type (1-3): '); + const packageTypeChoice = await askQuestion( + "Select the package type (1-5): " + ); packageType = packageTypeChoices[parseInt(packageTypeChoice) - 1]; if (!packageType) { - console.log('Invalid choice. Please select a valid package type.'); + console.log("Invalid choice. Please select a valid package type."); } } @@ -49,11 +60,12 @@ const installPackage = async () => { projectPath = path.join(currentPath, packageName); if (fs.existsSync(projectPath)) { - console.log(`The file ${projectName} already exist in the current directory, please give it another name.`); - process.exit(1); - } - else { - fs.mkdirSync(projectPath); + console.log( + `The file ${projectName} already exist in the current directory, please give it another name.` + ); + process.exit(1); + } else { + fs.mkdirSync(projectPath); } // Clone the repository @@ -63,11 +75,24 @@ const installPackage = async () => { gitSpinner.succeed(); let cleanupTasks = []; - let excluded_files = [".git", ".github", "CONTRIBUTING.md", "bin", "burner", "website", "docs", "CNAME"]; + let excluded_files = [ + ".git", + ".github", + "CONTRIBUTING.md", + "bin", + "burner", + "website", + "docs", + "CNAME", + ]; - if (packageType === "fullstack" || packageType === "dojo") { - const FRONTEND_BASE_PATH = "frontend/src/app"; - const componentsToRemove = [ + if ( + packageType === "fullstack" || + packageType === "dojo" || + packageType === "kakarot" + ) { + const FRONTEND_BASE_PATH = "frontend/src/app"; + const componentsToRemove = [ `${FRONTEND_BASE_PATH}/burner`, `${FRONTEND_BASE_PATH}/wikipedia`, `${FRONTEND_BASE_PATH}/scaffold-deployer`, @@ -78,49 +103,45 @@ const installPackage = async () => { `${FRONTEND_BASE_PATH}/components/AssetTransferModal.tsx`, `${FRONTEND_BASE_PATH}/components/ConnectionModal.tsx`, `${FRONTEND_BASE_PATH}/components/ContractExecutionModal.tsx`, - ]; - cleanupTasks.push( + ]; + cleanupTasks.push( ...componentsToRemove.map((comp) => - rm(path.join(projectPath, comp), { + rm(path.join(projectPath, comp), { recursive: true, force: true, - }), + }) ), ...excluded_files.map((comp) => - rm(path.join(projectPath, comp), { + rm(path.join(projectPath, comp), { recursive: true, force: true, - }), - ), + }) + ) ); - } - else if (packageType == "contract_only") { + } else if (packageType == "contract_only") { let componentsToRemove = [...excluded_files, "frontend", ".editorconfig"]; cleanupTasks.push( ...componentsToRemove.map((comp) => rm(path.join(projectPath, comp), { - recursive: true, - force: true, - }), - ), - ) - } - else { + recursive: true, + force: true, + }) + ) + ); + } else { cleanupTasks.push( ...excluded_files.map((comp) => - rm(path.join(projectPath, comp), { + rm(path.join(projectPath, comp), { recursive: true, force: true, - }), - ), + }) + ) ); } // remove useless files const cleanSpinner = ora("Removing useless files").start(); - await Promise.all([ - ...cleanupTasks, - ]); + await Promise.all([...cleanupTasks]); process.chdir(projectPath); // remove the packages needed for cli @@ -129,10 +150,40 @@ const installPackage = async () => { // install dependencies const npmSpinner = ora("Installing dependencies...").start(); - if(packageType == "dojo") { - await exec("npm run install --legacy-peer-deps && npm run initialize-dojo"); - } - else if(packageType !== "contract_only") { + if (packageType == "dojo") { + await exec( + `git clone --depth 1 ${dojo_starter} ${projectPath}/dojo-starter --quiet` + ); + + const dojo_version = getDojoVersion( + path.join(projectPath, "/dojo-starter/Scarb.toml") + ); + + await exec( + `npm run install --dojo-version=${dojo_version} --legacy-peer-deps` + ); + + await exec("npm run initialize-dojo"); + + fs.rmSync(path.join(projectPath, "/dojo-starter"), { + recursive: true, + force: true, + }); + } else if (packageType == "kakarot") { + await exec("npm run initialize-kakarot"); + + await exec("npm run setup-kakarot"); + + const tool_versions = await getVersionsFromToolFile( + path.join(projectPath, "/contracts/.tool-versions") + ); + + await exec( + `npm run install --scarb-version=${tool_versions.scarb} --legacy-peer-deps` + ); + + // await exec("npm run install-tools"); + } else if (packageType !== "contract_only") { await exec("npm run install --legacy-peer-deps"); } npmSpinner.succeed(); @@ -140,8 +191,12 @@ const installPackage = async () => { console.log("The installation is done!"); console.log("You can now run the scaffold with:"); console.log(` cd ${packageName}`); - console.log(` npm run start`); + if (packageType == "kakarot") { + console.log(` npm run start-kakarot`); + } else { + console.log(` npm run start`); + } } catch (err) { // clean up in case of error, so the user does not have to do it manually fs.rmSync(projectPath, { recursive: true, force: true }); @@ -151,4 +206,55 @@ const installPackage = async () => { } }; +/** + * Reads the .tool-versions file and returns the versions of packages. + * @param {string} filePath - The path to the .tool-versions file. + * @returns {Promise} - A promise that resolves to an object containing package names and their versions. + */ +function getVersionsFromToolFile(filePath) { + return new Promise((resolve, reject) => { + fs.readFile(filePath, "utf8", (err, data) => { + if (err) { + return reject(err); + } + + const versions = {}; + const lines = data.trim().split("\n"); + + for (const line of lines) { + const [packageName, version] = line + .split(" ") + .map((item) => item.trim()); + if (packageName && version) { + versions[packageName] = version; + } + } + + resolve(versions); + }); + }); +} + +/** + * Reads the Scarb.toml file and returns the versions of dojo + * @param {string} filePath - The path to the Scarb.toml file. + * @returns {string} - A string corresponding to the dojo version. + */ +function getDojoVersion(filePath) { + const tomlContent = fs.readFileSync(filePath, "utf-8"); + + // Use a regular expression to match the Dojo version tag + const dojoVersionMatch = tomlContent.match( + /dojo\s*=\s*{[^}]*tag\s*=\s*"v([\d\w\.\-]+)"/ + ); + + // Check if the match was found and return the version with 'v' prefix + if (dojoVersionMatch && dojoVersionMatch[1]) { + return dojoVersionMatch[1]; + } + + // Return null if no version found + return null; +} + installPackage(); diff --git a/docs/src/chapter_5.md b/docs/src/chapter_5.md index 8bf433d..803e0dc 100644 --- a/docs/src/chapter_5.md +++ b/docs/src/chapter_5.md @@ -53,26 +53,26 @@ generates a profile which is added to scarb.toml and can be passed to other comm ### Deploy Account To deploy an account: ``` -npm run deploy-account --profile= --name= --feetoken= --maxfee= +npm run deploy-account --profile= --name= --fee-token= --max-fee= ``` where the profile is gotten from `snfoundry.toml`, name is the prepared account and maxfee is the specified max fee. ### Delete Account To delete an account: ``` -npm run delete-account --profile= --accountfile= --name= --network= +npm run delete-account --profile= --account-file= --name= --network= ``` ### Declare Contract To declare a Starknet contract: ``` -npm run declare-contract --profile= --contract= --feetoken= +npm run declare-contract --profile= --contract-name= --url= --fee-token= ``` ### Deploy Contract To deploy a contract: ``` -npm run deploy-contract --profile= --feetoken= --class= +npm run deploy-contract --profile= --feetoken= --class= --url= ``` ### Starknet-Devnet @@ -81,7 +81,6 @@ Confirm that Docker is installed and running to use starknet-devnet. To run devn npm run devnet ``` - ## Dojo Scripts The `contracts` folder contains all the tools needed to write, build, test and deploy dojo projects. It is built with sozo and katana. Here are common operations you can perform on your dojo contracts. @@ -109,6 +108,54 @@ To migrate your dojo project, from the base repository run: npm run migrate-dojo --name= ``` +## Kakarot Scripts +Below are npm scripts provided by `Starknet-Scaffold` for your Kakarot development. + +### Setup Kakarot +To setup Kakarot, from the base repository run: +``` +npm run setup-kakarot +``` + +### Start Kakarot +To start Kakarot, from the base repository run: +``` +npm run start-kakarot +``` + +### Deploy Kakarot L1 Messaging Contracts to Local RPC +To deploy Kakarot l1 messaging contracts locally, from the base repository run: +``` +npm run deploy-kakarot-l1-messaging-contracts-local +``` + +### Deploy Kakarot EVM Contract +To deploy a Kakarot EVM contract, from the base repository run: +``` +npm run deploy-kakarot-evm-contract --contract-path= --rpc-url= --private-key= +``` +If you need to specify constructor args, run: +``` +npm run deploy-kakarot-evm-contract --contract-path= --constructor-args= --rpc-url= --private-key= +``` + +### Declare Cairo Contract Using Keystore +To declare a Cairo contract using keystore, from the base repository run: +``` +npm run keystore-declare-contract --keystore= --account= --contract-name= --url= --fee-token= +``` + +### Deploy Cairo Contract Using Keystore +To deploy a Cairo contract using keystore, from the base repository run: +``` +npm run keystore-deploy-contract --keystore= --account= --url= --fee-token= --class-hash= +``` + +### Whitelist Contract +To whitelist a contract, from the base repository run: +``` +npm run whitelist-contract --contract-address= +``` ## User Interface Scripts diff --git a/package.json b/package.json index 930bbe7..7df2362 100644 --- a/package.json +++ b/package.json @@ -7,22 +7,31 @@ "build-contracts": "cd contracts && scarb build", "test-contracts": "cd contracts && snforge test", "format-contracts": "cd contracts && scarb fmt", - "verify-contracts": "cd contracts && sncast verify --contract-address ${npm_config_contract-address} --contract-name ${npm_config_contract-name} --verifier walnut --network ${npm_config-network}", + "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_feetoken} --max-fee ${npm_config_maxfee}", - "delete-account": "cd contracts && sncast --profile ${npm_config_profile} --accounts-file ${npm_config_accountfile} 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} --fee-token ${npm_config_feetoken}", - "deploy-contract": "cd contracts && sncast --profile ${npm_config_profile} deploy --fee-token ${npm_config_feetoken} --class-hash ${npm_config_class}", + "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", "deploy-dojo-katana": "cd contracts/${npm_config_name} && katana --disable-fee --allowed-origins \"*\"", "migrate-dojo": "cd contracts/${npm_config_name} && sozo migrate apply", "devnet": "sh ./devnet/start.sh", "start": "cd frontend && npm run dev", - "install": "cd scripts && bash install_tools.sh && cd ../frontend && npm install --legacy-peer-deps", - "build-ui": "cd frontend && npm run build" + "install": "cd scripts && bash install_tools.sh $(test -n \"$npm_config_scarb_version\" && echo --scarb $npm_config_scarb_version) $(test -n \"$npm_config_starknet_foundry_version\" && echo --starknet-foundry $npm_config_starknet_foundry_version) $(test -n \"$npm_config_foundry_version\" && echo --foundry $npm_config_foundry_version) $(test -n \"$npm_config_dojo_version\" && echo --dojo $npm_config_dojo_version) && cd ../frontend && npm install --legacy-peer-deps", + "install-tools": "snfoundryup && foundryup", + "build-ui": "cd frontend && npm run build", + "initialize-kakarot": "git clone https://github.com/kkrt-labs/build-on-kakarot.git && rm -rf contracts && mkdir contracts && cp -r build-on-kakarot/cairo_contracts/. ./contracts && cp -r build-on-kakarot/katana.account.json ./contracts && cp -r build-on-kakarot/katana.key.json ./contracts && mkdir kakarot && cp -r build-on-kakarot/. ./kakarot && cd kakarot && rm -rf cairo_contracts && rm -rf .github && rm -rf .trunk && git init && cd .. && rm -rf build-on-kakarot", + "setup-kakarot": "cd kakarot && make setup", + "start-kakarot": "cd kakarot && make start", + "deploy-kakarot-l1-messaging-contracts-local": "cd kakarot && make deploy-l1", + "deploy-kakarot-evm-contract": "cd kakarot && forge create ${npm_config_contract_path} $(test -n \"$npm_config_constructor_args\" && echo --constructor-args $npm_config_constructor_args) --rpc-url ${npm_config_rpc_url} --private-key ${npm_config_private_key}", + "keystore-declare-contract": "cd contracts && sncast --keystore ${npm_config_keystore} --account ${npm_config_account} declare --contract-name ${npm_config_contract_name} --url ${npm_config_url} --fee-token ${npm_config_fee_token}", + "keystore-deploy-contract": "cd contracts && sncast --keystore ${npm_config_keystore} --account ${npm_config_account} deploy --url ${npm_config_url} --fee-token ${npm_config_fee_token} --class-hash ${npm_config_class_hash}", + "whitelist-contract": "cd kakarot && make whitelist-contract CONTRACT_ADDRESS=${npm_config_contract_address}" }, "repository": { "type": "git", diff --git a/scripts/install_tools.sh b/scripts/install_tools.sh index a2e3668..7b345f9 100644 --- a/scripts/install_tools.sh +++ b/scripts/install_tools.sh @@ -1,3 +1,4 @@ +# Main function to call installation scripts #!/bin/bash # Function to check if a command exists @@ -7,49 +8,128 @@ command_exists () { # Install Scarb install_scarb() { - if command_exists scarb; then - echo "Scarb is already installed." - else - echo "Installing Scarb..." - curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh + local version=$1 + + if [ -n "$version" ]; then + echo "Installing Scarb $version..." + curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh -s -- -v $version + else + if command_exists scarb; then + echo "Scarb is already installed." + else + echo "Installing Scarb latest..." + curl --proto '=https' --tlsv1.2 -sSf https://docs.swmansion.com/scarb/install.sh | sh + fi fi } # Install Starknet-Foundry install_starknet_foundry() { - if command_exists snforge; then - echo "Starknet-Foundry is already installed." - else - echo "Installing Starknet-Foundry..." - curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | sh - snfoundryup + local version=$1 + + curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | sh + + if [ -n "$version" ]; then + echo "Installing Starknet-Foundry $version..." + snfoundryup --version $version + else + if command_exists sncast && command_exists snforge; then + echo "Starknet-Foundry is already installed." + else + echo "Installing Starknet-Foundry latest..." + snfoundryup + fi fi +} + +# Install Foundry +install_foundry() { + local foundry_install_dir="$HOME/.foundry/bin" + + curl -L https://foundry.paradigm.xyz | bash + export PATH="$PATH:$foundry_install_dir" + if [ -n "$version" ]; then + echo "Installing Foundry $version..." + foundryup --version $version + else + if command_exists forge; then + echo "Foundry is already installed." + else + echo "Installing Foundry latest..." + foundryup + fi + fi } # Install Dojo install_dojo() { - if command_exists dojoup; then - echo "Dojo is already installed." - else - echo "Installing Dojo..." - git clone https://github.com/dojoengine/dojo - cd dojo - echo "Installing Sozo..." - cargo install --path ./bin/sozo --locked --force - echo "Installing Katana..." - cargo install --path ./bin/katana --locked --force - cd .. && rm -rf dojo + local version=$1 + local dojo_install_dir="$HOME/.dojo/bin" + + curl -L https://install.dojoengine.org | bash + + export PATH="$PATH:$dojo_install_dir" + + if [ -n "$version" ]; then + echo "Installing Dojo $version..." + dojoup --version $version + else + if command_exists dojoup; then + echo "Dojo is already installed." + else + echo "Installing Dojo latest..." + dojoup + fi fi } # Main function to call installation scripts main() { - install_scarb - install_starknet_foundry - install_dojo + # Default versions (empty means latest) + local scarb_version="" + local starknet_foundry_version="" + local dojo_version="" + local foundry_version="" + + # Parse the arguments + while [[ $# -gt 0 ]]; do + case $1 in + --scarb) + shift + scarb_version=$1 + shift + ;; + --starknet-foundry) + shift + starknet_foundry_version=$1 + shift + ;; + --dojo) + shift + dojo_version=$1 + shift + ;; + --foundry) + shift + foundry_version=$1 + shift + ;; + *) + echo "Unknown argument: $1" + echo "Available options: --scarb [version], --starknet-foundry [version], --dojo [version], --foundry [version]" + exit 1 + ;; + esac + done + + # Install all packages, using the specified version or default to latest + install_scarb "$scarb_version" + install_starknet_foundry "$starknet_foundry_version" + install_foundry "$foundry_version" + install_dojo "$dojo_version" echo "Installation complete!" } -main +main "$@"