diff --git a/.vscode/settings.json b/.vscode/settings.json index b964237c..73efe242 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,6 @@ "source.organizeImports": "explicit" }, "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "prettier.prettier-vscode" } } diff --git a/apps/cli/package.json b/apps/cli/package.json index faedb9b4..4e2eba21 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -39,7 +39,8 @@ "semver": "^7.7.2", "smol-toml": "^1.4.2", "tmp": "^0.2.5", - "viem": "^2.37.6" + "viem": "^2.37.6", + "yaml": "^2.8.2" }, "devDependencies": { "@biomejs/biome": "catalog:", @@ -56,7 +57,6 @@ "@types/tmp": "^0.2.6", "@vitest/coverage-istanbul": "^3.2.4", "@wagmi/cli": "^2.5.1", - "copyfiles": "^2.4.1", "npm-run-all": "^4.1.5", "rimraf": "^6.0.1", "ts-node": "^10.9.2", @@ -66,13 +66,12 @@ "vitest": "^3.2.4" }, "scripts": { - "build": "run-s clean codegen compile copy-files", + "build": "run-s clean codegen compile", "clean": "rimraf dist", "codegen": "run-p codegen:wagmi", "codegen:wagmi": "wagmi generate", "compile": "tsc -p tsconfig.build.json", "postcompile": "chmod +x dist/index.js", - "copy-files": "copyfiles -u 1 \"src/**/*.yaml\" \"src/**/*.env\" \"src/**/*.txt\" dist", "lint": "biome lint", "posttest": "pnpm lint", "test": "vitest" diff --git a/apps/cli/src/compose/anvil.ts b/apps/cli/src/compose/anvil.ts new file mode 100644 index 00000000..f70e6143 --- /dev/null +++ b/apps/cli/src/compose/anvil.ts @@ -0,0 +1,36 @@ +import { Config, Service } from "../types/compose.js"; +import { DEFAULT_HEALTHCHECK } from "./common.js"; + +// Anvil service +export const ANVIL_SVC: Service = { + image: "cartesi/sdk:latest", + command: ["devnet"], + healthcheck: { + ...DEFAULT_HEALTHCHECK, + test: ["CMD", "eth_isready"], + }, + environment: { + ANVIL_IP_ADDR: "0.0.0.0", + }, +}; + +export const ANVIL_PROXY_CFG: Config = { + name: "anvil-proxy", + content: `http: + routers: + anvil: + rule: "PathPrefix(\`/anvil\`)" + middlewares: + - "remove-anvil-prefix" + service: anvil + middlewares: + remove-anvil-prefix: + replacePathRegex: + regex: "^/anvil(.*)" + replacement: "$1" + services: + anvil: + loadBalancer: + servers: + - url: "http://anvil:8545"`, +}; diff --git a/apps/cli/src/compose/builder.ts b/apps/cli/src/compose/builder.ts new file mode 100644 index 00000000..f7ead84d --- /dev/null +++ b/apps/cli/src/compose/builder.ts @@ -0,0 +1,609 @@ +import { stringify } from "yaml"; +import { ComposeFile, Config, Service } from "../../src/types/compose.js"; +import { ANVIL_PROXY_CFG, ANVIL_SVC } from "./anvil.js"; +import { BUNDLER_PROXY_CFG, BUNDLER_SVC } from "./bundler.js"; +import { DATABASE_SVC } from "./database.js"; +import { + EXPLORER_API_PROXY_CFG, + EXPLORER_API_SVC, + EXPLORER_PROXY_CFG, + EXPLORER_SVC, + SQUID_PROCESSOR_SVC, +} from "./explorer.js"; +import { PASSKEY_PROXY_CFG, PASSKEY_SVC } from "./passkey.js"; +import { PAYMASTER_PROXY_CFG, PAYMASTER_SVC } from "./paymaster.js"; +import { PROXY_SVC } from "./proxy.js"; +import { ROLLUPS_NODE_PROXY_CFG, ROLLUPS_NODE_SVC } from "./rollupsNode.js"; + +export interface ServiceOptions { + imageTag?: string; +} + +export interface ProxyServiceOptions extends ServiceOptions { + listenPort?: number; +} + +export interface RollupsNodeServiceOptions extends ServiceOptions { + cpus?: number; + memory?: number; // in MB +} + +export interface AnvilServiceOptions extends ServiceOptions { + blockTime?: number; +} + +/** + * Builder class for creating Docker Compose files with Cartesi services. + * + * Example usage: + * ```typescript + * const yaml = await new ComposeBuilder() + * .withName("my-cartesi-app") + * .withBaseServices() + * .withBundler() + * .build(); + * ``` + */ +export class ComposeBuilder { + private composeFile: ComposeFile = { + services: {}, + networks: {}, + volumes: {}, + configs: {}, + secrets: {}, + }; + + /** + * Set the project name for the Compose file + */ + withName(name: string): this { + this.composeFile.name = name; + return this; + } + + /** + * Add the Anvil service (Ethereum local node) + */ + withAnvil(options?: AnvilServiceOptions): this { + if (!this.composeFile.services!.anvil) { + this.composeFile.services!.anvil = ANVIL_SVC; + this.resolveDependencies("anvil"); + + this.addServiceConfig( + ANVIL_PROXY_CFG, + "proxy", + "/etc/traefik/conf.d/anvil.yaml", + ); + } + + this.composeFile.services!.anvil = { + ...ANVIL_SVC, + ...this.composeFile.services!.anvil, + }; + + if (options?.blockTime !== undefined) { + (this.composeFile.services!.anvil.command as Array).push( + `--block-time=${options.blockTime.toString()}`, + ); + } + + if (options?.imageTag) { + this.setImageTag("anvil", options); + } + + return this; + } + + /** + * Add the Database service (PostgreSQL) + */ + withDatabase(options?: ServiceOptions): this { + if (!this.composeFile.services!.database) { + this.composeFile.services!.database = DATABASE_SVC; + this.resolveDependencies("database"); + } + + this.composeFile.services!.database = { + ...DATABASE_SVC, + ...this.composeFile.services!.database, + }; + + if (options?.imageTag) { + this.setImageTag("database", options); + } + + return this; + } + + /** + * Add the Proxy service (Traefik) + */ + withProxy(options?: ProxyServiceOptions): this { + if (!this.composeFile.services!.proxy) { + this.composeFile.services!.proxy = PROXY_SVC; + this.resolveDependencies("proxy"); + } + + this.composeFile.services!.proxy = { + ...PROXY_SVC, + ...this.composeFile.services!.proxy, + }; + + if (options?.imageTag) { + this.setImageTag("proxy", options); + } + + if (options?.listenPort) { + this.composeFile.services!.proxy.ports = [ + `${options.listenPort}:8088`, + ]; + } + + return this; + } + + /** + * Add the Rollups Node service + */ + withRollupsNode(options?: RollupsNodeServiceOptions): this { + if (!this.composeFile.services!["rollups-node"]) { + this.composeFile.services!["rollups-node"] = ROLLUPS_NODE_SVC; + this.resolveDependencies("rollups-node"); + + this.addServiceConfig( + ROLLUPS_NODE_PROXY_CFG, + "proxy", + "/etc/traefik/conf.d/rollups-node.yaml", + ); + } + + this.composeFile.services!["rollups-node"] = { + ...ROLLUPS_NODE_SVC, + ...this.composeFile.services!["rollups-node"], + }; + + if (options?.imageTag) { + this.setImageTag("rollups-node", options); + } + + if (options?.cpus) { + this.setCpuLimit("rollups-node", options.cpus); + } + + if (options?.memory) { + this.setMemoryLimit("rollups-node", options.memory); + } + + return this; + } + + /** + * Add the Bundler service (ERC-4337) + */ + withBundler(options?: ServiceOptions): this { + if (!this.composeFile.services!.bundler) { + this.composeFile.services!.bundler = BUNDLER_SVC; + this.resolveDependencies("bundler"); + + this.addServiceConfig( + BUNDLER_PROXY_CFG, + "proxy", + "/etc/traefik/conf.d/bundler.yaml", + ); + } + + this.composeFile.services!.bundler = { + ...BUNDLER_SVC, + ...this.composeFile.services!.bundler, + }; + + if (options?.imageTag) { + this.setImageTag("bundler", options); + } + + return this; + } + + /** + * Add the Explorer API service + */ + withExplorerApi(options?: ServiceOptions): this { + if (!this.composeFile.services!["explorer-api"]) { + this.composeFile.services!["explorer-api"] = EXPLORER_API_SVC; + this.withSquidProcessor(options); + this.resolveDependencies("explorer-api"); + + this.addServiceConfig( + EXPLORER_API_PROXY_CFG, + "proxy", + "/etc/traefik/conf.d/explorer-api.yaml", + ); + } + + this.composeFile.services!["explorer-api"] = { + ...EXPLORER_API_SVC, + ...this.composeFile.services!["explorer-api"], + }; + + if (options?.imageTag) { + this.setImageTag("explorer-api", options); + } + + return this; + } + + /** + * Add the Squid Processor service + */ + withSquidProcessor(options?: ServiceOptions): this { + // Initialize + if (!this.composeFile.services!["squid-processor"]) { + this.composeFile.services!["squid-processor"] = SQUID_PROCESSOR_SVC; + this.resolveDependencies("squid-processor"); + } + + // Merge existing + this.composeFile.services!["squid-processor"] = { + ...SQUID_PROCESSOR_SVC, + ...this.composeFile.services!["squid-processor"], + }; + + if (options?.imageTag) { + this.setImageTag("squid-processor", options); + } + + return this; + } + + /** + * Add the Explorer service + */ + withExplorer(options?: ProxyServiceOptions): this { + if (!this.composeFile.services!.explorer) { + this.composeFile.services!.explorer = EXPLORER_SVC; + this.resolveDependencies("explorer"); + + this.addServiceConfig( + EXPLORER_PROXY_CFG, + "proxy", + "/etc/traefik/conf.d/explorer.yaml", + ); + } + + this.composeFile.services!.explorer = { + ...EXPLORER_SVC, + ...this.composeFile.services!.explorer, + }; + + if (options?.imageTag) { + this.setImageTag("explorer", options); + } + + if (options?.listenPort) { + this.setEnvironmentVariable( + "explorer", + "NODE_RPC_URL", + `http://127.0.0.1:${options.listenPort}/anvil`, + ); + this.setEnvironmentVariable( + "explorer", + "EXPLORER_API_URL", + `http://127.0.0.1:${options.listenPort}/explorer-api/graphql`, + ); + } + + return this; + } + + /** + * Add the Paymaster service + */ + withPaymaster(options?: ServiceOptions): this { + if (!this.composeFile.services!.paymaster) { + this.composeFile.services!.paymaster = PAYMASTER_SVC; + this.resolveDependencies("paymaster"); + + this.addServiceConfig( + PAYMASTER_PROXY_CFG, + "proxy", + "/etc/traefik/conf.d/paymaster.yaml", + ); + } + + this.composeFile.services!.paymaster = { + ...PAYMASTER_SVC, + ...this.composeFile.services!.paymaster, + }; + + if (options?.imageTag) { + this.setImageTag("paymaster", options); + } + + return this; + } + + /** + * Add the Passkey Server service + */ + withPasskeyServer(options?: ServiceOptions): this { + if (!this.composeFile.services!["passkey-server"]) { + this.composeFile.services!["passkey-server"] = PASSKEY_SVC; + this.resolveDependencies("passkey-server"); + + this.addServiceConfig( + PASSKEY_PROXY_CFG, + "proxy", + "/etc/traefik/conf.d/passkey-server.yaml", + ); + } + + this.composeFile.services!["passkey-server"] = { + ...PASSKEY_SVC, + ...this.composeFile.services!["passkey-server"], + }; + + if (options?.imageTag) { + this.setImageTag("passkey-server", options); + } + + return this; + } + + /** + * Add a config definition + * @param config - Config configuration + */ + withConfig(config: Config): this { + const name: string = config.name; + this.composeFile.configs![name] = config; + return this; + } + + /** + * Build and return the ComposeFile object + */ + buildComposeFile(): ComposeFile { + // Clean up empty collections + if (Object.keys(this.composeFile.services!).length === 0) + delete this.composeFile.services; + if (Object.keys(this.composeFile.networks!).length === 0) + delete this.composeFile.networks; + if (Object.keys(this.composeFile.volumes!).length === 0) + delete this.composeFile.volumes; + if (Object.keys(this.composeFile.configs!).length === 0) + delete this.composeFile.configs; + if (Object.keys(this.composeFile.secrets!).length === 0) + delete this.composeFile.secrets; + + return this.composeFile; + } + + /** + * Build and return the YAML string for the Docker Compose file + */ + build(): string { + const composeFile = this.buildComposeFile(); + return stringify(composeFile, { + lineWidth: 0, // Disable line wrapping + indent: 2, + }); + } + + /** + * Reset the builder to start fresh + */ + reset(): this { + this.composeFile = { + services: {}, + networks: {}, + volumes: {}, + configs: {}, + secrets: {}, + }; + return this; + } + + // Private helper methods + + /** + * Set an environment variable for a service + * @param service + * @param key + * @param value + */ + private setEnvironmentVariable( + service: string, + key: string, + value: string, + ): this { + if (!this.composeFile.services![service]) { + throw new Error( + `Service '${service}' does not exist. Please add it before setting environment variables.`, + ); + } + + if ( + !this.composeFile.services![service].environment || + Array.isArray(this.composeFile.services![service].environment) + ) { + this.composeFile.services![service].environment = {}; + } + + ( + this.composeFile.services![service].environment as Record< + string, + string + > + )[key] = value; + + return this; + } + + /** + * Define CPU limit for a service + * @param service + * @param cpus + */ + private setCpuLimit(service: string, cpus: number): this { + if (!this.composeFile.services![service]) { + throw new Error( + `Service '${service}' does not exist. Please add it before setting CPU limits.`, + ); + } + + if (!this.composeFile.services![service].deploy) { + this.composeFile.services![service].deploy = {}; + } + + if (!this.composeFile.services![service].deploy!.resources) { + this.composeFile.services![service].deploy!.resources = {}; + } + + this.composeFile.services![service].deploy!.resources!.limits = { + ...(this.composeFile.services![service].deploy!.resources!.limits || + {}), + cpus: cpus.toString(), + }; + + return this; + } + + /** + * Define Memory limit for a service + * @param service + * @param memoryMB + */ + private setMemoryLimit(service: string, memoryMB: number): this { + if (!this.composeFile.services![service]) { + throw new Error( + `Service '${service}' does not exist. Please add it before setting memory limits.`, + ); + } + + if (!this.composeFile.services![service].deploy) { + this.composeFile.services![service].deploy = {}; + } + + if (!this.composeFile.services![service].deploy!.resources) { + this.composeFile.services![service].deploy!.resources = {}; + } + + this.composeFile.services![service].deploy!.resources!.limits = { + ...(this.composeFile.services![service].deploy!.resources!.limits || + {}), + memory: `${memoryMB}M`, + }; + return this; + } + + /** + * Build an image string from ServiceOptions + * @param options - ServiceOptions containing imageName and/or imageTag + * @param currentImage - Current image string to use as fallback + * @returns Complete image string in format "imageName:imageTag" + */ + private setImageTag(service: string, options: ServiceOptions): this { + if (!this.composeFile.services![service]) { + throw new Error( + `Service '${service}' does not exist. Please add it before setting image tag.`, + ); + } + + const currentImage = this.composeFile.services![service].image; + const [currentName, currentTag] = currentImage?.includes(":") + ? currentImage.split(":", 2) + : [currentImage || "", "latest"]; + + const imageTag = options.imageTag || currentTag; + + this.composeFile.services![service].image = + `${currentName}:${imageTag}`; + + return this; + } + + /** + * Generic helper to add a config to a service and register it in the compose file + * @param configContent - Config object with content + * @param serviceName - Name of the service to attach the config to + * @param targetPath - Path where the config will be mounted in the container + */ + private addServiceConfig( + configContent: Config, + serviceName: string, + targetPath: string, + ): void { + const configName = configContent.name; + // Register the config in the compose file + this.withConfig(configContent); + + // Ensure the target service exists by adding it if needed + this.ensureServiceExists(serviceName); + + // Initialize configs array if it doesn't exist + if (!(this.composeFile.services![serviceName] as Service).configs) { + (this.composeFile.services![serviceName] as Service).configs = []; + } + + // Add config reference to the service + (this.composeFile.services![serviceName] as Service).configs!.push({ + source: configName, + target: targetPath, + }); + } + + /** + * Ensure a service exists in the compose file by calling its builder method if needed + * @param serviceName - Name of the service to ensure exists + */ + private ensureServiceExists(serviceName: string): void { + // If service already exists, nothing to do + if (this.composeFile.services![serviceName]) { + return; + } + + // Map of service names to their builder methods + const serviceMap: Record this> = { + anvil: () => this.withAnvil(), + database: () => this.withDatabase(), + proxy: () => this.withProxy(), + bundler: () => this.withBundler(), + "explorer-api": () => this.withExplorerApi(), + "squid-processor": () => this.withSquidProcessor(), + explorer: () => this.withExplorer(), + paymaster: () => this.withPaymaster(), + "passkey-server": () => this.withPasskeyServer(), + "rollups-node": () => this.withRollupsNode(), + }; + + const addMethod = serviceMap[serviceName]; + if (addMethod) { + addMethod.call(this); + } else { + throw new Error( + `Service '${serviceName}' does not exist and has no known builder method.`, + ); + } + } + + /** + * Automatically resolve and add dependencies for a service based on its depends_on field + * @param serviceName - Name of the service whose dependencies should be resolved + */ + private resolveDependencies(serviceName: string): void { + const service = this.composeFile.services![serviceName]; + if (!service || !service.depends_on) { + return; + } + + // Handle both array and object format for depends_on + const dependencies = Array.isArray(service.depends_on) + ? service.depends_on + : Object.keys(service.depends_on); + + // Add each dependency if it doesn't already exist + for (const dep of dependencies) { + this.ensureServiceExists(dep); + } + } +} diff --git a/apps/cli/src/compose/bundler.ts b/apps/cli/src/compose/bundler.ts new file mode 100644 index 00000000..d48e1ec3 --- /dev/null +++ b/apps/cli/src/compose/bundler.ts @@ -0,0 +1,67 @@ +import { Config, Service } from "../types/compose.js"; +import { DEFAULT_HEALTHCHECK } from "./common.js"; + +export const BUNDLER_SVC: Service = { + image: "cartesi/sdk:latest", + command: [ + "alto", + "--entrypoints", + "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789,0x0000000071727De22E5E9d8BAf0edAc6f37da032", + "--log-level", + "info", + "--rpc-url", + "http://anvil:8545", + "--min-executor-balance", + "0", + "--utility-private-key", + "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97", + "--executor-private-keys", + "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6,0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356,0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e,0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba,0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a", + "--safe-mode", + "false", + "--port", + "4337", + "--public-client-log-level", + "error", + "--wallet-client-log-level", + "error", + "--enable-debug-endpoints", + ], + healthcheck: { + ...DEFAULT_HEALTHCHECK, + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:4337/health"], + }, +}; + +export const BUNDLER_PROXY_CFG: Config = { + name: "bundler-proxy", + content: `http: + routers: + bundler: + rule: "PathPrefix(\`/bundler\`)" + middlewares: + - "cors" + - "remove-bundler-prefix" + service: bundler + middlewares: + cors: + headers: + accessControlAllowMethods: + - GET + - OPTIONS + - PUT + accessControlAllowHeaders: "*" + accessControlAllowOriginList: + - "*" + accessControlMaxAge: 100 + addVaryHeader: true + remove-bundler-prefix: + replacePathRegex: + regex: "^/bundler/(.*)" + replacement: "/$1" + services: + bundler: + loadBalancer: + servers: + - url: "http://bundler:4337"`, +}; diff --git a/apps/cli/src/compose/common.ts b/apps/cli/src/compose/common.ts new file mode 100644 index 00000000..2fc3c747 --- /dev/null +++ b/apps/cli/src/compose/common.ts @@ -0,0 +1,60 @@ +import { Healthcheck } from "../types/compose"; + +export const DB_ENV: Record = { + DB_PORT: 5432, + DB_HOST: "database", + DB_PASS: "password", +}; + +export const DEFAULT_HEALTHCHECK: Healthcheck = { + start_period: "10s", + start_interval: "200ms", + interval: "10s", + timeout: "1s", + retries: 5, +}; + +export const DEFAULT_ENV: Record = { + CARTESI_LOG_LEVEL: "${CARTESI_LOG_LEVEL:-info}", + // features + CARTESI_FEATURE_INPUT_READER_ENABLED: + "${CARTESI_FEATURE_INPUT_READER_ENABLED:-true}", + CARTESI_FEATURE_CLAIM_SUBMISSION_ENABLED: + "${CARTESI_FEATURE_CLAIM_SUBMISSION_ENABLED:-true}", + CARTESI_FEATURE_MACHINE_HASH_CHECK_ENABLED: + "${CARTESI_FEATURE_MACHINE_HASH_CHECK_ENABLED:-true}", + CARTESI_SNAPSHOTS_DIR: "/var/lib/cartesi-rollups-node/snapshots", + // rollups + CARTESI_EVM_READER_RETRY_POLICY_MAX_RETRIES: + "${CARTESI_EVM_READER_RETRY_POLICY_MAX_RETRIES:-3}", + CARTESI_EVM_READER_RETRY_POLICY_MAX_DELAY: + "${CARTESI_EVM_READER_RETRY_POLICY_MAX_DELAY:-3}", + CARTESI_ADVANCER_POLLING_INTERVAL: + "${CARTESI_ADVANCER_POLLING_INTERVAL:-3}", + CARTESI_VALIDATOR_POLLING_INTERVAL: + "${CARTESI_VALIDATOR_POLLING_INTERVAL:-3}", + CARTESI_CLAIMER_POLLING_INTERVAL: "${CARTESI_CLAIMER_POLLING_INTERVAL:-3}", + CARTESI_MAX_STARTUP_TIME: "${CARTESI_MAX_STARTUP_TIME:-15}", + // blockchain + CARTESI_BLOCKCHAIN_ID: "${CARTESI_BLOCKCHAIN_ID:-13370}", + CARTESI_BLOCKCHAIN_HTTP_ENDPOINT: + "${CARTESI_BLOCKCHAIN_HTTP_ENDPOINT:-http://anvil:8545}", + CARTESI_BLOCKCHAIN_WS_ENDPOINT: + "${CARTESI_BLOCKCHAIN_WS_ENDPOINT:-ws://anvil:8545}", + CARTESI_BLOCKCHAIN_DEFAULT_BLOCK: + "${CARTESI_BLOCKCHAIN_DEFAULT_BLOCK:-latest}", + // contracts + CARTESI_CONTRACTS_APPLICATION_FACTORY_ADDRESS: + "${CARTESI_CONTRACTS_APPLICATION_FACTORY_ADDRESS:-0xc7006f70875BaDe89032001262A846D3Ee160051}", + CARTESI_CONTRACTS_AUTHORITY_FACTORY_ADDRESS: + "${CARTESI_CONTRACTS_AUTHORITY_FACTORY_ADDRESS:-0xC7003566dD09Aa0fC0Ce201aC2769aFAe3BF0051}", + CARTESI_CONTRACTS_INPUT_BOX_ADDRESS: + "${CARTESI_CONTRACTS_INPUT_BOX_ADDRESS:-0xc70074BDD26d8cF983Ca6A5b89b8db52D5850051}", + CARTESI_CONTRACTS_SELF_HOSTED_APPLICATION_FACTORY_ADDRESS: + "${CARTESI_CONTRACTS_SELF_HOSTED_APPLICATION_FACTORY_ADDRESS:-0xc700285Ab555eeB5201BC00CFD4b2CC8DED90051}", + // auth + CARTESI_AUTH_MNEMONIC: + "${CARTESI_AUTH_MNEMONIC:-test test test test test test test test test test test junk}", + // postgres + CARTESI_DATABASE_CONNECTION: `postgres://postgres:${DB_ENV.DB_PASS}@${DB_ENV.DB_HOST}:${DB_ENV.DB_PORT}/rollupsdb?sslmode=disable`, +}; diff --git a/apps/cli/src/compose/database.ts b/apps/cli/src/compose/database.ts new file mode 100644 index 00000000..b323cd22 --- /dev/null +++ b/apps/cli/src/compose/database.ts @@ -0,0 +1,14 @@ +import { Service } from "../types/compose.js"; +import { DB_ENV, DEFAULT_HEALTHCHECK } from "./common.js"; + +// Database service +export const DATABASE_SVC: Service = { + image: "cartesi/rollups-database:latest", + healthcheck: { + ...DEFAULT_HEALTHCHECK, + test: ["CMD-SHELL", "pg_isready -U postgres || exit 1"], + }, + environment: { + POSTGRES_PASSWORD: DB_ENV.DB_PASS, + }, +}; diff --git a/apps/cli/src/compose/default.env b/apps/cli/src/compose/default.env deleted file mode 100644 index 34474b47..00000000 --- a/apps/cli/src/compose/default.env +++ /dev/null @@ -1,36 +0,0 @@ -# cartesi/rollups-node - -#logs -CARTESI_LOG_LEVEL="${CARTESI_LOG_LEVEL:-info}" - -# features -CARTESI_FEATURE_INPUT_READER_ENABLED="${CARTESI_FEATURE_INPUT_READER_ENABLED:-true}" -CARTESI_FEATURE_CLAIM_SUBMISSION_ENABLED="${CARTESI_FEATURE_CLAIM_SUBMISSION_ENABLED:-true}" -CARTESI_FEATURE_MACHINE_HASH_CHECK_ENABLED="${CARTESI_FEATURE_MACHINE_HASH_CHECK_ENABLED:-true}" -CARTESI_SNAPSHOTS_DIR="/var/lib/cartesi-rollups-node/snapshots" - -# rollups -CARTESI_EVM_READER_RETRY_POLICY_MAX_RETRIES="${CARTESI_EVM_READER_RETRY_POLICY_MAX_RETRIES:-3}" -CARTESI_EVM_READER_RETRY_POLICY_MAX_DELAY="${CARTESI_EVM_READER_RETRY_POLICY_MAX_DELAY:-3}" -CARTESI_ADVANCER_POLLING_INTERVAL="${CARTESI_ADVANCER_POLLING_INTERVAL:-3}" -CARTESI_VALIDATOR_POLLING_INTERVAL="${CARTESI_VALIDATOR_POLLING_INTERVAL:-3}" -CARTESI_CLAIMER_POLLING_INTERVAL="${CARTESI_CLAIMER_POLLING_INTERVAL:-3}" -CARTESI_MAX_STARTUP_TIME="${CARTESI_MAX_STARTUP_TIME:-15}" - -# blockchain -CARTESI_BLOCKCHAIN_ID="${CARTESI_BLOCKCHAIN_ID:-13370}" -CARTESI_BLOCKCHAIN_HTTP_ENDPOINT="${CARTESI_BLOCKCHAIN_HTTP_ENDPOINT:-http://anvil:8545}" -CARTESI_BLOCKCHAIN_WS_ENDPOINT="${CARTESI_BLOCKCHAIN_WS_ENDPOINT:-ws://anvil:8545}" -CARTESI_BLOCKCHAIN_DEFAULT_BLOCK="${CARTESI_BLOCKCHAIN_DEFAULT_BLOCK:-latest}" - -# contracts -CARTESI_CONTRACTS_APPLICATION_FACTORY_ADDRESS="${CARTESI_CONTRACTS_APPLICATION_FACTORY_ADDRESS:-0xc7006f70875BaDe89032001262A846D3Ee160051}" -CARTESI_CONTRACTS_AUTHORITY_FACTORY_ADDRESS="${CARTESI_CONTRACTS_AUTHORITY_FACTORY_ADDRESS:-0xC7003566dD09Aa0fC0Ce201aC2769aFAe3BF0051}" -CARTESI_CONTRACTS_INPUT_BOX_ADDRESS="${CARTESI_CONTRACTS_INPUT_BOX_ADDRESS:-0xc70074BDD26d8cF983Ca6A5b89b8db52D5850051}" -CARTESI_CONTRACTS_SELF_HOSTED_APPLICATION_FACTORY_ADDRESS="${CARTESI_CONTRACTS_SELF_HOSTED_APPLICATION_FACTORY_ADDRESS:-0xc700285Ab555eeB5201BC00CFD4b2CC8DED90051}" - -# auth -CARTESI_AUTH_MNEMONIC="${CARTESI_AUTH_MNEMONIC:-test test test test test test test test test test test junk}" - -# postgres -CARTESI_DATABASE_CONNECTION="postgres://postgres:password@database:5432/rollupsdb?sslmode=disable" diff --git a/apps/cli/src/compose/docker-compose-anvil.yaml b/apps/cli/src/compose/docker-compose-anvil.yaml deleted file mode 100644 index 4e024fbd..00000000 --- a/apps/cli/src/compose/docker-compose-anvil.yaml +++ /dev/null @@ -1,39 +0,0 @@ -configs: - anvil_proxy: - content: | - http: - routers: - anvil: - rule: "PathPrefix(`/anvil`)" - middlewares: - - "remove-anvil-prefix" - service: anvil - middlewares: - remove-anvil-prefix: - replacePathRegex: - regex: "^/anvil(.*)" - replacement: "$1" - services: - anvil: - loadBalancer: - servers: - - url: "http://anvil:8545" - -services: - anvil: - image: ${CARTESI_SDK_IMAGE} - command: ["devnet", "--block-time", "${CARTESI_BLOCK_TIME:-2}"] - healthcheck: - test: ["CMD", "eth_isready"] - start_period: 10s - start_interval: 200ms - interval: 10s - timeout: 1s - retries: 5 - environment: - ANVIL_IP_ADDR: 0.0.0.0 - - proxy: - configs: - - source: anvil_proxy - target: /etc/traefik/conf.d/anvil.yaml diff --git a/apps/cli/src/compose/docker-compose-bundler.yaml b/apps/cli/src/compose/docker-compose-bundler.yaml deleted file mode 100644 index 9ef9f392..00000000 --- a/apps/cli/src/compose/docker-compose-bundler.yaml +++ /dev/null @@ -1,71 +0,0 @@ -configs: - bundler_proxy: - content: | - http: - routers: - bundler: - rule: "PathPrefix(`/bundler`)" - middlewares: - - "cors" - - "remove-bundler-prefix" - service: bundler - middlewares: - cors: - headers: - accessControlAllowMethods: - - GET - - OPTIONS - - PUT - accessControlAllowHeaders: "*" - accessControlAllowOriginList: - - "*" - accessControlMaxAge: 100 - addVaryHeader: true - remove-bundler-prefix: - replacePathRegex: - regex: "^/bundler/(.*)" - replacement: "/$1" - services: - bundler: - loadBalancer: - servers: - - url: "http://bundler:4337" - -services: - bundler: - image: ${CARTESI_SDK_IMAGE} - command: - - "alto" - - "--entrypoints" - - "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789,0x0000000071727De22E5E9d8BAf0edAc6f37da032" - - "--log-level" - - "info" - - "--rpc-url" - - "http://anvil:8545" - - "--min-executor-balance" - - "0" - - "--utility-private-key" - - "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97" - - "--executor-private-keys" - - "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6,0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356,0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e,0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba,0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a" - - "--safe-mode" - - "false" - - "--port" - - "4337" - - "--public-client-log-level" - - "error" - - "--wallet-client-log-level" - - "error" - - "--enable-debug-endpoints" - healthcheck: - test: ["CMD", "curl", "-fsS", "http://127.0.0.1:4337/health"] - start_period: 10s - start_interval: 200ms - interval: 10s - timeout: 1s - retries: 5 - - proxy: - configs: - - source: bundler_proxy - target: /etc/traefik/conf.d/bundler.yaml diff --git a/apps/cli/src/compose/docker-compose-database.yaml b/apps/cli/src/compose/docker-compose-database.yaml deleted file mode 100644 index 55470b24..00000000 --- a/apps/cli/src/compose/docker-compose-database.yaml +++ /dev/null @@ -1,12 +0,0 @@ -services: - database: - image: cartesi/rollups-database:${CARTESI_SDK_VERSION} - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres || exit 1"] - start_period: 10s - start_interval: 200ms - interval: 10s - timeout: 1s - retries: 5 - environment: - - POSTGRES_PASSWORD=password diff --git a/apps/cli/src/compose/docker-compose-explorer.yaml b/apps/cli/src/compose/docker-compose-explorer.yaml deleted file mode 100644 index ea2edb83..00000000 --- a/apps/cli/src/compose/docker-compose-explorer.yaml +++ /dev/null @@ -1,96 +0,0 @@ -x-explorer_db_env: &explorer_db_env - DB_NAME: explorer - DB_PORT: 5432 - DB_HOST: database - DB_PASS: password - -configs: - explorer_api_proxy: - content: | - http: - routers: - explorer-api: - rule: "PathPrefix(`/explorer-api`)" - middlewares: - - "remove-explorer-api-prefix" - service: explorer-api - middlewares: - remove-explorer-api-prefix: - replacePathRegex: - regex: "^/explorer-api/(.*)" - replacement: "/$1" - services: - explorer-api: - loadBalancer: - servers: - - url: "http://explorer_api:4350" - explorer_proxy: - content: | - http: - routers: - explorer: - rule: "PathPrefix(`/explorer`)" - service: explorer - services: - explorer: - loadBalancer: - servers: - - url: "http://explorer:3000" - -services: - explorer_api: - image: cartesi/rollups-explorer-api:1.0.0 - environment: - <<: *explorer_db_env - GQL_PORT: 4350 - expose: - - 4350 - command: ["sqd", "serve:prod"] - healthcheck: - test: - [ - "CMD", - "wget", - "--spider", - "-q", - "http://127.0.0.1:4350/graphql?query=%7B__typename%7D", - ] - start_period: 10s - start_interval: 200ms - interval: 10s - timeout: 1s - retries: 5 - depends_on: - database: - condition: service_healthy - - squid_processor: - image: cartesi/rollups-explorer-api:1.0.0 - environment: - <<: *explorer_db_env - CHAIN_IDS: ${CARTESI_BLOCKCHAIN_ID:-13370} - RPC_URL_13370: ${RPC_URL:-http://anvil:8545} - BLOCK_CONFIRMATIONS_13370: 0 - GENESIS_BLOCK_13370: 1 - command: ["sqd", "process:prod"] - depends_on: - database: - condition: service_healthy - - explorer: - image: cartesi/rollups-explorer:1.3.3 - environment: - NODE_RPC_URL: "http://127.0.0.1:${CARTESI_LISTEN_PORT:-6751}/anvil" - EXPLORER_API_URL: "http://127.0.0.1:${CARTESI_LISTEN_PORT:-6751}/explorer-api/graphql" - expose: - - 3000 - depends_on: - database: - condition: service_healthy - - proxy: - configs: - - source: explorer_proxy - target: /etc/traefik/conf.d/explorer.yaml - - source: explorer_api_proxy - target: /etc/traefik/conf.d/explorer-api.yaml diff --git a/apps/cli/src/compose/docker-compose-node-cpus.yaml b/apps/cli/src/compose/docker-compose-node-cpus.yaml deleted file mode 100644 index 46955e08..00000000 --- a/apps/cli/src/compose/docker-compose-node-cpus.yaml +++ /dev/null @@ -1,6 +0,0 @@ -services: - rollups-node: - deploy: - resources: - limits: - cpus: "${CARTESI_ROLLUPS_NODE_CPUS}" diff --git a/apps/cli/src/compose/docker-compose-node-memory.yaml b/apps/cli/src/compose/docker-compose-node-memory.yaml deleted file mode 100644 index cd5c8421..00000000 --- a/apps/cli/src/compose/docker-compose-node-memory.yaml +++ /dev/null @@ -1,6 +0,0 @@ -services: - rollups-node: - deploy: - resources: - limits: - memory: "${CARTESI_ROLLUPS_NODE_MEMORY}M" diff --git a/apps/cli/src/compose/docker-compose-node.yaml b/apps/cli/src/compose/docker-compose-node.yaml deleted file mode 100644 index b4a8d059..00000000 --- a/apps/cli/src/compose/docker-compose-node.yaml +++ /dev/null @@ -1,50 +0,0 @@ -configs: - rollups_node_proxy: - content: | - http: - routers: - inspect_server: - rule: "PathPrefix(`/inspect`)" - service: inspect_server - rpc_server: - rule: "PathPrefix(`/rpc`)" - service: rpc_server - services: - inspect_server: - loadBalancer: - servers: - - url: "http://rollups-node:10012" - rpc_server: - loadBalancer: - servers: - - url: "http://rollups-node:10011" - -services: - rollups-node: - image: cartesi/rollups-runtime:${CARTESI_SDK_VERSION} - depends_on: - database: - condition: service_healthy - anvil: - condition: service_healthy - expose: - - "10000" - - "10011" - - "10012" - healthcheck: - test: ["CMD", "curl", "-fsS", "http://127.0.0.1:10000/livez"] - start_period: 10s - start_interval: 200ms - interval: 10s - timeout: 1s - retries: 5 - command: ["cartesi-rollups-node"] - env_file: - - ${CARTESI_BIN_PATH}/compose/default.env - volumes: - - ./.cartesi:/var/lib/cartesi-rollups-node/snapshots:ro - - proxy: - configs: - - source: rollups_node_proxy - target: /etc/traefik/conf.d/rollups-node.yaml diff --git a/apps/cli/src/compose/docker-compose-passkey-server.yaml b/apps/cli/src/compose/docker-compose-passkey-server.yaml deleted file mode 100644 index 86ef45c9..00000000 --- a/apps/cli/src/compose/docker-compose-passkey-server.yaml +++ /dev/null @@ -1,37 +0,0 @@ -configs: - passkey_server_proxy: - content: | - http: - routers: - passkey-server: - rule: "PathPrefix(`/passkey`)" - middlewares: - - "remove-passkey-server-prefix" - service: passkey-server - middlewares: - remove-passkey-server-prefix: - replacePathRegex: - regex: "^/passkey/(.*)" - replacement: "/$1" - services: - passkey-server: - loadBalancer: - servers: - - url: "http://passkey-server:3000" - -services: - passkey-server: - image: ${CARTESI_SDK_IMAGE} - command: ["passkey-server"] - healthcheck: - test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000/health"] - start_period: 10s - start_interval: 200ms - interval: 10s - timeout: 1s - retries: 5 - - proxy: - configs: - - source: passkey_server_proxy - target: /etc/traefik/conf.d/passkey-server.yaml diff --git a/apps/cli/src/compose/docker-compose-paymaster.yaml b/apps/cli/src/compose/docker-compose-paymaster.yaml deleted file mode 100644 index 3f5d0bcf..00000000 --- a/apps/cli/src/compose/docker-compose-paymaster.yaml +++ /dev/null @@ -1,40 +0,0 @@ -configs: - paymaster_proxy: - content: | - http: - routers: - paymaster: - rule: "PathPrefix(`/paymaster`)" - middlewares: - - "remove-paymaster-prefix" - service: paymaster - middlewares: - remove-paymaster-prefix: - replacePathRegex: - regex: "^/paymaster/(.*)" - replacement: "/$1" - services: - paymaster: - loadBalancer: - servers: - - url: "http://paymaster:3000" - -services: - paymaster: - image: ${CARTESI_SDK_IMAGE} - command: "mock-verifying-paymaster" - environment: - - ALTO_RPC=http://bundler:4337 - - ANVIL_RPC=http://anvil:8545 - healthcheck: - test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000/ping"] - start_period: 10s - start_interval: 200ms - interval: 10s - timeout: 1s - retries: 5 - - proxy: - configs: - - source: paymaster_proxy - target: /etc/traefik/conf.d/paymaster.yaml diff --git a/apps/cli/src/compose/docker-compose-proxy.yaml b/apps/cli/src/compose/docker-compose-proxy.yaml deleted file mode 100644 index 4d1012aa..00000000 --- a/apps/cli/src/compose/docker-compose-proxy.yaml +++ /dev/null @@ -1,24 +0,0 @@ -services: - proxy: - image: traefik:v3.3.4 - healthcheck: - test: ["CMD", "traefik", "healthcheck", "--ping"] - start_period: 10s - start_interval: 200ms - interval: 10s - timeout: 1s - retries: 5 - command: - [ - "--ping=true", - "--entryPoints.web.address=:8088", - "--entryPoints.traefik.address=:8080", - "--metrics.prometheus=true", - "--metrics.prometheus.addServicesLabels=true", - "--providers.file.directory=/etc/traefik/conf.d", - "--providers.file.watch=true", - "--log", - "--log.level=INFO", - ] - ports: - - ${CARTESI_LISTEN_PORT:-6751}:8088 diff --git a/apps/cli/src/compose/explorer.ts b/apps/cli/src/compose/explorer.ts new file mode 100644 index 00000000..132569bb --- /dev/null +++ b/apps/cli/src/compose/explorer.ts @@ -0,0 +1,96 @@ +import { Config, Service } from "../types/compose.js"; +import { DB_ENV, DEFAULT_HEALTHCHECK } from "./common.js"; + +// Explorer API service +export const EXPLORER_API_SVC: Service = { + image: "cartesi/rollups-explorer-api:latest", + environment: { + ...DB_ENV, + DB_NAME: "explorer", + GQL_PORT: 4350, + }, + expose: ["4350"], + command: ["sqd", "serve:prod"], + healthcheck: { + ...DEFAULT_HEALTHCHECK, + test: [ + "CMD", + "wget", + "--spider", + "-q", + "http://127.0.0.1:4350/graphql?query=%7B__typename%7D", + ], + }, + depends_on: { + database: { condition: "service_healthy" }, + "squid-processor": { condition: "service_started" }, + }, +}; + +// Explorer API Proxy configuration +export const EXPLORER_API_PROXY_CFG: Config = { + name: "explorer-api-proxy", + content: `http: + routers: + explorer-api: + rule: "PathPrefix(\`/explorer-api\`)" + middlewares: + - "remove-explorer-api-prefix" + service: explorer-api + middlewares: + remove-explorer-api-prefix: + replacePathRegex: + regex: "^/explorer-api/(.*)" + replacement: "/$1" + services: + explorer-api: + loadBalancer: + servers: + - url: "http://explorer-api:4350" +`, +}; + +// Squid Processor service +export const SQUID_PROCESSOR_SVC: Service = { + image: "cartesi/rollups-explorer-api:latest", + environment: { + ...DB_ENV, + DB_NAME: "explorer", + CHAIN_IDS: "${CARTESI_BLOCKCHAIN_ID:-13370}", + RPC_URL_13370: "${RPC_URL:-http://anvil:8545}", + BLOCK_CONFIRMATIONS_13370: 0, + GENESIS_BLOCK_13370: 1, + }, + command: ["sqd", "process:prod"], + depends_on: { + database: { condition: "service_healthy" }, + }, +}; + +// Explorer service +export const EXPLORER_SVC: Service = { + image: "cartesi/rollups-explorer:latest", + environment: { + NODE_RPC_URL: "http://127.0.0.1:6571/anvil", + EXPLORER_API_URL: "http://127.0.0.1:6571/explorer-api/graphql", + }, + expose: ["3000"], + depends_on: { + database: { condition: "service_healthy" }, + }, +}; + +// Explorer Proxy configuration +export const EXPLORER_PROXY_CFG: Config = { + name: "explorer-proxy", + content: `http: + routers: + explorer: + rule: "PathPrefix(\`/explorer\`)" + service: explorer + services: + explorer: + loadBalancer: + servers: + - url: "http://explorer:3000"`, +}; diff --git a/apps/cli/src/compose/passkey.ts b/apps/cli/src/compose/passkey.ts new file mode 100644 index 00000000..6c4c430e --- /dev/null +++ b/apps/cli/src/compose/passkey.ts @@ -0,0 +1,34 @@ +import { Config, Service } from "../types/compose.js"; +import { DEFAULT_HEALTHCHECK } from "./common.js"; + +// Passkey Server service +export const PASSKEY_SVC: Service = { + image: "cartesi/sdk:latest", + command: ["passkey-server"], + healthcheck: { + ...DEFAULT_HEALTHCHECK, + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000/health"], + }, +}; + +// Passkey Proxy configuration +export const PASSKEY_PROXY_CFG: Config = { + name: "passkey-proxy", + content: `http: + routers: + passkey-server: + rule: "PathPrefix(\`/passkey\`)" + middlewares: + - "remove-passkey-server-prefix" + service: passkey-server + middlewares: + remove-passkey-server-prefix: + replacePathRegex: + regex: "^/passkey/(.*)" + replacement: "/$1" + services: + passkey-server: + loadBalancer: + servers: + - url: "http://passkey-server:3000"`, +}; diff --git a/apps/cli/src/compose/paymaster.ts b/apps/cli/src/compose/paymaster.ts new file mode 100644 index 00000000..8fbda767 --- /dev/null +++ b/apps/cli/src/compose/paymaster.ts @@ -0,0 +1,38 @@ +import { Config, Service } from "../types/compose.js"; +import { DEFAULT_HEALTHCHECK } from "./common.js"; + +// Paymaster service +export const PAYMASTER_SVC: Service = { + image: "cartesi/sdk:latest", + command: "mock-verifying-paymaster", + environment: { + ALTO_RPC: "http://bundler:4337", + ANVIL_RPC: "http://anvil:8545", + }, + healthcheck: { + ...DEFAULT_HEALTHCHECK, + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000/ping"], + }, +}; + +// Paymaster Proxy configuration +export const PAYMASTER_PROXY_CFG: Config = { + name: "paymaster-proxy", + content: `http: + routers: + paymaster: + rule: "PathPrefix(\`/paymaster\`)" + middlewares: + - "remove-paymaster-prefix" + service: paymaster + middlewares: + remove-paymaster-prefix: + replacePathRegex: + regex: "^/paymaster/(.*)" + replacement: "/$1" + services: + paymaster: + loadBalancer: + servers: + - url: "http://paymaster:3000"`, +}; diff --git a/apps/cli/src/compose/proxy.ts b/apps/cli/src/compose/proxy.ts new file mode 100644 index 00000000..4e9668ca --- /dev/null +++ b/apps/cli/src/compose/proxy.ts @@ -0,0 +1,23 @@ +import { Service } from "../types/compose.js"; +import { DEFAULT_HEALTHCHECK } from "./common.js"; + +// Proxy service +export const PROXY_SVC: Service = { + image: "traefik:latest", + healthcheck: { + ...DEFAULT_HEALTHCHECK, + test: ["CMD", "traefik", "healthcheck", "--ping"], + }, + command: [ + "--ping=true", + "--entryPoints.web.address=:8088", + "--entryPoints.traefik.address=:8080", + "--metrics.prometheus=true", + "--metrics.prometheus.addServicesLabels=true", + "--providers.file.directory=/etc/traefik/conf.d", + "--providers.file.watch=true", + "--log", + "--log.level=INFO", + ], + ports: ["6751:8088"], +}; diff --git a/apps/cli/src/compose/rollupsNode.ts b/apps/cli/src/compose/rollupsNode.ts new file mode 100644 index 00000000..38bce7ba --- /dev/null +++ b/apps/cli/src/compose/rollupsNode.ts @@ -0,0 +1,43 @@ +import { Config, Service } from "../types/compose.js"; +import { DEFAULT_ENV, DEFAULT_HEALTHCHECK } from "./common.js"; + +// Rollups Node service +export const ROLLUPS_NODE_SVC: Service = { + image: "cartesi/rollups-runtime:latest", + depends_on: { + database: { condition: "service_healthy" }, + anvil: { condition: "service_healthy" }, + }, + expose: ["10000", "10011", "10012"], + healthcheck: { + ...DEFAULT_HEALTHCHECK, + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:10000/livez"], + }, + command: ["cartesi-rollups-node"], + environment: DEFAULT_ENV, + volumes: ["./.cartesi:/var/lib/cartesi-rollups-node/snapshots:ro"], +}; + +// Rollups Node proxy configuration +export const ROLLUPS_NODE_PROXY_CFG: Config = { + name: "rollups-node-proxy", + content: ` +http: + routers: + inspect_server: + rule: "PathPrefix(\`/inspect\`)" + service: inspect_server + rpc_server: + rule: "PathPrefix(\`/rpc\`)" + service: rpc_server + services: + inspect_server: + loadBalancer: + servers: + - url: "http://rollups-node:10012" + rpc_server: + loadBalancer: + servers: + - url: "http://rollups-node:10011" +`, +}; diff --git a/apps/cli/src/exec/rollups.ts b/apps/cli/src/exec/rollups.ts index fc7b2def..ddd2c373 100644 --- a/apps/cli/src/exec/rollups.ts +++ b/apps/cli/src/exec/rollups.ts @@ -1,7 +1,6 @@ import chalk from "chalk"; import { execa } from "execa"; import { Listr, type ListrTask } from "listr2"; -import path from "node:path"; import pRetry from "p-retry"; import { type Address, @@ -16,7 +15,7 @@ import { getProjectName, getServiceHealth, } from "../base.js"; -import { DEFAULT_SDK_IMAGE } from "../config.js"; +import { ComposeBuilder } from "../compose/builder.js"; export type RollupsDeployment = { name: string; @@ -94,7 +93,6 @@ export const getApplicationAddress = async (options: { type Service = { name: string; // name of the service - file: string; // docker compose file name healthySemaphore?: string; // service to check if the service is healthy healthyTitle?: string | ((port: number, name?: string) => string); // title of the service when it is healthy waitTitle?: string; // title of the service when it is starting @@ -107,7 +105,6 @@ const host = "http://127.0.0.1"; const baseServices: Service[] = [ { name: "anvil", - file: "docker-compose-anvil.yaml", healthySemaphore: "anvil", healthyTitle: (port) => `${chalk.cyan("anvil")} service ready at ${chalk.cyan(`${host}:${port}/anvil`)}`, @@ -116,15 +113,12 @@ const baseServices: Service[] = [ }, { name: "proxy", - file: "docker-compose-proxy.yaml", }, { name: "database", - file: "docker-compose-database.yaml", }, { name: "rpc", - file: "docker-compose-node.yaml", healthySemaphore: "rollups-node", healthyTitle: (port) => `${chalk.cyan("rpc")} service ready at ${chalk.cyan(`${host}:${port}/rpc`)}`, @@ -133,7 +127,6 @@ const baseServices: Service[] = [ }, { name: "inspect", - file: "docker-compose-node.yaml", healthySemaphore: "rollups-node", healthyTitle: (port, name) => `${chalk.cyan("inspect")} service ready at ${chalk.cyan(`${host}:${port}/inspect/${name ?? ""}`)}`, @@ -145,7 +138,6 @@ const baseServices: Service[] = [ const availableServices: Service[] = [ { name: "bundler", - file: "docker-compose-bundler.yaml", healthySemaphore: "bundler", healthyTitle: (port) => `${chalk.cyan("bundler")} service ready at ${chalk.cyan(`${host}:${port}/bundler/rpc`)}`, @@ -154,8 +146,7 @@ const availableServices: Service[] = [ }, { name: "explorer", - file: "docker-compose-explorer.yaml", - healthySemaphore: "explorer_api", + healthySemaphore: "explorer-api", healthyTitle: (port) => `${chalk.cyan("explorer")} service ready at ${chalk.cyan(`${host}:${port}/explorer`)}`, waitTitle: `${chalk.cyan("explorer")} service starting...`, @@ -163,7 +154,6 @@ const availableServices: Service[] = [ }, { name: "paymaster", - file: "docker-compose-paymaster.yaml", healthySemaphore: "paymaster", healthyTitle: (port) => `${chalk.cyan("paymaster")} service ready at ${chalk.cyan(`${host}:${port}/paymaster`)}`, @@ -172,7 +162,6 @@ const availableServices: Service[] = [ }, { name: "passkey", - file: "docker-compose-passkey-server.yaml", healthySemaphore: "passkey-server", healthyTitle: (port) => `${chalk.cyan("passkey")} service ready at ${chalk.cyan(`${host}:${port}/passkey`)}`, @@ -240,59 +229,59 @@ export const startEnvironment = async (options: { const address = `${host}:${port}`; - // path of the tool instalation - const binPath = path.join( - path.dirname(new URL(import.meta.url).pathname), - "..", - ); - // setup the environment variable used in docker compose const env: NodeJS.ProcessEnv = { - CARTESI_BIN_PATH: binPath, - CARTESI_BLOCK_TIME: blockTime.toString(), CARTESI_BLOCKCHAIN_DEFAULT_BLOCK: defaultBlock, CARTESI_LISTEN_PORT: port.toString(), CARTESI_LOG_LEVEL: verbose ? "debug" : "info", - CARTESI_ROLLUPS_NODE_CPUS: cpus?.toString(), - CARTESI_ROLLUPS_NODE_MEMORY: memory?.toString(), - CARTESI_SDK_IMAGE: `${DEFAULT_SDK_IMAGE}:${runtimeVersion}`, - CARTESI_SDK_VERSION: runtimeVersion, }; - // build a list of unique compose files - const composeFiles = [...new Set(baseServices.map(({ file }) => file))]; + const composeFile: ComposeBuilder = new ComposeBuilder() + .withName(projectName) + .withAnvil({ + blockTime, + imageTag: runtimeVersion, + }) + .withDatabase({ + imageTag: runtimeVersion, + }) + .withRollupsNode({ + imageTag: runtimeVersion, + }) + .withProxy({ + imageTag: "v3.3.4", + listenPort: port, + }); - // cpu and memory limits, mostly for testing and debuggingpurposes + // cpu and memory limits, mostly for testing and debugging purposes if (cpus) { - composeFiles.push("docker-compose-node-cpus.yaml"); + composeFile.withRollupsNode({ cpus }); } if (memory) { - composeFiles.push("docker-compose-node-memory.yaml"); + composeFile.withRollupsNode({ memory }); } - // select subset of optional services - const optionalServices = - services.length === 1 && services[0] === "all" - ? availableServices - : availableServices.filter(({ name }) => services.includes(name)); - - // add to compose files list - composeFiles.push(...optionalServices.map(({ file }) => file)); - - // create the "--file " list - const files = composeFiles.flatMap((f) => [ - "--file", - path.join(binPath, "compose", f), - ]); + if (services.includes("explorer")) { + composeFile + .withExplorer({ + imageTag: "1.3.3", + listenPort: port, + }) + .withExplorerApi({ + imageTag: "1.0.0", + }); + } + if (services.includes("bundler")) { + composeFile.withBundler({ imageTag: runtimeVersion }); + } + if (services.includes("paymaster")) { + composeFile.withPaymaster({ imageTag: runtimeVersion }); + } + if (services.includes("passkey")) { + composeFile.withPasskeyServer({ imageTag: runtimeVersion }); + } - const composeArgs = [ - "compose", - ...files, - "--project-directory", - ".", - "--project-name", - projectName, - ]; + const composeArgs = ["compose", "-f", "-", "--project-directory", "."]; // run in detached mode (background) const upArgs = ["--detach"]; @@ -303,22 +292,25 @@ export const startEnvironment = async (options: { const { stdout: config } = await execa( "docker", [...composeArgs, "config", "--format", "yaml"], - { env }, + { env, input: composeFile.build() }, ); return { address, config }; } // pull images first - const pullArgs = ["--policy", "missing"]; - await execa("docker", [...composeArgs, "pull", ...pullArgs], { - env, - stdio: "inherit", - }); + // const pullArgs = ["--policy", "missing"]; + // await execa("docker", [...composeArgs, "pull", ...pullArgs], { + // env, + ////FIXME: stdio and input won't work together + // stdio: "inherit", + // input: composeFile.build() + // }); // run compose await execa("docker", [...composeArgs, "up", ...upArgs], { env, + input: composeFile.build(), }); return { address }; diff --git a/apps/cli/src/types/compose.ts b/apps/cli/src/types/compose.ts new file mode 100644 index 00000000..5dbfbb03 --- /dev/null +++ b/apps/cli/src/types/compose.ts @@ -0,0 +1,800 @@ +/** + * Docker Compose File specification + * Based on the official Compose Specification + * https://github.com/compose-spec/compose-go/blob/v2.10.0/schema/compose-spec.json + * https://transform.tools/json-schema-to-typescript + */ +export interface ComposeFile { + /** Define the Compose project name */ + name?: string; + + /** Compose sub-projects to be included */ + include?: Array; + + /** Services that will be used by your application */ + services?: Record; + + /** Networks that are shared among multiple services */ + networks?: Record; + + /** Named volumes that are shared among multiple services */ + volumes?: Record; + + /** Secrets that are shared among multiple services */ + secrets?: Record; + + /** Configurations that are shared among multiple services */ + configs?: Record; + + /** Language models that will be used by your application */ + models?: Record; +} + +export interface Include { + /** Path to the Compose application or sub-project files to include */ + path: string | string[]; + + /** Path to environment files */ + env_file?: string | string[]; + + /** Path to resolve relative paths */ + project_directory?: string; +} + +export interface Service { + /** Specify the image to start the container from */ + image?: string; + + /** Configuration options for building the service's image */ + build?: string | BuildConfig; + + /** Override the default command */ + command?: string | string[] | null; + + /** Override the default entrypoint */ + entrypoint?: string | string[] | null; + + /** Add environment variables */ + environment?: Record | string[]; + + /** Add environment variables from files */ + env_file?: string | string[] | EnvFile[]; + + /** Mount volumes */ + volumes?: Array; + + /** Expose ports without publishing them to the host */ + expose?: Array; + + /** Map ports from container to host */ + ports?: Array; + + /** Express dependency between services */ + depends_on?: string[] | Record; + + /** Restart policy for the container */ + restart?: "no" | "always" | "on-failure" | "unless-stopped" | string; + + /** Grant access to configs */ + configs?: Array; + + /** Grant access to secrets */ + secrets?: Array; + + /** Configure a health check */ + healthcheck?: Healthcheck; + + /** Specify a custom container name */ + container_name?: string; + + /** Custom hostname for the service container */ + hostname?: string; + + /** Add metadata using Docker labels */ + labels?: Record | string[]; + + /** Logging configuration */ + logging?: LoggingConfig; + + /** Network mode */ + network_mode?: string; + + /** Networks to join */ + networks?: string[] | Record; + + /** PID namespace mode */ + pid?: string; + + /** IPC sharing mode */ + ipc?: string; + + /** Deployment configuration */ + deploy?: DeployConfig; + + /** Development configuration */ + develop?: DevelopConfig; + + /** Working directory */ + working_dir?: string; + + /** User to run commands as */ + user?: string; + + /** Add Linux capabilities */ + cap_add?: string[]; + + /** Drop Linux capabilities */ + cap_drop?: string[]; + + /** Device mappings */ + devices?: Array; + + /** DNS servers */ + dns?: string | string[]; + + /** DNS search domains */ + dns_search?: string | string[]; + + /** DNS options */ + dns_opt?: string[]; + + /** Extra hosts */ + extra_hosts?: string[] | Record; + + /** External links */ + external_links?: string[]; + + /** GPU configuration */ + gpus?: string | GpuConfig[]; + + /** Additional groups */ + group_add?: Array; + + /** Memory limit */ + mem_limit?: string | number; + + /** Memory reservation */ + mem_reservation?: string | number; + + /** Memory swap limit */ + memswap_limit?: string | number; + + /** OOM killer disable */ + oom_kill_disable?: boolean | string; + + /** OOM score adjustment */ + oom_score_adj?: number | string; + + /** Privileged mode */ + privileged?: boolean | string; + + /** Read-only root filesystem */ + read_only?: boolean | string; + + /** Shared memory size */ + shm_size?: string | number; + + /** stdin_open */ + stdin_open?: boolean | string; + + /** tty */ + tty?: boolean | string; + + /** Ulimits */ + ulimits?: Record; + + /** Mount volumes from another service */ + volumes_from?: string[]; + + /** CPU configuration */ + cpus?: string | number; + cpuset?: string; + cpu_shares?: string | number; + cpu_quota?: string | number; + cpu_period?: string | number; + cpu_count?: string | number; + cpu_percent?: string | number; + + /** Annotations */ + annotations?: Record | string[]; + + /** Cgroup configuration */ + cgroup?: "host" | "private"; + cgroup_parent?: string; + + /** Domain name */ + domainname?: string; + + /** Init process */ + init?: boolean | string; + + /** Isolation technology */ + isolation?: string; + + /** Links */ + links?: string[]; + + /** Mac address */ + mac_address?: string; + + /** Platform */ + platform?: string; + + /** Security options */ + security_opt?: string[]; + + /** Stop grace period */ + stop_grace_period?: string; + + /** Stop signal */ + stop_signal?: string; + + /** Sysctls */ + sysctls?: Record | string[]; + + /** Tmpfs */ + tmpfs?: string | string[]; + + /** Attach */ + attach?: boolean | string; + + /** Extends another service */ + extends?: string | ExtendsConfig; + + /** External provider */ + provider?: ProviderConfig; + + /** Blkio configuration */ + blkio_config?: BlkioConfig; + + /** Credential spec */ + credential_spec?: CredentialSpec; + + /** Device cgroup rules */ + device_cgroup_rules?: string[]; +} + +export interface BuildConfig { + /** Path to the build context */ + context: string; + + /** Dockerfile name */ + dockerfile?: string; + + /** Inline Dockerfile content */ + dockerfile_inline?: string; + + /** Build arguments */ + args?: Record | string[]; + + /** SSH agent configuration */ + ssh?: Record | string[]; + + /** Labels to apply to the built image */ + labels?: Record | string[]; + + /** Cache sources */ + cache_from?: string[]; + + /** Cache destinations */ + cache_to?: string[]; + + /** Disable build cache */ + no_cache?: boolean | string; + + /** Build stages to not use cache for */ + no_cache_filter?: string | string[]; + + /** Additional build contexts */ + additional_contexts?: Record | string[]; + + /** Network mode for build */ + network?: string; + + /** Pull policy */ + pull?: boolean | string; + + /** Target build stage */ + target?: string; + + /** Shared memory size */ + shm_size?: string | number; + + /** Extra hosts */ + extra_hosts?: string[] | Record; + + /** Isolation technology */ + isolation?: string; + + /** Privileged mode */ + privileged?: boolean | string; + + /** Build secrets */ + secrets?: Array; + + /** Image tags */ + tags?: string[]; + + /** Ulimits */ + ulimits?: Record; + + /** Target platforms */ + platforms?: string[]; + + /** Provenance attestation */ + provenance?: string | boolean; + + /** SBOM attestation */ + sbom?: string | boolean; + + /** Entitlements */ + entitlements?: string[]; +} + +export interface Healthcheck { + /** Disable healthcheck */ + disable?: boolean | string; + + /** Health check test command */ + test?: string | string[]; + + /** Time between checks */ + interval?: string; + + /** Timeout for a single check */ + timeout?: string; + + /** Number of consecutive failures */ + retries?: number | string; + + /** Start period before checks count */ + start_period?: string; + + /** Interval during start period */ + start_interval?: string; +} + +export interface DependsOnConfig { + /** Restart dependent services */ + restart?: boolean | string; + + /** Whether dependency is required */ + required?: boolean; + + /** Condition to wait for */ + condition: + | "service_started" + | "service_healthy" + | "service_completed_successfully"; +} + +export interface ConfigReference { + /** Config name */ + source: string; + + /** Target path in container */ + target?: string; + + /** File UID */ + uid?: string; + + /** File GID */ + gid?: string; + + /** File mode */ + mode?: number | string; +} + +export interface SecretReference { + /** Secret name */ + source: string; + + /** Target path in container */ + target?: string; + + /** File UID */ + uid?: string; + + /** File GID */ + gid?: string; + + /** File mode */ + mode?: number | string; +} + +export interface VolumeMount { + /** Mount type */ + type: "bind" | "volume" | "tmpfs" | "cluster" | "npipe" | "image"; + + /** Source path or volume name */ + source?: string; + + /** Target path in container */ + target: string; + + /** Read-only flag */ + read_only?: boolean | string; + + /** Consistency requirements */ + consistency?: string; + + /** Bind-specific options */ + bind?: { + propagation?: string; + create_host_path?: boolean | string; + recursive?: "enabled" | "disabled" | "writable" | "readonly"; + selinux?: "z" | "Z"; + }; + + /** Volume-specific options */ + volume?: { + labels?: Record | string[]; + nocopy?: boolean | string; + subpath?: string; + }; + + /** Tmpfs-specific options */ + tmpfs?: { + size?: number | string; + mode?: number | string; + }; + + /** Image-specific options */ + image?: { + subpath?: string; + }; +} + +export interface PortMapping { + /** Target port in container */ + target: number; + + /** Published port on host */ + published?: string | number; + + /** Protocol */ + protocol?: "tcp" | "udp"; + + /** Host IP */ + host_ip?: string; + + /** Port mode */ + mode?: "host" | "ingress"; +} + +export interface EnvFile { + /** Path to env file */ + path: string; + + /** Required flag */ + required?: boolean; + + /** Format of env file */ + format?: "env" | "dotenv"; +} + +export interface LoggingConfig { + /** Logging driver */ + driver?: string; + + /** Driver-specific options */ + options?: Record; +} + +export interface NetworkConfig { + /** Aliases for the service on the network */ + aliases?: string[]; + + /** IPv4 address */ + ipv4_address?: string; + + /** IPv6 address */ + ipv6_address?: string; + + /** Link-local IPs */ + link_local_ips?: string[]; + + /** MAC address */ + mac_address?: string; + + /** Driver options */ + driver_opts?: Record; + + /** Priority */ + priority?: number | string; +} + +export interface DeployConfig { + /** Number of replicas */ + replicas?: number | string; + + /** Update configuration */ + update_config?: UpdateConfig; + + /** Rollback configuration */ + rollback_config?: RollbackConfig; + + /** Resource limits and reservations */ + resources?: ResourcesConfig; + + /** Restart policy */ + restart_policy?: RestartPolicyConfig; + + /** Placement constraints */ + placement?: PlacementConfig; + + /** Endpoint mode */ + endpoint_mode?: string; + + /** Labels */ + labels?: Record | string[]; +} + +export interface UpdateConfig { + parallelism?: number | string; + delay?: string; + failure_action?: "continue" | "pause" | "rollback"; + monitor?: string; + max_failure_ratio?: number | string; + order?: "start-first" | "stop-first"; +} + +export interface RollbackConfig { + parallelism?: number | string; + delay?: string; + failure_action?: "continue" | "pause"; + monitor?: string; + max_failure_ratio?: number | string; + order?: "start-first" | "stop-first"; +} + +export interface ResourcesConfig { + limits?: { + cpus?: string | number; + memory?: string; + pids?: number | string; + }; + reservations?: { + cpus?: string | number; + memory?: string; + generic_resources?: GenericResource[]; + devices?: DeviceRequest[]; + }; +} + +export interface GenericResource { + discrete_resource_spec?: { + kind?: string; + value?: number | string; + }; +} + +export interface DeviceRequest { + capabilities: string[]; + count?: string | number; + device_ids?: string[]; + driver?: string; + options?: Record | string[]; +} + +export interface RestartPolicyConfig { + condition?: "none" | "on-failure" | "any"; + delay?: string; + max_attempts?: number | string; + window?: string; +} + +export interface PlacementConfig { + constraints?: string[]; + preferences?: Array<{ spread?: string }>; + max_replicas_per_node?: number | string; +} + +export interface DevelopConfig { + watch?: WatchConfig[]; +} + +export interface WatchConfig { + /** Path to watch */ + path: string; + + /** Action to take */ + action: "rebuild" | "sync" | "restart" | "sync+restart" | "sync+exec"; + + /** Target path for sync */ + target?: string; + + /** Patterns to ignore */ + ignore?: string | string[]; + + /** Patterns to include */ + include?: string | string[]; +} + +export interface DeviceMapping { + source: string; + target?: string; + permissions?: string; +} + +export interface GpuConfig { + capabilities?: string[]; + count?: string | number; + device_ids?: string[]; + driver?: string; + options?: Record | string[]; +} + +export interface UlimitConfig { + soft: number; + hard: number; +} + +export interface ExtendsConfig { + /** Service to extend */ + service: string; + + /** File containing the service */ + file?: string; +} + +export interface ProviderConfig { + /** Provider type */ + type: string; + + /** Provider options */ + options?: Record< + string, + string | number | boolean | Array + >; +} + +export interface BlkioConfig { + device_read_bps?: BlkioLimit[]; + device_read_iops?: BlkioLimit[]; + device_write_bps?: BlkioLimit[]; + device_write_iops?: BlkioLimit[]; + weight?: number | string; + weight_device?: BlkioWeight[]; +} + +export interface BlkioLimit { + path: string; + rate: number | string; +} + +export interface BlkioWeight { + path: string; + weight: number | string; +} + +export interface CredentialSpec { + config?: string; + file?: string; + registry?: string; +} + +export interface Network { + /** Custom name for the network */ + name?: string; + + /** Network driver */ + driver?: string; + + /** Driver-specific options */ + driver_opts?: Record; + + /** IPAM configuration */ + ipam?: IpamConfig; + + /** External network */ + external?: boolean | string | { name?: string }; + + /** Internal network */ + internal?: boolean | string; + + /** Enable IPv4 */ + enable_ipv4?: boolean | string; + + /** Enable IPv6 */ + enable_ipv6?: boolean | string; + + /** Attachable */ + attachable?: boolean | string; + + /** Labels */ + labels?: Record | string[]; +} + +export interface IpamConfig { + driver?: string; + config?: IpamPoolConfig[]; + options?: Record; +} + +export interface IpamPoolConfig { + subnet?: string; + ip_range?: string; + gateway?: string; + aux_addresses?: Record; +} + +export interface Volume { + /** Custom name for the volume */ + name?: string; + + /** Volume driver */ + driver?: string; + + /** Driver-specific options */ + driver_opts?: Record; + + /** External volume */ + external?: boolean | string | { name?: string }; + + /** Labels */ + labels?: Record | string[]; +} + +export interface Secret { + /** Custom name for the secret */ + name?: string; + + /** Environment variable name */ + environment?: string; + + /** File path */ + file?: string; + + /** External secret */ + external?: boolean | string | { name?: string }; + + /** Labels */ + labels?: Record | string[]; + + /** Driver */ + driver?: string; + + /** Driver options */ + driver_opts?: Record; + + /** Template driver */ + template_driver?: string; +} + +export interface Config { + /** Custom name for the config */ + name: string; + + /** Inline content */ + content?: string; + + /** Environment variable name */ + environment?: string; + + /** File path */ + file?: string; + + /** External config */ + external?: boolean | string | { name?: string }; + + /** Labels */ + labels?: Record | string[]; + + /** Template driver */ + template_driver?: string; +} + +export interface Model { + /** Custom name for the model */ + name?: string; + + /** Language model to run */ + model: string; + + /** Context size */ + context_size?: number; + + /** Runtime flags */ + runtime_flags?: string[]; +} diff --git a/apps/cli/tests/unit/compose.test.ts b/apps/cli/tests/unit/compose.test.ts new file mode 100644 index 00000000..ad403398 --- /dev/null +++ b/apps/cli/tests/unit/compose.test.ts @@ -0,0 +1,271 @@ +import { describe, expect, it } from "vitest"; +import { ComposeBuilder } from "../../src/compose/builder.js"; +import { Config } from "../../src/types/compose.js"; + +describe("ComposeBuilder", () => { + describe("when using withName()", () => { + it("should set the name correctly", () => { + const builder = new ComposeBuilder().withName("test-name"); + const compose = builder.buildComposeFile(); + expect(compose.name).toEqual("test-name"); + }); + + it("should override the name if called multiple times", () => { + const builder = new ComposeBuilder().withName("second-name"); + const compose = builder.buildComposeFile(); + + expect(compose.name).toEqual("second-name"); + }); + }); + + describe("when using reset()", () => { + it("should reset the builder state", () => { + const builder = new ComposeBuilder().withName("test-name"); + builder.reset(); + const compose = builder.buildComposeFile(); + + expect(compose.name).toBeUndefined(); + }); + }); + + describe("when using withConfig()", () => { + it("should set the config correctly", () => { + const config: Config = { + name: "my-config", + content: "my content\n new line", + }; + const compose = new ComposeBuilder().withConfig(config).buildComposeFile(); + + expect(compose.configs!["my-config"].content).toContain("content"); + }); + }); + + describe("when using withAnvil()", () => { + it("should have the default image with empty options", () => { + const compose = new ComposeBuilder().withAnvil().buildComposeFile(); + expect(compose.services!["anvil"].image).toEqual("cartesi/sdk:latest"); + }); + + it("should set the block time correctly", () => { + const compose = new ComposeBuilder().withAnvil({blockTime: 7}).buildComposeFile(); + expect(compose.services!["anvil"].command).toContain( + "--block-time=7", + ); + }); + + it("should have the defined image tag", () => { + const composeWithTag = new ComposeBuilder().withAnvil({imageTag: "v1.0.0"}).buildComposeFile(); + + expect(composeWithTag.services!["anvil"].image).toEqual( + "cartesi/sdk:v1.0.0", + ); + }); + + it("should define a config named anvil-proxy", () => { + const compose = new ComposeBuilder().withAnvil().buildComposeFile(); + expect(compose.configs!["anvil-proxy"].content).toBeDefined(); + }); + }); + + describe("when using withProxy()", () => { + it("should have traefik:latest by default", () => { + const compose = new ComposeBuilder().withProxy().buildComposeFile(); + expect(compose.services!["proxy"].image).toBe("traefik:latest"); + }); + + it("should have the defined image tag", () => { + const composeWithTag = new ComposeBuilder().withProxy({imageTag: "v2.5.0"}).buildComposeFile(); + + expect(composeWithTag.services!["proxy"].image).toEqual( + "traefik:v2.5.0", + ); + }); + + it("should listen on the defined port", () => { + const compose = new ComposeBuilder().withProxy().buildComposeFile(); + expect(compose.services!["proxy"].ports).toContain("6751:8088"); + }); + + it("should have a single port mapping", () => { + const compose = new ComposeBuilder().withProxy().buildComposeFile(); + expect(compose.services!["proxy"].ports!.length).toBe(1); + }); + + it("should expose the defined listen port", () => { + const compose = new ComposeBuilder().withProxy({listenPort: 9090}).buildComposeFile(); + expect(compose.services!["proxy"].ports).toContain("9090:8088"); + }); + }); + + describe("when using withRollupsNode()", () => { + it("should have the defined image tag", () => { + const compose = new ComposeBuilder().withRollupsNode({imageTag: "v0.5.0"}).buildComposeFile(); + expect(compose.services!["rollups-node"].image).toEqual("cartesi/rollups-runtime:v0.5.0"); + }); + + it("should define a config for proxy", () => { + const compose = new ComposeBuilder().withRollupsNode().buildComposeFile(); + expect(compose.configs!["rollups-node-proxy"].content).toBeDefined(); + }); + + it("should have its dependencies set", () => { + const compose = new ComposeBuilder().withRollupsNode().buildComposeFile(); + expect(compose.services!["anvil"]).toBeDefined(); + expect(compose.services!["database"]).toBeDefined(); + }); + + it("should allow cpu and memory limits to be set", () => { + const compose = new ComposeBuilder() + .withRollupsNode({ + cpus: 1.5, + memory: 512, + }) + .buildComposeFile(); + expect(compose.services!["rollups-node"].deploy!.resources!.limits).toEqual({ + cpus: "1.5", + memory: "512M", + }); + }); + }); + + describe("when using withDatabase()", () => { + it("should have default image with empty options", () => { + const compose = new ComposeBuilder().withDatabase().buildComposeFile(); + expect(compose.services!["database"].image).toEqual("cartesi/rollups-database:latest"); + }); + + it("should have the defined image tag", () => { + const compose = new ComposeBuilder().withDatabase({imageTag: "v2.0.0"}).buildComposeFile(); + expect(compose.services!["database"].image).toEqual("cartesi/rollups-database:v2.0.0"); + }); + }); + + describe("when using withExplorer()", () => { + it("should have the default image with empty options", () => { + const compose = new ComposeBuilder().withExplorer().buildComposeFile(); + expect(compose.services!["explorer"].image).toEqual("cartesi/rollups-explorer:latest"); + }); + + it("should have the defined image tag", () => { + const compose = new ComposeBuilder().withExplorer({imageTag: "v1.2.3"}).buildComposeFile(); + expect(compose.services!["explorer"].image).toEqual("cartesi/rollups-explorer:v1.2.3"); + }); + + it("should have its dependencies set", () => { + const compose = new ComposeBuilder().withExplorer().buildComposeFile(); + expect(compose.services!["database"]).toBeDefined(); + }); + + it("should be configured to connect to exposed proxy port", () => { + const compose = new ComposeBuilder().withExplorer({listenPort: 8081}).withProxy({listenPort: 8081}).buildComposeFile(); + expect((compose.services!["explorer"].environment as Record)!["NODE_RPC_URL"]).toContain("8081"); + expect((compose.services!["explorer"].environment as Record)!["EXPLORER_API_URL"]).toContain("8081"); + }); + + it("should persist custom changes after the second empty call", () => { + const compose = new ComposeBuilder() + .withExplorer({imageTag: "v3.3.3", listenPort: 8082}) + .withExplorer() + .buildComposeFile(); + expect(compose.services!["explorer"].image).toEqual("cartesi/rollups-explorer:v3.3.3"); + expect((compose.services!["explorer"].environment as Record)!["NODE_RPC_URL"]).toContain("8082"); + expect((compose.services!["explorer"].environment as Record)!["EXPLORER_API_URL"]).toContain("8082"); + }); + + it("should define a config named explorer-proxy", () => { + const compose = new ComposeBuilder().withExplorer().buildComposeFile(); + expect(compose.configs!["explorer-proxy"].content).toBeDefined(); + }); + }); + + describe("when using withExplorerApi()", () => { + it("should have the default image with empty options", () => { + const compose = new ComposeBuilder().withExplorerApi().buildComposeFile(); + expect(compose.services!["explorer-api"].image).toEqual("cartesi/rollups-explorer-api:latest"); + }); + + it("should have the defined image tag", () => { + const compose = new ComposeBuilder().withExplorerApi({imageTag: "v0.9.0"}).buildComposeFile(); + expect(compose.services!["explorer-api"].image).toEqual("cartesi/rollups-explorer-api:v0.9.0"); + }); + + it("should have its dependencies set", () => { + const compose = new ComposeBuilder().withExplorerApi().buildComposeFile(); + expect(compose.services!["database"]).toBeDefined(); + expect(compose.services!["squid-processor"]).toBeDefined(); + }); + + it("should define a config named explorer-api-proxy", () => { + const compose = new ComposeBuilder().withExplorerApi().buildComposeFile(); + expect(compose.configs!["explorer-api-proxy"].content).toBeDefined(); + }); + }); + + describe("when using withSquidProcessor()", () => { + it("should have the default image with empty options", () => { + const compose = new ComposeBuilder().withSquidProcessor().buildComposeFile(); + expect(compose.services!["squid-processor"].image).toEqual("cartesi/rollups-explorer-api:latest"); + }); + + it("should have the defined image tag", () => { + const compose = new ComposeBuilder().withSquidProcessor({imageTag: "v1.1.1"}).buildComposeFile(); + expect(compose.services!["squid-processor"].image).toEqual("cartesi/rollups-explorer-api:v1.1.1"); + }); + + it("should have its dependencies set", () => { + const compose = new ComposeBuilder().withSquidProcessor().buildComposeFile(); + expect(compose.services!["database"]).toBeDefined(); + }); + }); + + describe("when using withBundler()", () => { + it("should have the default image with empty options", () => { + const compose = new ComposeBuilder().withBundler().buildComposeFile(); + expect(compose.services!["bundler"].image).toEqual("cartesi/sdk:latest"); + }); + + it("should have the defined image tag", () => { + const compose = new ComposeBuilder().withBundler({imageTag: "v2.1.0"}).buildComposeFile(); + expect(compose.services!["bundler"].image).toEqual("cartesi/sdk:v2.1.0"); + }); + + it("should define a config named bundler-proxy", () => { + const compose = new ComposeBuilder().withBundler().buildComposeFile(); + expect(compose.configs!["bundler-proxy"].content).toBeDefined(); + }); + }); + + describe("when using withPaymaster()", () => { + it("should have the default image with empty options", () => { + const compose = new ComposeBuilder().withPaymaster().buildComposeFile(); + expect(compose.services!["paymaster"].image).toEqual("cartesi/sdk:latest"); + }); + + it("should have the defined image tag", () => { + const compose = new ComposeBuilder().withPaymaster({imageTag: "v3.0.0"}).buildComposeFile(); + expect(compose.services!["paymaster"].image).toEqual("cartesi/sdk:v3.0.0"); + }); + + it("should define a config named paymaster-proxy", () => { + const compose = new ComposeBuilder().withPaymaster().buildComposeFile(); + expect(compose.configs!["paymaster-proxy"].content).toBeDefined(); + }); + }); + + describe("when using withPasskeyServer()", () => { + it("should have the default image with empty options", () => { + const compose = new ComposeBuilder().withPasskeyServer().buildComposeFile(); + expect(compose.services!["passkey-server"].image).toEqual("cartesi/sdk:latest"); + }); + + it("should have the defined image tag", () => { + const compose = new ComposeBuilder().withPasskeyServer({imageTag: "v4.0.0"}).buildComposeFile(); + expect(compose.services!["passkey-server"].image).toEqual("cartesi/sdk:v4.0.0"); + }); + + it("should define a config named passkey-proxy", () => { + const compose = new ComposeBuilder().withPasskeyServer().buildComposeFile(); + expect(compose.configs!["passkey-proxy"].content).toBeDefined(); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a957e5b..1c7cb2f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: viem: specifier: ^2.37.6 version: 2.37.6(typescript@5.9.2)(zod@3.25.42) + yaml: + specifier: ^2.8.2 + version: 2.8.2 devDependencies: '@biomejs/biome': specifier: 'catalog:' @@ -141,9 +144,6 @@ importers: '@wagmi/cli': specifier: ^2.5.1 version: 2.5.1(typescript@5.9.2) - copyfiles: - specifier: ^2.4.1 - version: 2.4.1 npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -210,7 +210,7 @@ importers: version: link:../tsconfig tsup: specifier: ^8.2.3 - version: 8.3.0(postcss@8.4.47)(typescript@5.5.4) + version: 8.3.0(postcss@8.4.47)(typescript@5.5.4)(yaml@2.8.2) typescript: specifier: 5.5.4 version: 5.5.4 @@ -1654,9 +1654,6 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} - cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -1717,10 +1714,6 @@ packages: cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} - copyfiles@2.4.1: - resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==} - hasBin: true - core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -2065,9 +2058,6 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2091,10 +2081,6 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.3.0: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} @@ -2149,10 +2135,6 @@ packages: engines: {node: 20 || >=22} hasBin: true - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -2263,10 +2245,6 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -2428,9 +2406,6 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} - isarray@0.0.1: - resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -2713,11 +2688,6 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - mnemonist@0.39.6: resolution: {integrity: sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==} @@ -2789,9 +2759,6 @@ packages: node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} - noms@0.0.0: - resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==} - normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -2847,9 +2814,6 @@ packages: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -2922,10 +2886,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - path-key@2.0.1: resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} engines: {node: '>=4'} @@ -3122,9 +3082,6 @@ packages: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} - readable-stream@1.0.34: - resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} - readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -3155,10 +3112,6 @@ packages: resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} engines: {node: '>= 0.4'} - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -3318,6 +3271,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sparse-array@1.3.2: resolution: {integrity: sha512-ZT711fePGn3+kQyLuv1fpd3rNSkNF8vd5Kv2D+qnOANeyKs3fx6bUMGWRPvgTTcYV64QMqZKZwcuaQSP3AZ0tg==} @@ -3392,9 +3346,6 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} - string_decoder@0.10.31: - resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} - string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -3843,9 +3794,6 @@ packages: resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} engines: {node: '>=18'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.17.1: resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} engines: {node: '>=10.0.0'} @@ -3889,24 +3837,21 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} - yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} - yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -5369,12 +5314,6 @@ snapshots: cli-width@4.1.0: {} - cliui@7.0.4: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -5415,16 +5354,6 @@ snapshots: cookiejar@2.1.4: {} - copyfiles@2.4.1: - dependencies: - glob: 7.2.3 - minimatch: 3.1.2 - mkdirp: 1.0.4 - noms: 0.0.0 - through2: 2.0.5 - untildify: 4.0.0 - yargs: 16.2.0 - core-util-is@1.0.3: {} create-require@1.1.1: {} @@ -5916,8 +5845,6 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true @@ -5936,8 +5863,6 @@ snapshots: gensync@1.0.0-beta.2: {} - get-caller-file@2.0.5: {} - get-east-asian-width@1.3.0: {} get-intrinsic@1.2.4: @@ -6021,15 +5946,6 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 2.0.0 - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - globals@11.12.0: {} globalthis@1.0.4: @@ -6122,11 +6038,6 @@ snapshots: indent-string@4.0.0: {} - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - inherits@2.0.4: {} interface-ipld-format@1.0.1: @@ -6289,8 +6200,6 @@ snapshots: is-windows@1.0.2: {} - isarray@0.0.1: {} - isarray@1.0.0: {} isarray@2.0.5: {} @@ -6562,8 +6471,6 @@ snapshots: minipass@7.1.2: {} - mkdirp@1.0.4: {} - mnemonist@0.39.6: dependencies: obliterator: 2.0.4 @@ -6626,11 +6533,6 @@ snapshots: node-releases@2.0.18: {} - noms@0.0.0: - dependencies: - inherits: 2.0.4 - readable-stream: 1.0.34 - normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -6697,10 +6599,6 @@ snapshots: on-exit-leak-free@2.1.2: {} - once@1.4.0: - dependencies: - wrappy: 1.0.2 - onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -6780,8 +6678,6 @@ snapshots: path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} - path-key@2.0.1: {} path-key@3.1.1: {} @@ -6860,11 +6756,12 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-load-config@6.0.1(postcss@8.4.47): + postcss-load-config@6.0.1(postcss@8.4.47)(yaml@2.8.2): dependencies: lilconfig: 3.1.2 optionalDependencies: postcss: 8.4.47 + yaml: 2.8.2 postcss@8.4.47: dependencies: @@ -6967,13 +6864,6 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 - readable-stream@1.0.34: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 0.0.1 - string_decoder: 0.10.31 - readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -7012,8 +6902,6 @@ snapshots: es-errors: 1.3.0 set-function-name: 2.0.2 - require-directory@2.1.1: {} - require-from-string@2.0.2: {} resolve-from@5.0.0: {} @@ -7263,8 +7151,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 - string_decoder@0.10.31: {} - string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -7434,7 +7320,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.3.0(postcss@8.4.47)(typescript@5.5.4): + tsup@8.3.0(postcss@8.4.47)(typescript@5.5.4)(yaml@2.8.2): dependencies: bundle-require: 5.0.0(esbuild@0.23.1) cac: 6.7.14 @@ -7445,7 +7331,7 @@ snapshots: execa: 5.1.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.4.47) + postcss-load-config: 6.0.1(postcss@8.4.47)(yaml@2.8.2) resolve-from: 5.0.0 rollup: 4.24.0 source-map: 0.8.0-beta.0 @@ -7770,8 +7656,6 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.1.0 - wrappy@1.0.2: {} - ws@8.17.1: {} ws@8.18.3: {} @@ -7784,23 +7668,13 @@ snapshots: xtend@4.0.2: {} - y18n@5.0.8: {} - yallist@3.1.1: {} yallist@4.0.0: {} - yargs-parser@20.2.9: {} + yaml@2.8.2: {} - yargs@16.2.0: - dependencies: - cliui: 7.0.4 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.9 + yargs-parser@20.2.9: {} yn@3.1.1: {}