UNPKG

xchange-create

Version:
1,109 lines (1,074 loc) โ€ข 45.9 kB
import { execa } from 'execa'; import url, { fileURLToPath } from 'url'; import path from 'path'; import * as fs from 'fs'; import fs__default, { lstatSync, readdirSync, existsSync, promises } from 'fs'; import mergeJsonStr from 'merge-packages'; import ncp from 'ncp'; import { promisify } from 'util'; import { spawn } from 'child_process'; import chalk from 'chalk'; import Listr from 'listr'; import { parse, stringify } from 'envfile'; import arg from 'arg'; import inquirer from 'inquirer'; import { createPublicClient, http } from 'viem'; import { base, polygon, bsc, mainnet, arbitrum, optimism, baseSepolia, sepolia } from 'viem/chains'; import { Table } from 'console-table-printer'; import axios from 'axios'; const isExtension = (item) => item !== null; /** * This function makes sure that the `T` generic type is narrowed down to * whatever `extensions` are passed in the question prop. That way we can type * check the `default` prop is not using any valid extension, but only one * already provided in the `extensions` prop. * * Questions can be created without this function, just using a normal object, * but `default` type will be any valid Extension. */ const typedQuestion = (question) => question; const typedContractTypeQuestion = (question) => question; const isDefined = (item) => item !== undefined && item !== null; const extensionWithSubextensions = (extension) => { return Object.prototype.hasOwnProperty.call(extension, "extensions"); }; const baseDir = "base"; const extensionDict = {}; const currentFileUrl = import.meta.url; const templatesDirectory = path.resolve(decodeURI(fileURLToPath(currentFileUrl)), "../../templates"); /** * This function has side effects. It generates the extensionDict. * * @param basePath the path at which to start the traverse * @returns the extensions found in this path. Useful for the recursion */ const traverseExtensions = async (basePath) => { const extensionsPath = path.resolve(basePath, "extensions"); let extensions; try { extensions = fs__default.readdirSync(extensionsPath); } catch (error) { return []; } await Promise.all(extensions.map(async (ext) => { const extPath = path.resolve(extensionsPath, ext); const configPath = path.resolve(extPath, "config.json"); let config = {}; try { config = JSON.parse(fs__default.readFileSync(configPath, "utf8")); } catch (error) { if (fs__default.existsSync(configPath)) { throw new Error(`Couldn't parse existing config.json file. Extension: ${ext}; Config file path: ${configPath}`); } } let name = config.name ?? ext; let value = ext; const subExtensions = await traverseExtensions(extPath); const hasSubExtensions = subExtensions.length !== 0; const extDescriptor = { name, value, path: extPath, extensions: subExtensions, extends: config.extends, }; if (!hasSubExtensions) { delete extDescriptor.extensions; } extensionDict[ext] = extDescriptor; return subExtensions; })); return extensions; }; await traverseExtensions(templatesDirectory); const findFilesRecursiveSync = (baseDir, criteriaFn = () => true) => { const subPaths = fs__default.readdirSync(baseDir); const files = subPaths.map((relativePath) => { const fullPath = path.resolve(baseDir, relativePath); return fs__default.lstatSync(fullPath).isDirectory() ? [...findFilesRecursiveSync(fullPath, criteriaFn)] : criteriaFn(fullPath) ? [fullPath] : []; }); return files.flat(); }; // @ts-expect-error We don't have types for this probably add .d.ts file function mergePackageJson(targetPackageJsonPath, secondPackageJsonPath, isDev) { const existsTarget = fs__default.existsSync(targetPackageJsonPath); const existsSecond = fs__default.existsSync(secondPackageJsonPath); if (!existsTarget && !existsSecond) { return; } const targetPackageJson = existsTarget ? fs__default.readFileSync(targetPackageJsonPath, "utf8") : '{}'; const secondPackageJson = existsSecond ? fs__default.readFileSync(secondPackageJsonPath, "utf8") : '{}'; const mergedPkgStr = mergeJsonStr.default(targetPackageJson, secondPackageJson); fs__default.writeFileSync(targetPackageJsonPath, mergedPkgStr, "utf8"); if (isDev) { const devStr = `TODO: write relevant information for the contributor`; fs__default.writeFileSync(`${targetPackageJsonPath}.dev`, devStr, "utf8"); } } const { mkdir, link } = promises; /** * The goal is that this function has the same API as ncp, so they can be used * interchangeably. * * - clobber not implemented */ const linkRecursive = async (source, destination, options) => { const passesFilter = options?.filter === undefined ? true // no filter : typeof options.filter === 'function' ? options.filter(source) // filter is function : options.filter.test(source); // filter is regex if (!passesFilter) { return; } if (lstatSync(source).isDirectory()) { const subPaths = readdirSync(source); await Promise.all(subPaths.map(async (subPath) => { const sourceSubpath = path.join(source, subPath); const isSubPathAFolder = lstatSync(sourceSubpath).isDirectory(); const destSubPath = path.join(destination, subPath); const existsDestSubPath = existsSync(destSubPath); if (isSubPathAFolder && !existsDestSubPath) { await mkdir(destSubPath); } await linkRecursive(sourceSubpath, destSubPath, options); })); return; } return link(source, destination); }; const copy = promisify(ncp); let copyOrLink = copy; const expandExtensions = (options) => { const expandedExtensions = options.extensions .map((extension) => extensionDict[extension]) .map((extDescriptor) => [extDescriptor.extends, extDescriptor.value].filter(isDefined)) .flat() // this reduce just removes duplications .reduce((exts, ext) => (exts.includes(ext) ? exts : [...exts, ext]), []); return expandedExtensions; }; const isTemplateRegex = /([^\/\\]*?)\.template\./; const isPackageJsonRegex = /package\.json/; const isPnpmLockRegex = /pnpm-lock\.yaml/; const isNextGeneratedRegex = /packages\/nextjs\/generated/; const isConfigRegex = /([^\/\\]*?)\\config\.json/; const isArgsRegex = /([^\/\\]*?)\.args\./; const isExtensionFolderRegex = /extensions$/; const isPackagesFolderRegex = /packages$/; const copyBaseFiles = async ({ dev: isDev }, basePath, targetDir) => { await copyOrLink(basePath, targetDir, { clobber: false, filter: (fileName) => { const isTemplate = isTemplateRegex.test(fileName); const isPackageJson = isPackageJsonRegex.test(fileName); const isPnpmLock = isPnpmLockRegex.test(fileName); const isNextGenerated = isNextGeneratedRegex.test(fileName); const skipAlways = isTemplate || isPackageJson; const skipDevOnly = isPnpmLock || isNextGenerated; const shouldSkip = skipAlways || (isDev && skipDevOnly); return !shouldSkip; }, }); const basePackageJsonPaths = findFilesRecursiveSync(basePath, path => isPackageJsonRegex.test(path)); basePackageJsonPaths.forEach(packageJsonPath => { const partialPath = packageJsonPath.split(basePath)[1]; mergePackageJson(path.join(targetDir, partialPath), path.join(basePath, partialPath), isDev); }); if (isDev) { const basePnpmLockPaths = findFilesRecursiveSync(basePath, path => isPnpmLockRegex.test(path)); basePnpmLockPaths.forEach(pnpmLockPath => { const partialPath = pnpmLockPath.split(basePath)[1]; copy(path.join(basePath, partialPath), path.join(targetDir, partialPath)); }); const nextGeneratedPaths = findFilesRecursiveSync(basePath, path => isNextGeneratedRegex.test(path)); nextGeneratedPaths.forEach(nextGeneratedPath => { const partialPath = nextGeneratedPath.split(basePath)[1]; copy(path.join(basePath, partialPath), path.join(targetDir, partialPath)); }); } }; const copyExtensionsFiles = async ({ extensions, dev: isDev }, targetDir) => { await Promise.all(extensions.map(async (extension) => { const extensionPath = extensionDict[extension].path; // copy (or link if dev) root files await copyOrLink(extensionPath, path.join(targetDir), { clobber: false, filter: (path) => { const isConfig = isConfigRegex.test(path); const isArgs = isArgsRegex.test(path); const isExtensionFolder = isExtensionFolderRegex.test(path) && fs__default.lstatSync(path).isDirectory(); const isPackagesFolder = isPackagesFolderRegex.test(path) && fs__default.lstatSync(path).isDirectory(); const isTemplate = isTemplateRegex.test(path); // PR NOTE: this wasn't needed before because ncp had the clobber: false const isPackageJson = isPackageJsonRegex.test(path); const shouldSkip = isConfig || isArgs || isTemplate || isPackageJson || isExtensionFolder || isPackagesFolder; return !shouldSkip; }, }); // merge root package.json mergePackageJson(path.join(targetDir, "package.json"), path.join(extensionPath, "package.json"), isDev); const extensionPackagesPath = path.join(extensionPath, "packages"); const hasPackages = fs__default.existsSync(extensionPackagesPath); if (hasPackages) { // copy extension packages files await copyOrLink(extensionPackagesPath, path.join(targetDir, "packages"), { clobber: false, filter: (path) => { const isArgs = isArgsRegex.test(path); const isTemplate = isTemplateRegex.test(path); const isPackageJson = isPackageJsonRegex.test(path); const shouldSkip = isArgs || isTemplate || isPackageJson; return !shouldSkip; }, }); // copy each package's package.json const extensionPackages = fs__default.readdirSync(extensionPackagesPath); extensionPackages.forEach((packageName) => { mergePackageJson(path.join(targetDir, "packages", packageName, "package.json"), path.join(extensionPath, "packages", packageName, "package.json"), isDev); }); } })); }; const processTemplatedFiles = async ({ extensions, dev: isDev }, basePath, targetDir) => { const baseTemplatedFileDescriptors = findFilesRecursiveSync(basePath, (path) => isTemplateRegex.test(path)).map((baseTemplatePath) => ({ path: baseTemplatePath, fileUrl: url.pathToFileURL(baseTemplatePath).href, relativePath: baseTemplatePath.split(basePath)[1], source: "base", })); const extensionsTemplatedFileDescriptors = extensions .map((ext) => findFilesRecursiveSync(extensionDict[ext].path, (filePath) => isTemplateRegex.test(filePath)).map((extensionTemplatePath) => ({ path: extensionTemplatePath, fileUrl: url.pathToFileURL(extensionTemplatePath).href, relativePath: extensionTemplatePath.split(extensionDict[ext].path)[1], source: `extension ${extensionDict[ext].name}`, }))) .flat(); await Promise.all([ ...baseTemplatedFileDescriptors, ...extensionsTemplatedFileDescriptors, ].map(async (templateFileDescriptor) => { const templateTargetName = templateFileDescriptor.path.match(isTemplateRegex)?.[1]; const argsPath = templateFileDescriptor.relativePath.replace(isTemplateRegex, `${templateTargetName}.args.`); const argsFileUrls = extensions .map((extension) => { const argsFilePath = path.join(extensionDict[extension].path, argsPath); const fileExists = fs__default.existsSync(argsFilePath); if (!fileExists) { return []; } return url.pathToFileURL(argsFilePath).href; }) .flat(); const args = await Promise.all(argsFileUrls.map(async (argsFileUrl) => await import(argsFileUrl))); const template = (await import(templateFileDescriptor.fileUrl)).default; if (!template) { throw new Error(`Template ${templateTargetName} from ${templateFileDescriptor.source} doesn't have a default export`); } if (typeof template !== "function") { throw new Error(`Template ${templateTargetName} from ${templateFileDescriptor.source} is not exporting a function by default`); } const freshArgs = Object.fromEntries(Object.keys(args[0] ?? {}).map((key) => [ key, // INFO: key for the freshArgs object [], // INFO: initial value for the freshArgs object ])); const combinedArgs = args.reduce((accumulated, arg) => { Object.entries(arg).map(([key, value]) => { accumulated[key].push(value); }); return accumulated; }, freshArgs); // TODO test: if first arg file found only uses 1 name, I think the rest are not used? const output = template(combinedArgs); const targetPath = path.join(targetDir, templateFileDescriptor.relativePath.split(templateTargetName)[0], templateTargetName); fs__default.writeFileSync(targetPath, output); if (isDev) { const hasCombinedArgs = Object.keys(combinedArgs).length > 0; const hasArgsPaths = argsFileUrls.length > 0; const devOutput = `--- TEMPLATE FILE templates/${templateFileDescriptor.source}${templateFileDescriptor.relativePath} --- ARGS FILES ${hasArgsPaths ? argsFileUrls.map(url => `\t- ${path.join('templates', url.split('templates')[1])}`).join('\n') : '(no args files writing to the template)'} --- RESULTING ARGS ${hasCombinedArgs ? Object.entries(combinedArgs) .map(([argName, argValue]) => `\t- ${argName}:\t[${argValue.join(',')}]`) // TODO improvement: figure out how to add the values added by each args file .join('\n') : '(no args sent for the template)'} `; fs__default.writeFileSync(`${targetPath}.dev`, devOutput); } })); }; async function copyTemplateFiles(options, templateDir, targetDir) { copyOrLink = options.dev ? linkRecursive : copy; const basePath = path.join(templateDir, baseDir); // 1. Copy base template to target directory await copyBaseFiles(options, basePath, targetDir); // 2. Add "parent" extensions (set via config.json#extend field) const expandedExtension = expandExtensions(options); options.extensions = expandedExtension; // 3. Copy extensions folders await copyExtensionsFiles(options, targetDir); // 4. Process templated files and generate output await processTemplatedFiles(options, basePath, targetDir); // 5. Initialize git repo to avoid husky error await execa("git", ["init"], { cwd: targetDir }); await execa("git", ["checkout", "-b", "main"], { cwd: targetDir }); } async function createProjectDirectory(projectName) { try { const result = await execa("mkdir", [projectName]); if (result.failed) { throw new Error("There was a problem running the mkdir command"); } } catch (error) { throw new Error("Failed to create directory", { cause: error }); } return true; } function installPackages(targetDir) { return new Promise((resolve, reject) => { const install = spawn('pnpm', ['install', '--reporter=verbose'], { cwd: targetDir, stdio: 'inherit', }); install.on('close', (code) => { if (code !== 0) { reject(new Error(`pnpm install failed with exit code ${code}`)); } else { resolve(true); } }); install.on('error', (error) => { reject(new Error('Failed to start pnpm install', { cause: error })); }); }); } // Checkout the latest release tag in a git submodule async function checkoutLatestTag(submodulePath) { try { const { stdout } = await execa("git", ["tag", "-l", "--sort=-v:refname"], { cwd: submodulePath, }); const tagLines = stdout.split("\n"); if (tagLines.length > 0) { const latestTag = tagLines[0]; await execa("git", ["-C", `${submodulePath}`, "checkout", latestTag]); } else { throw new Error(`No tags found in submodule at ${submodulePath}`); } } catch (error) { console.error("Error checking out latest tag:", error); throw error; } } async function createFirstGitCommit(targetDir, options) { try { // TODO: Move the logic for adding submodules to tempaltes if (options.extensions?.includes("foundry")) { const foundryWorkSpacePath = path.resolve(targetDir, "packages", "foundry"); await execa("git", [ "submodule", "add", "https://github.com/foundry-rs/forge-std", "lib/forge-std", ], { cwd: foundryWorkSpacePath, }); await execa("git", [ "submodule", "add", "https://github.com/OpenZeppelin/openzeppelin-contracts", "lib/openzeppelin-contracts", ], { cwd: foundryWorkSpacePath, }); await execa("git", [ "submodule", "add", "https://github.com/gnsps/solidity-bytes-utils", "lib/solidity-bytes-utils", ], { cwd: foundryWorkSpacePath, }); await execa("git", ["submodule", "update", "--init", "--recursive"], { cwd: foundryWorkSpacePath, }); await checkoutLatestTag(path.resolve(foundryWorkSpacePath, "lib", "forge-std")); await checkoutLatestTag(path.resolve(foundryWorkSpacePath, "lib", "openzeppelin-contracts")); } await execa("git", ["add", "-A"], { cwd: targetDir }); await execa("git", ["commit", "-m", "Initial commit with ๐Ÿงช Xchange Create", "--no-verify"], { cwd: targetDir }); // Update the submodule, since we have checked out the latest tag in the previous step of foundry if (options.extensions?.includes("foundry")) { await execa("git", ["submodule", "update", "--init", "--recursive"], { cwd: path.resolve(targetDir, "packages", "foundry"), }); } } catch (e) { // cast error as ExecaError to get stderr throw new Error("Failed to initialize git repository", { cause: e?.stderr ?? e, }); } } async function prettierFormat(targetDir) { return new Promise((resolve, reject) => { const install = spawn('pnpm', ['format'], { cwd: targetDir, stdio: 'inherit', }); install.on('close', (code) => { if (code !== 0) { reject(new Error(`pnpm install failed with exit code ${code}`)); } else { resolve(true); } }); install.on('error', (error) => { reject(new Error('Failed to start pnpm install', { cause: error })); }); }); // try { // const result = await execa("pnpm run", ["format"], { cwd: targetDir }); // if (result.failed) { // throw new Error("There was a problem running the format command"); // } // } catch (error) { // throw new Error("Failed to create directory", { cause: error }); // } // return true; } async function renderOutroMessage(options) { let message = ` \n ${chalk.bold.green("Congratulations!")} Your project has been created! ๐Ÿ”‹ ${chalk.bold("Next steps:")} ${chalk.dim("cd")} ${options.project} `; if (options.extensions.includes("hardhat") || options.extensions.includes("foundry")) { message += ` \t${chalk.bold("Start the local development node")} \t${chalk.dim("pnpm run")} chain `; if (options.extensions.includes("foundry")) { try { await execa("foundryup", ["-h"]); } catch (error) { message += ` \t${chalk.bold.yellow("(NOTE: Foundryup is not installed in your system)")} \t${chalk.dim("Checkout: https://getfoundry.sh")} `; } } message += ` \t${chalk.bold("In a new terminal window, get your selected contract on chain")} \t${chalk.dim("pnpm run")} generate ${chalk.dim("// generates a new wallet")} \t${chalk.dim("pnpm run")} account ${chalk.dim("// ensure your wallet is funded")} `; } message += ` \t${chalk.bold("In a new terminal window, start the frontend")} \t${chalk.dim("pnpm run")} start `; message += ` ${chalk.bold.green("Thanks for using ๐Ÿงช Xchange Create, Trust No One. Trust Code. Long Live DeFi!")} `; console.log(message); } function getContractName(contractType) { switch (contractType) { case "standard-token": return "StandardToken"; case "tax-token": return "StandardToken"; // Assuming tax-token also uses StandardToken case "deflationary-token": return "DeflationaryToken"; case "test-erc20": return "MockERC20"; case "my-custom-contract": return undefined; // Return undefined for custom contracts default: return undefined; } } async function createProject(options) { console.log(`\n`); const currentFileUrl = import.meta.url; const templateDirectory = path.resolve(decodeURI(fileURLToPath(currentFileUrl)), "../../templates"); const targetDirectory = path.resolve(process.cwd(), options.project); const envFilePath = path.join(targetDirectory, "packages", "hardhat", ".env"); const tasks = new Listr([ { title: `๐Ÿ“ Create project directory ${targetDirectory}`, task: () => createProjectDirectory(options.project), }, { title: `๐Ÿงช Creating a new Xchange project in ${chalk.green.bold(options.project)}`, task: () => copyTemplateFiles(options, templateDirectory, targetDirectory), }, { title: "๐Ÿ“ Writing options to .env file", task: () => { const existingEnvConfig = fs.existsSync(envFilePath) ? parse(fs.readFileSync(envFilePath, "utf8")) : {}; const newEnvConfig = { ...existingEnvConfig, TOKEN_NAME: options.project, TOKEN_SYMBOL: options.ticker, TOKEN_SUPPLY: options.supply.toString(), TOKEN_SUPPLY_PAIRED: options.supply, DEPLOYER_PRIVATE_KEY: "", CONTRACT_NAME: getContractName(options.contractType), LOAN_TERM_CONTRACT_ADDRESS: "0xd95f799276A8373F7F234A7F211DE9E3a0ae6639", LOAN_AMOUNT: 0.5, INITIAL_PAYMENT_DUE: 0.11 }; fs.writeFileSync(envFilePath, stringify(newEnvConfig)); // Rename the contract file to the TOKEN_NAME in the new project directory const contractFilePath = path.join(targetDirectory, "packages", "hardhat", "contracts", `${getContractName(options.contractType)}.sol`); const newContractFilePath = path.join(path.dirname(contractFilePath), `${options.project}.sol`); // Read the contents of the contract file const contractContent = fs.readFileSync(contractFilePath, "utf8"); // Replace the contract name in the file contents const updatedContractContent = contractContent.replace(new RegExp(`contract ${getContractName(options.contractType)}`, "g"), `contract ${options.project}`); // Write the updated contents to the new contract file fs.writeFileSync(newContractFilePath, updatedContractContent); // Remove the original contract file fs.unlinkSync(contractFilePath); // Update the CONTRACT_NAMES export in the new project directory const constantsFilePath = path.join(targetDirectory, "packages", "hardhat", "utils", "constants.ts"); const constantsContent = fs.readFileSync(constantsFilePath, "utf8"); const updatedConstantsContent = constantsContent.replace(/export const CONTRACT_NAMES = {[^}]*}/, `export const CONTRACT_NAMES = { StandardToken: "StandardToken", DeflationaryToken: "DeflationaryToken", MockERC20: "MockERC20", ${options.project}: "${options.project}", };`); fs.writeFileSync(constantsFilePath, updatedConstantsContent); }, }, { title: `๐Ÿ“ฆ Installing dependencies with pnpm, this could take a while`, task: () => installPackages(targetDirectory), skip: () => { if (!options.install) { return "Manually skipped"; } }, }, { title: `๐Ÿช„${" "}Formatting files with prettier`, task: () => prettierFormat(targetDirectory), skip: () => { if (!options.install) { return "Skipping because prettier install was skipped"; } }, }, { title: `๐Ÿ“ก${" "}Initializing Git repository ${options.extensions?.includes("foundry") ? "and submodules" : ""}`, task: () => createFirstGitCommit(targetDirectory, options), }, ]); try { await tasks.run(); renderOutroMessage(options); } catch (error) { console.log("%s Error occurred", chalk.red.bold("ERROR"), error); console.log("%s Exiting...", chalk.red.bold("Uh oh! ๐Ÿ˜• Sorry about that!")); } } function parseArgumentsIntoOptions(rawArgs) { const args = arg({ "--project": String, "-p": "--project", "--install": Boolean, "-i": "--install", "--skip-install": Boolean, "--skip": "--skip-install", "-s": "--skip-install", "--dev": Boolean, "--ticker": String, "-t": "--ticker", "--supply": Number, "-u": "--supply", "--contract-type": String, "-c": "--contract-type", "--extensions": [String], "-e": "--extensions", "--help": Boolean, "-h": "--help", "--quote": Boolean, "-q": "--quote", "--usd": Boolean, "--network": String, "-n": "--network", }, { argv: rawArgs.slice(2).map((a) => a.toLowerCase()), }); const install = args["--install"] ?? null; const skipInstall = args["--skip-install"] ?? null; const hasInstallRelatedFlag = install || skipInstall; const dev = args["--dev"] ?? false; const project = args["--project"] ?? null; const ticker = args["--ticker"] ?? null; const supply = args["--supply"] ?? null; const contractType = args["--contract-type"] ?? null; const extensions = args["--extensions"] ?? null; const help = args["--help"] ?? false; const usd = args["--usd"] ?? false; const quote = args["--quote"] ?? false; const network = args["--network"] ?? null; return { project, install: hasInstallRelatedFlag ? install || !skipInstall : null, ticker, supply, contractType, dev, extensions, help, quote, network, usd }; } const config = { questions: [ typedQuestion({ type: "single-select", name: "solidity-framework", message: "What solidity framework do you want to use?", extensions: ["hardhat", "foundry", null], default: "hardhat", }), typedContractTypeQuestion({ type: "single-select", name: "contract-type", message: "What kind of token contract do you want to use?", contractTypes: ["standard-token", "tax-token", "deflationary-token", "test-erc20", "my-custom-contract"], default: "standard-token", }), ], }; // default values for unspecified args const defaultOptions = { project: "my-project", ticker: "TICKER", supply: 100000000, install: true, dev: false, extensions: [], contractType: "standard-token", quote: false, network: "sepolia", usd: false, }; const invalidQuestionNames = ["project", "install"]; const nullExtensionChoice = { name: 'None', value: null }; async function promptForMissingOptions(options, questionType) { const questions = []; if (questionType === "create") { questions.push({ type: "input", name: "project", message: "Your project name:", default: defaultOptions.project, validate: (value) => value.length > 0, }); questions.push({ type: "input", name: "ticker", message: "What is your TICKER?", default: defaultOptions.ticker, validate: (value) => value.length > 0, }); questions.push({ type: "number", name: "supply", message: "What is your token supply?", default: defaultOptions.supply, validate: (value) => value > 0, }); const recurringAddFollowUps = (extensions, relatedQuestion) => { extensions.filter(extensionWithSubextensions).forEach((ext) => { const nestedExtensions = ext.extensions.map((nestedExt) => extensionDict[nestedExt]); questions.push({ // INFO: assuming nested extensions are all optional. To change this, // update ExtensionDescriptor adding type, and update code here. type: "checkbox", name: `${ext.value}-extensions`, message: `Select optional extensions for ${ext.name}`, choices: nestedExtensions, when: (answers) => { const relatedResponse = answers[relatedQuestion]; const wasMultiselectResponse = Array.isArray(relatedResponse); return wasMultiselectResponse ? relatedResponse.includes(ext.value) : relatedResponse === ext.value; }, }); recurringAddFollowUps(nestedExtensions, `${ext.value}-extensions`); }); }; config.questions.forEach((question) => { if (invalidQuestionNames.includes(question.name)) { throw new Error(`The name of the question can't be "${question.name}". The invalid names are: ${invalidQuestionNames .map((w) => `"${w}"`) .join(", ")}`); } if (question.type === "single-select" && question.name === "contract-type") { const contractTypes = question.contractTypes; questions.push({ type: "list", name: question.name, message: question.message, choices: contractTypes, }); } else { const extensions = question.extensions .filter(isExtension) .map((ext) => extensionDict[ext]) .filter(isDefined); const hasNoneOption = question.extensions.includes(null); questions.push({ type: question.type === "multi-select" ? "checkbox" : "list", name: question.name, message: question.message, choices: hasNoneOption ? [...extensions, nullExtensionChoice] : extensions, }); recurringAddFollowUps(extensions, question.name); } }); questions.push({ type: "confirm", name: "install", message: "Install packages?", default: defaultOptions.install, }); } if (questionType === "quote") { // Prompt only for the options required for the quote questions.push({ type: "input", name: "network", message: "Enter the network:", validate: (value) => ["base", "polygon", "bsc", "eth", "arbitrum", "optimism"].includes(value), when: () => !options.network, }); questions.push({ type: "input", name: "contractType", message: "Enter the contract name:", validate: (value) => value.trim() !== "", when: () => !options.contractType, }); } const answers = await inquirer.prompt(questions); const mergedOptions = { project: options.project ?? answers.project, ticker: options.ticker ?? answers.ticker, supply: options.supply ?? answers.supply, install: options.install ?? answers.install, contractType: options.contractType ?? defaultOptions.contractType, dev: options.dev ?? defaultOptions.dev, quote: options?.quote ?? defaultOptions.quote, network: options?.network ?? defaultOptions.network, usd: options?.usd ?? defaultOptions.usd, extensions: [], }; config.questions.forEach((question) => { const { name } = question; if (question.name === "solidity-framework") { const choice = [answers[name]].flat().filter(isDefined); mergedOptions.extensions.push(...choice); } else if (question.type === "single-select" && question.name === "contract-type") { mergedOptions.contractType = answers[name]; } }); const recurringAddNestedExtensions = (baseExtensions) => { baseExtensions.forEach((extValue) => { const nestedExtKey = `${extValue}-extensions`; const nestedExtensions = answers[nestedExtKey]; if (nestedExtensions) { mergedOptions.extensions.push(...nestedExtensions); recurringAddNestedExtensions(nestedExtensions); } }); }; recurringAddNestedExtensions(mergedOptions.extensions); return mergedOptions; } const TITLE_TEXT = ` ${chalk.bold.green(" $$\\ $$\\ $$\\ ")} ${chalk.bold.green(" $$ | $$ | $$ | ")} ${chalk.bold.green(" \\$$\\ $$ | $$$$$$$\\ $$$$$$$\\ $$$$$$\\ $$$$$$$\\ $$$$$$\\ $$$$$$\\ ")} ${chalk.bold.green(" \\$$$$ / $$ _____|$$ __$$\\ \\____$$\\ $$ __$$\\ $$ __$$\\ $$ __$$\\ ")} ${chalk.bold.green(" $$ $$< $$ / $$ | $$ | $$$$$$$ |$$ | $$ |$$ / $$ |$$$$$$$$ |")} ${chalk.bold.green(" $$ /\\$$\\ $$ | $$ | $$ |$$ __$$ |$$ | $$ |$$ | $$ |$$ ____|")} ${chalk.bold.green(" $$ / $$ |\\$$$$$$$\\ $$ | $$ |\\$$$$$$$ |$$ | $$ |\\$$$$$$$ |\\$$$$$$$\\ ")} ${chalk.bold.green(" \\__| \\__| \\_______|\\__| \\__| \\_______|\\__| \\__| \\____$$ | \\_______|")} ${chalk.bold.green(" $$\\ $$ | ")} ${chalk.bold.green(" \\$$$$$$ | ")} ${chalk.bold.green(" \\______/ ")} ${chalk.dim("๐Ÿงช Xchange Create will scaffold and entire project allowing you to deploy your project to Xchange in seconds.")} ${chalk.dim(" ")} ${chalk.dim("Trust No One. Trust Code. Long Live DeFi!")} `; function renderIntroMessage() { console.log(TITLE_TEXT); } const chainMap = { base: base, polygon: polygon, bsc: bsc, mainnet: mainnet, arbitrum: arbitrum, optimism: optimism, // testnets baseSepolia: baseSepolia, sepolia: sepolia, }; async function getGasPrice(network) { const chain = chainMap[network]; const client = createPublicClient({ chain, transport: http(), }); const gasPrice = await client.getGasPrice(); const gasPriceInGwei = Number(gasPrice) / 10 ** 9; return gasPriceInGwei; } const ammPoolsPerChain = { base: ["Xchange", "Uniswap", "Sushiswap"], polygon: ["Xchange", "Uniswap", "Sushiswap"], bsc: ["Xchange", "Uniswap", "Sushiswap"], mainnet: ["Xchange", "Uniswap", "Sushiswap"], arbitrum: ["Xchange", "Uniswap", "Sushiswap"], optimism: ["Xchange", "Uniswap", "Sushiswap"], // testnets baseSepolia: ["Xchange", "Uniswap", "Sushiswap"], sepolia: ["Xchange", "Uniswap", "Sushiswap"], }; const COINGECKO_API_URL = "https://api.coingecko.com/api/v3"; const nativeTokenIds = { mainnet: "ethereum", bsc: "binancecoin", polygon: "matic-network", arbitrum: "ethereum", // Arbitrum uses ETH as its native token optimism: "ethereum", // Optimism uses ETH as its native token base: "ethereum", // Base uses ETH as its native token }; async function fetchNativeTokenPrice(network) { const tokenId = nativeTokenIds[network]; if (!tokenId) { throw new Error(`Unsupported network: ${network}`); } try { const response = await axios.get(`${COINGECKO_API_URL}/simple/price`, { params: { ids: tokenId, vs_currencies: "usd", }, }); const data = response.data; const price = data[tokenId]?.usd; if (!price) { throw new Error(`Failed to fetch price for ${tokenId}`); } return price; } catch (error) { console.error("Error fetching native token price:", error); throw error; } } async function displayQuote(options) { const { network, contractType, usd } = options; if (!isValidNetwork(network)) { console.error(`Error: Unsupported network "${network}". Supported networks are: ${Object.keys(ammPoolsPerChain).join(", ")}.`); return; } if (!isValidContractType(contractType)) { console.error(`Error: Unsupported contract type "${contractType}". Supported contract types are: standard-token, tax-token, deflationary-token, test-erc20, my-custom-contract.`); return; } const gasPrice = await getGasPrice(network); const availablePools = ammPoolsPerChain[network]; const nativeTokenPrice = usd ? await fetchNativeTokenPrice(network) : 0; const quoteData = availablePools.map((pool) => ({ pool, deploymentCost: calculateDeploymentCost(gasPrice), pairCreationCost: calculatePairCreationCost(gasPrice), lendingPoolApprovalCost: pool === "Xchange" ? calculateLendingPoolApprovalCost(gasPrice) : "N/A", loanInitiationCost: pool === "Xchange" ? calculateLoanInitiationCost(gasPrice) : "N/A", deploymentCostUsd: usd ? calculateDeploymentCost(gasPrice) * nativeTokenPrice : undefined, pairCreationCostUsd: usd ? calculatePairCreationCost(gasPrice) * nativeTokenPrice : undefined, lendingPoolApprovalCostUsd: pool === "Xchange" && usd ? calculateLendingPoolApprovalCost(gasPrice) * nativeTokenPrice : "N/A", loanInitiationCostUsd: pool === "Xchange" && usd ? calculateLoanInitiationCost(gasPrice) * nativeTokenPrice : "N/A", })); const table = new Table({ title: "Cost To Deploy Contract + AMM Pool", columns: [ { name: "Network", alignment: "left" }, { name: "Pool", alignment: "left" }, { name: "Deployment Cost", alignment: "right" }, { name: "Pair Creation Cost", alignment: "right" }, { name: "Lending Pool Approval Cost", alignment: "right" }, { name: "Loan Initiation Cost", alignment: "right" }, { name: "Gas Price (Gwei)", alignment: "right" }, ], }); quoteData.forEach((data) => { const row = { Network: network, Pool: data.pool, "Deployment Cost": usd ? formatPrice(data.deploymentCostUsd, true, network) : formatPrice(data.deploymentCost, false, network), "Pair Creation Cost": usd ? formatPrice(data.pairCreationCostUsd, true, network) : formatPrice(data.pairCreationCost, false, network), "Lending Pool Approval Cost": usd ? formatPrice(data.lendingPoolApprovalCostUsd, true, network) : formatPrice(data.lendingPoolApprovalCost, false, network), "Loan Initiation Cost": usd ? formatPrice(data.loanInitiationCostUsd, true, network) : formatPrice(data.loanInitiationCost, false, network), "Gas Price (Gwei)": gasPrice, }; table.addRow(row); }); table.printTable(); } function isValidNetwork(network) { return Object.keys(ammPoolsPerChain).includes(network); } function isValidContractType(contractType) { const supportedContractTypes = ["standard-token", "tax-token", "deflationary-token", "my-custom-contract"]; return supportedContractTypes.includes(contractType); } // Placeholder functions for calculating costs function calculateDeploymentCost(gasPrice, contractType) { // Calculate the deployment cost based on the gas price and contract name // You'll need to replace this with the actual calculation logic return gasPrice * 0.05; } function calculatePairCreationCost(gasPrice, contractType) { // Calculate the pair creation cost based on the gas price and contract name // You'll need to replace this with the actual calculation logic return gasPrice * 0.002; } function calculateLendingPoolApprovalCost(gasPrice, contractType) { // Calculate the lending pool approval cost based on the gas price and contract name // You'll need to replace this with the actual calculation logic return gasPrice * 0.001; } function calculateLoanInitiationCost(gasPrice, contractType) { // Calculate the loan initiation cost based on the gas price and contract name // You'll need to replace this with the actual calculation logic return gasPrice * 0.01; } function formatNativePrice(price, network) { return `${price.toFixed(4)} ${getNativeTokenSymbol(network)}`; } function formatUsdPrice(price) { return `$${price.toFixed(2)} USD`; } function formatPrice(price, isUsd, network) { if (price === "N/A" || price === undefined) { return "N/A"; } return isUsd ? formatUsdPrice(price) : formatNativePrice(price, network); } function getNativeTokenSymbol(network) { switch (network) { case "mainnet": return "ETH"; case "bsc": return "BNB"; case "polygon": return "MATIC"; case "arbitrum": return "ETH"; case "optimism": return "ETH"; case "base": return "ETH"; default: return ""; } } async function cli(args) { const rawOptions = parseArgumentsIntoOptions(args); if (rawOptions.help) { displayHelp(); return; } if (rawOptions.quote) { const options = await promptForMissingOptions(rawOptions, "quote"); await displayQuote(options); return; } renderIntroMessage(); const options = await promptForMissingOptions(rawOptions, 'create'); await createProject(options); } function displayHelp() { console.log(` Usage: xc [options] Options: --project, -p <name> Specify the project name --ticker, -t <symbol> Specify the ticker symbol --supply, -u <number> Specify the token supply --contract-type, -c <type> Specify the contract type --extensions, -e <list> Specify the extensions (comma-separated) --install, -i Install dependencies --skip-install, -s Skip dependency installation --dev Enable development mode --help, -h Display help information --quote, -q Display deployment cost quote --usd Display cost in USD --network, -n <network> Specify the network Examples: xc --project my-project --ticker MYTICKER --supply 1000000 --contract-type standard-token --extensions extension1,extension2 --install --dev xc -p my-project -t MYTICKER -u 1000000 -c standard-token -e extension1,extension2 -i --dev xc --quote --network eth --contract-type standard-token --usd `); } export { cli }; //# sourceMappingURL=cli.js.map