diff --git a/.changeset/brave-foxes-climb.md b/.changeset/brave-foxes-climb.md new file mode 100644 index 00000000..426b5fe4 --- /dev/null +++ b/.changeset/brave-foxes-climb.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Pass `--claim-staging-period` to the node for every consensus type. It was dropped when deploying with `--prt`, so a value given alongside that flag was silently ignored. diff --git a/.changeset/busy-bags-sing.md b/.changeset/busy-bags-sing.md new file mode 100644 index 00000000..68e37cb7 --- /dev/null +++ b/.changeset/busy-bags-sing.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Add `TestUsdc` to the devnet address book, and `RefundOutputBuilder` and `UsdWithdrawalOutputBuilderFactory` to the common one. diff --git a/.changeset/busy-olives-press.md b/.changeset/busy-olives-press.md new file mode 100644 index 00000000..32c50fdf --- /dev/null +++ b/.changeset/busy-olives-press.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Read the machine snapshot hash with the project's SDK image. Without a local `cartesi-machine-stored-hash`, `cartesi run`, `cartesi hash` and the commands that look up the deployed application read the snapshot in the default SDK image, whose emulator may not load a snapshot built by the `sdk` configured in `cartesi.toml`. diff --git a/.changeset/eleven-actors-spend.md b/.changeset/eleven-actors-spend.md new file mode 100644 index 00000000..fa1cc2ca --- /dev/null +++ b/.changeset/eleven-actors-spend.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Fix `cartesi deposit` for ERC-20, ERC-721 and ERC-1155 against the renamed portal functions. diff --git a/.changeset/great-doors-sing.md b/.changeset/great-doors-sing.md new file mode 100644 index 00000000..49aaada7 --- /dev/null +++ b/.changeset/great-doors-sing.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Warn in `cartesi run` when the application stops being healthy, reporting the node's reason instead of letting it go quiet while inputs keep being accepted on-chain. diff --git a/.changeset/lemon-geese-wander.md b/.changeset/lemon-geese-wander.md new file mode 100644 index 00000000..46927b6b --- /dev/null +++ b/.changeset/lemon-geese-wander.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Allow the node's six `CARTESI_AUTH_*` claimer variables to be overridden from the host environment, matching the PRT set. Without `CARTESI_AUTH_KIND` only plain-mnemonic signing was reachable. diff --git a/.changeset/lucky-birds-wave.md b/.changeset/lucky-birds-wave.md new file mode 100644 index 00000000..acb8df19 --- /dev/null +++ b/.changeset/lucky-birds-wave.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Colour the application status in `cartesi status`, distinguishing the recoverable `FAILED` state from the terminal ones, and print the node's reason beneath the table. diff --git a/.changeset/olive-games-tap.md b/.changeset/olive-games-tap.md new file mode 100644 index 00000000..9fbddd04 --- /dev/null +++ b/.changeset/olive-games-tap.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Stop waiting for a service whose container has exited, so a crash loop reports immediately instead of retrying until the timeout. diff --git a/.changeset/olive-moons-repeat.md b/.changeset/olive-moons-repeat.md new file mode 100644 index 00000000..9ec962b5 --- /dev/null +++ b/.changeset/olive-moons-repeat.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Recognize the four terminal application states the node reports (`GUEST_EXCEPTION`, `MACHINE_HALTED`, `MCYCLE_OVERFLOW`, `UNEXPECTED_YIELD`) and surface the node's `reason` diagnostic alongside them. diff --git a/.changeset/quiet-pugs-search.md b/.changeset/quiet-pugs-search.md new file mode 100644 index 00000000..6d9e861b --- /dev/null +++ b/.changeset/quiet-pugs-search.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Bound the wait for services to become healthy. The backoff had no upper limit, so a service that never started held `cartesi run` for up to 19 hours instead of failing. diff --git a/.changeset/rotten-roses-write.md b/.changeset/rotten-roses-write.md new file mode 100644 index 00000000..ba97483d --- /dev/null +++ b/.changeset/rotten-roses-write.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Fix fork validation against rollups-contracts 3.0.0-alpha.10 and dave 3.0.0-alpha.5. diff --git a/.changeset/shiny-hounds-rest.md b/.changeset/shiny-hounds-rest.md new file mode 100644 index 00000000..66d9b576 --- /dev/null +++ b/.changeset/shiny-hounds-rest.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Pass the devnet mnemonic to the node as `CARTESI_PRT_AUTH_MNEMONIC`. The node starts its PRT service on every run and, with claim submission enabled by default, that service requires its own signer, for which PRT has no default. diff --git a/.changeset/thick-experts-notice.md b/.changeset/thick-experts-notice.md new file mode 100644 index 00000000..af22afd0 --- /dev/null +++ b/.changeset/thick-experts-notice.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Generate contracts from the rollups-contracts and dave releases instead of `@cartesi/devnet`. diff --git a/.changeset/tidy-pears-shave.md b/.changeset/tidy-pears-shave.md new file mode 100644 index 00000000..bb1702fa --- /dev/null +++ b/.changeset/tidy-pears-shave.md @@ -0,0 +1,5 @@ +--- +"@cartesi/cli": patch +--- + +Allow the node's six `CARTESI_PRT_AUTH_*` variables to be overridden from the host environment. diff --git a/.github/workflows/cli.yaml b/.github/workflows/cli.yaml index 1898d4f2..07cae489 100644 --- a/.github/workflows/cli.yaml +++ b/.github/workflows/cli.yaml @@ -4,6 +4,7 @@ on: paths: - ".github/workflows/cli.yaml" - "apps/cli/**" + - "packages/sdk/**" - "packages/tsconfig/**" concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} @@ -18,6 +19,12 @@ jobs: env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} + # TEMPORARY: run the integration tests against the SDK image sdk.yaml + # builds for this PR, instead of the released cartesi/sdk version. + CARTESI_TEST_SDK: ghcr.io/cartesi/sdk:pr-${{ github.event.number }} + # TEMPORARY: compose parameterises only the image tag, never the + # cartesi/ prefix, so the PR images are retagged below and picked by tag. + CARTESI_TEST_RUNTIME_VERSION: pr-${{ github.event.number }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -30,17 +37,30 @@ jobs: - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@908c540300062bd5a7e473851cdb4282204cee09 # v1.9.1 - with: - version: v1.4.3 - - name: Install dependencies run: bun install --frozen-lockfile - name: Build run: bun run build --filter @cartesi/cli + # TEMPORARY: needed to pull the PR-tagged SDK image referenced by CARTESI_TEST_SDK. + - name: Login to GitHub Container Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # TEMPORARY: docker compose resolves cartesi/: from Docker Hub, + # which has no PR tags, so the ghcr images are retagged locally. + - name: Retag the PR-built images for docker compose + run: | + for t in sdk rollups-runtime rollups-database; do + docker pull "ghcr.io/cartesi/$t:pr-${{ github.event.number }}" + docker tag "ghcr.io/cartesi/$t:pr-${{ github.event.number }}" \ + "cartesi/$t:pr-${{ github.event.number }}" + done + - name: Test run: bun test apps/cli/ diff --git a/apps/cli/package.json b/apps/cli/package.json index 95ad6649..d209be81 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -42,8 +42,7 @@ "yaml": "^2.8.2" }, "devDependencies": { - "@cartesi/devnet": "2.0.0-alpha.14", - "@sunodo/wagmi-plugin-hardhat-deploy": "^0.4.0", + "@cartesi/wagmi-plugin": "1.0.0-alpha.7", "@types/bun": "^1.3.6", "@types/bytes": "^3.1.5", "@types/fs-extra": "^11.0.4", diff --git a/apps/cli/src/base.ts b/apps/cli/src/base.ts index f567856b..4d805326 100644 --- a/apps/cli/src/base.ts +++ b/apps/cli/src/base.ts @@ -22,11 +22,14 @@ import { erc721PortalAddress, etherPortalAddress, inputBoxAddress, + refundOutputBuilderAddress, selfHostedApplicationFactoryAddress, testFungibleTokenAddress, testMultiTokenAddress, testNonFungibleTokenAddress, + testUsdcAddress, testUsdWithdrawalOutputBuilderAddress, + usdWithdrawalOutputBuilderFactoryAddress, } from "./contracts.js"; import { cartesiMachineStoredHash } from "./exec"; import { getApplicationAddress, getForkConfig } from "./exec/rollups.js"; @@ -37,11 +40,34 @@ export const getContextPath = (...paths: string[]): string => { return path.join(".cartesi", ...paths); }; -export const getMachineHash = async (): Promise => { - // read hash of the cartesi machine snapshot, if one exists +/** + * SDK image of the project, which built its machine snapshot + */ +const getProjectSdk = (): string | undefined => { + try { + return getApplicationConfig(["cartesi.toml"]).sdk; + } catch { + // an invalid config is reported by the commands that build with it + return undefined; + } +}; + +/** + * Read the hash of the cartesi machine snapshot, if one exists. Without a + * local cartesi-machine-stored-hash, it runs in the SDK image the snapshot was + * built with, as an emulator of another version may not load it. + * @param options sdk image of the project, read from cartesi.toml if not given + */ +export const getMachineHash = async (options?: { + sdk?: string; +}): Promise => { const imagePath = getContextPath("image"); if (fs.existsSync(imagePath)) { - return await cartesiMachineStoredHash.computeHash(imagePath); + const image = options?.sdk ?? getProjectSdk(); + return await cartesiMachineStoredHash.computeHash( + imagePath, + image ? { image } : undefined, + ); } return undefined; }; @@ -118,6 +144,7 @@ export const getAddressBook = async (options: { TestToken: testFungibleTokenAddress, TestNFT: testNonFungibleTokenAddress, TestMultiToken: testMultiTokenAddress, + TestUsdc: testUsdcAddress, TestUsdWithdrawalOutputBuilder: testUsdWithdrawalOutputBuilderAddress, }; @@ -132,7 +159,10 @@ export const getAddressBook = async (options: { ERC721Portal: erc721PortalAddress, EtherPortal: etherPortalAddress, InputBox: inputBoxAddress, + RefundOutputBuilder: refundOutputBuilderAddress, SelfHostedApplicationFactory: selfHostedApplicationFactoryAddress, + UsdWithdrawalOutputBuilderFactory: + usdWithdrawalOutputBuilderFactoryAddress, }; // gather all contracts, depending whether is fork or devnet @@ -166,6 +196,9 @@ export const getServiceInfo = async (options: { "--project-name", projectName, "ps", + // --all so an exited container is still reported, instead of the + // service simply vanishing from the output + "--all", service, "--format", "json", diff --git a/apps/cli/src/commands/deposit/erc1155.ts b/apps/cli/src/commands/deposit/erc1155.ts index eefb4542..a57287bb 100644 --- a/apps/cli/src/commands/deposit/erc1155.ts +++ b/apps/cli/src/commands/deposit/erc1155.ts @@ -146,7 +146,7 @@ export const createErc1155SingleCommand = () => { abi: erc1155SinglePortalAbi, account, address: erc1155SinglePortalAddress, - functionName: "depositSingleERC1155Token", + functionName: "depositSingleErc1155Token", args: [ token.address, application, @@ -311,7 +311,7 @@ export const createErc1155BatchCommand = () => { abi: erc1155BatchPortalAbi, account, address: erc1155BatchPortalAddress, - functionName: "depositBatchERC1155Token", + functionName: "depositBatchErc1155Token", args: [ token.address, application, diff --git a/apps/cli/src/commands/deposit/erc20.ts b/apps/cli/src/commands/deposit/erc20.ts index 6ced70d4..70b775ed 100755 --- a/apps/cli/src/commands/deposit/erc20.ts +++ b/apps/cli/src/commands/deposit/erc20.ts @@ -164,7 +164,7 @@ export const createErc20Command = () => { abi: erc20PortalAbi, account, address: erc20PortalAddress, - functionName: "depositERC20Tokens", + functionName: "depositErc20Tokens", args: [token.address, application, amount, execLayerData], }); diff --git a/apps/cli/src/commands/deposit/erc721.ts b/apps/cli/src/commands/deposit/erc721.ts index 53567bb8..896c51e7 100755 --- a/apps/cli/src/commands/deposit/erc721.ts +++ b/apps/cli/src/commands/deposit/erc721.ts @@ -182,7 +182,7 @@ export const createErc721Command = () => { abi: erc721PortalAbi, account, address: erc721PortalAddress, - functionName: "depositERC721Token", + functionName: "depositErc721Token", args: [ token.address, application, diff --git a/apps/cli/src/commands/run.ts b/apps/cli/src/commands/run.ts index 3ff5d45f..bc84b331 100755 --- a/apps/cli/src/commands/run.ts +++ b/apps/cli/src/commands/run.ts @@ -3,7 +3,7 @@ import { type CommandUnknownOpts, Option, } from "@commander-js/extra-typings"; -import { ExitPromptError } from "@inquirer/core"; +import { AbortPromptError, ExitPromptError } from "@inquirer/core"; import chalk from "chalk"; import { ExecaError } from "execa"; import getPort, { portNumbers } from "get-port"; @@ -31,9 +31,11 @@ import { deployApplication, host, removeApplication, + getDeployments, type RollupsDeployment, startEnvironment, stopEnvironment, + TERMINAL_APPLICATION_STATUSES, waitHealthyEnvironment, } from "../exec/rollups.js"; import { keySelect } from "../prompts.js"; @@ -42,6 +44,37 @@ import { assertForkConfig } from "../validations.js"; const commaSeparatedList = (value: string) => value.split(","); +/** How often the interactive shell re-reads the application status. */ +const STATUS_POLL_INTERVAL = 5_000; + +/** + * Report an application that is no longer in the `OK` state. The node stops + * processing inputs on a terminal status while still accepting them on-chain, + * so without this the application goes quiet with no indication why. + */ +const reportApplicationStatus = (deployment: RollupsDeployment) => { + const terminal = TERMINAL_APPLICATION_STATUSES.includes(deployment.status); + const paint = terminal ? chalk.red : chalk.yellow; + + console.warn( + paint( + terminal + ? `application ${deployment.name} is ${deployment.status} and stopped processing inputs` + : `application ${deployment.name} is ${deployment.status}`, + ), + ); + + if (deployment.reason) { + console.warn(paint(`reason: ${deployment.reason}`)); + } + + console.warn( + chalk.yellow( + `run ${chalk.cyan("cartesi logs")} for details, or rebuild to deploy a new application`, + ), + ); +}; + const shell = async (options: { build?: CommandUnknownOpts; deployment?: RollupsDeployment; @@ -50,6 +83,7 @@ const shell = async (options: { projectName: string; prt?: boolean; salt: number; + sdk: string; withdrawalConfig?: WithdrawalConfig; claimStagingPeriod: number; }) => { @@ -59,14 +93,53 @@ const shell = async (options: { log, projectName, prt, + sdk, withdrawalConfig, claimStagingPeriod, } = options; let lastDeployment = options.deployment; let salt = options.salt; + let lastStatus = lastDeployment?.status; + let pendingReport: RollupsDeployment | undefined; + + /** + * Re-read the deployed application and remember it when its status changed. + * The prompt owns the terminal while it is open, so nothing is printed from + * here -- the caller aborts the prompt and reports once it has closed. + */ + const pollStatus = async (abort: () => void) => { + const address = lastDeployment?.address; + if (!address) { + return; + } + + const deployments = await getDeployments({ projectName }); + const deployment = deployments.find((d) => d.address === address); + if (!deployment || deployment.status === lastStatus) { + return; + } + + lastStatus = deployment.status; + if (deployment.status !== "OK") { + pendingReport = deployment; + abort(); + } + }; while (true) { + if (pendingReport) { + reportApplicationStatus(pendingReport); + pendingReport = undefined; + } + + const controller = new AbortController(); + const watcher = setInterval(() => { + pollStatus(() => controller.abort()).catch(() => { + // a transient docker failure should not interrupt the session + }); + }, STATUS_POLL_INTERVAL); + try { const option = await keySelect( { @@ -76,7 +149,7 @@ const shell = async (options: { { name: "Quit", value: "q" }, ] as const, }, - {}, + { signal: controller.signal }, ); switch (option) { case "l": { @@ -103,7 +176,7 @@ const shell = async (options: { await build?.parseAsync([], { from: "user" }); // redeploy - const hash = await getMachineHash(); + const hash = await getMachineHash({ sdk }); if (hash) { if (lastDeployment) { await undeploy({ projectName }); @@ -118,6 +191,8 @@ const shell = async (options: { withdrawalConfig, claimStagingPeriod, }); + lastStatus = lastDeployment?.status; + pendingReport = undefined; } break; @@ -127,11 +202,17 @@ const shell = async (options: { } } } catch (error: unknown) { + if (error instanceof AbortPromptError) { + // the status changed while the prompt was open; report and re-prompt + continue; + } if (error instanceof ExitPromptError) { // gracefully exit return; } throw error; + } finally { + clearInterval(watcher); } } }; @@ -281,7 +362,7 @@ export const createRunCommand = () => { .addOption( new Option( "--claim-staging-period ", - "claim staging period (in blocks). Number of blocks between a claim being submitted and accepted (Authority/Quorum Only)", + "claim staging period (in blocks). Number of blocks between a claim being submitted and accepted", ) .argParser(Number) .default(0), @@ -414,7 +495,9 @@ export const createRunCommand = () => { // deploy the application let deployment: RollupsDeployment | undefined; let salt = 0; - const hash = await getMachineHash(); + const hash = await getMachineHash({ + sdk: applicationConfig.sdk, + }); if (hash) { deployment = await deploy({ epochLength, @@ -465,6 +548,7 @@ export const createRunCommand = () => { projectName, prt, salt, + sdk: applicationConfig.sdk, claimStagingPeriod, withdrawalConfig: applicationConfig?.withdrawalConfig, }); diff --git a/apps/cli/src/commands/status.ts b/apps/cli/src/commands/status.ts index 5ea37ee5..1deb6dbb 100644 --- a/apps/cli/src/commands/status.ts +++ b/apps/cli/src/commands/status.ts @@ -2,7 +2,25 @@ import { Command } from "@commander-js/extra-typings"; import chalk from "chalk"; import Table from "cli-table3"; import { getProjectName, getServiceState } from "../base.js"; -import { getDeployments } from "../exec/rollups.js"; +import { + type ApplicationStatus, + getDeployments, + type RollupsDeployment, + TERMINAL_APPLICATION_STATUSES, +} from "../exec/rollups.js"; + +/** + * `OK` is healthy and `FAILED` is recoverable; the remaining statuses are + * terminal and the node has stopped processing inputs for the application. + */ +const formatStatus = (status: ApplicationStatus) => { + if (status === "OK") { + return chalk.green(status); + } + return TERMINAL_APPLICATION_STATUSES.includes(status) + ? chalk.red(status) + : chalk.yellow(status); +}; export const createStatusCommand = () => { return new Command("status") @@ -51,13 +69,34 @@ export const createStatusCommand = () => { ...deployments.map((deployment) => [ deployment.templateHash, deployment.address, - deployment.status, + formatStatus(deployment.status), deployment.enabled ? chalk.green("yes") : chalk.red("no"), ]), ); console.log(table.toString()); + + const unhealthy = deployments.filter( + (deployment: RollupsDeployment) => + deployment.status !== "OK", + ); + + for (const deployment of unhealthy) { + if (deployment.reason) { + console.log( + `${chalk.cyan(deployment.name)}: ${deployment.reason}`, + ); + } + } + + if (unhealthy.length > 0) { + console.log( + chalk.yellow( + `run ${chalk.cyan("cartesi logs")} for details`, + ), + ); + } } } } diff --git a/apps/cli/src/compose/node.ts b/apps/cli/src/compose/node.ts index 91b540ed..2ad22f83 100644 --- a/apps/cli/src/compose/node.ts +++ b/apps/cli/src/compose/node.ts @@ -29,8 +29,12 @@ export type ServiceOptions = { * A number of environment variables are rule out to avoid confusion e.g. CORS, FEATURE Enablement, etc. */ export const nodeAllowedEnvironmentVariables = [ + "CARTESI_AUTH_KIND", "CARTESI_AUTH_MNEMONIC", "CARTESI_AUTH_MNEMONIC_ACCOUNT_INDEX", + "CARTESI_AUTH_MNEMONIC_FILE", + "CARTESI_AUTH_PRIVATE_KEY", + "CARTESI_AUTH_PRIVATE_KEY_FILE", "CARTESI_BLOCKCHAIN_DEFAULT_BLOCK", "CARTESI_BLOCKCHAIN_HTTP_AUTHORIZATION", "CARTESI_BLOCKCHAIN_HTTP_ENDPOINT", @@ -50,6 +54,15 @@ export const nodeAllowedEnvironmentVariables = [ "CARTESI_LOG_LEVEL_PRT", "CARTESI_LOG_LEVEL_VALIDATOR", "CARTESI_JSONRPC_MACHINE_LOG_LEVEL", + // Only the mnemonic and private-key kinds are reachable here: the aws + // kind needs AWS_REGION and the SDK credential chain, which are not + // CARTESI_-prefixed and so never reach getCartesiEnvironmentVariables. + "CARTESI_PRT_AUTH_KIND", + "CARTESI_PRT_AUTH_MNEMONIC", + "CARTESI_PRT_AUTH_MNEMONIC_ACCOUNT_INDEX", + "CARTESI_PRT_AUTH_MNEMONIC_FILE", + "CARTESI_PRT_AUTH_PRIVATE_KEY", + "CARTESI_PRT_AUTH_PRIVATE_KEY_FILE", "CARTESI_SNAPSHOTS_DIR", ] as const; @@ -122,6 +135,10 @@ const service = (options: ServiceOptions): Service => { selfHostedApplicationFactoryAddress, CARTESI_DATABASE_CONNECTION: `postgres://postgres:${databasePassword}@${databaseHost}:${databasePort}/rollupsdb?sslmode=disable`, CARTESI_LOG_LEVEL: logLevel, + // The node always starts its PRT service, and with claim submission + // enabled by default that service needs its own signer. PRT has no + // default mnemonic, so the devnet one is supplied explicitly. + CARTESI_PRT_AUTH_MNEMONIC: mnemonic, CARTESI_SNAPSHOTS_DIR: "/var/lib/cartesi-rollups-node/snapshots", }; diff --git a/apps/cli/src/exec/cartesi-machine-stored-hash.ts b/apps/cli/src/exec/cartesi-machine-stored-hash.ts index 6c360f3f..004affd9 100644 --- a/apps/cli/src/exec/cartesi-machine-stored-hash.ts +++ b/apps/cli/src/exec/cartesi-machine-stored-hash.ts @@ -29,7 +29,10 @@ export const computeHash = async ( ); if (undefined !== stdout) { - const hash = `0x${stdout.toString().trim()}`; + // cartesi-machine-stored-hash prints a bare digest up to emulator + // 0.20 and a 0x-prefixed one from 0.21 on. + const digest = stdout.toString().trim(); + const hash = digest.startsWith("0x") ? digest : `0x${digest}`; if (isHash(hash)) { return hash; diff --git a/apps/cli/src/exec/cartesi-machine.ts b/apps/cli/src/exec/cartesi-machine.ts index 2a27ab0f..bf8da625 100644 --- a/apps/cli/src/exec/cartesi-machine.ts +++ b/apps/cli/src/exec/cartesi-machine.ts @@ -5,7 +5,7 @@ import { type ExecaOptionsDockerFallback, } from "./util.js"; -export const requiredVersion = new Range("^0.20.0"); +export const requiredVersion = new Range("^0.21.0"); export const boot = ( args: readonly string[], diff --git a/apps/cli/src/exec/rollups.ts b/apps/cli/src/exec/rollups.ts index 64392327..662ed12e 100644 --- a/apps/cli/src/exec/rollups.ts +++ b/apps/cli/src/exec/rollups.ts @@ -1,7 +1,7 @@ import chalk from "chalk"; import { execa } from "execa"; import { Listr, type ListrTask } from "listr2"; -import pRetry from "p-retry"; +import pRetry, { AbortError } from "p-retry"; import { type Address, type Hash, @@ -17,7 +17,7 @@ import { getContextPath, getMachineHash, getProjectName, - getServiceHealth, + getServiceInfo, } from "../base.js"; import anvil from "../compose/anvil.js"; import { concat } from "../compose/builder.js"; @@ -31,7 +31,34 @@ import proxy from "../compose/proxy.js"; import type { WithdrawalConfig } from "../config.js"; import type { ForkConfig } from "../types/chain.js"; -type ApplicationStatus = "OK" | "FAILED" | "DIVERGED" | "CORRUPTED"; +/** + * Application status as reported by the node. `OK` is healthy and `FAILED` is a + * recoverable failure; every other value is terminal and the node stops + * processing inputs for that application, including across restarts. + */ +export type ApplicationStatus = + | "OK" + | "FAILED" + | "DIVERGED" + | "CORRUPTED" + | "GUEST_EXCEPTION" + | "MACHINE_HALTED" + | "MCYCLE_OVERFLOW" + | "UNEXPECTED_YIELD" + | "INVALID_OUTPUTS_ROOT"; + +/** + * Statuses in which the node no longer processes inputs for the application. + */ +export const TERMINAL_APPLICATION_STATUSES: readonly ApplicationStatus[] = [ + "DIVERGED", + "CORRUPTED", + "GUEST_EXCEPTION", + "MACHINE_HALTED", + "MCYCLE_OVERFLOW", + "UNEXPECTED_YIELD", + "INVALID_OUTPUTS_ROOT", +]; export type RollupsDeployment = { name: string; @@ -40,6 +67,8 @@ export type RollupsDeployment = { templateHash: Hash; epochLength: number; status: ApplicationStatus; + /** Node-provided diagnostic, present when the status is not `OK`. */ + reason?: string; enabled: boolean; }; @@ -50,6 +79,7 @@ type CliRollupsDeployment = { template_hash: string; epoch_length: string; status: string; + reason?: string | null; enabled: boolean; }; @@ -65,10 +95,18 @@ const parseDeployment = ( epochLength: hexToNumber(deployment.epoch_length as Hex), name: deployment.name, status: deployment.status as ApplicationStatus, + reason: deployment.reason ?? undefined, enabled: deployment.enabled, templateHash: deployment.template_hash as Hex, }); +/** + * Parse the JSON `cartesi-rollups-cli app list` writes to stdout. Throws on + * malformed input; callers decide what that means. + */ +export const parseApplications = (stdout: string): RollupsDeployment[] => + (JSON.parse(stdout) as CliRollupsDeployment[]).map(parseDeployment); + export const getDeployments = async ( options: ComposeParams, ): Promise => { @@ -83,7 +121,7 @@ export const getDeployments = async ( "app", "list", ]); - return JSON.parse(stdout).map(parseDeployment); + return parseApplications(stdout); } catch { return []; } @@ -152,6 +190,13 @@ type Service = { export const host = "http://127.0.0.1"; +/** + * Upper bound for the delay between health checks. Without it the 1.1 growth + * factor reaches a 104 minute sleep by the last of the 100 attempts, so a + * service that never becomes healthy would hold the command for 19 hours. + */ +const SERVICE_HEALTH_MAX_INTERVAL = 5_000; + // services configuration const baseServices: Service[] = [ { @@ -236,15 +281,30 @@ const serviceMonitorTask = (options: { task: async (_ctx, task) => { await pRetry( async () => { - const health = await getServiceHealth(options); - if (health !== "healthy") { + const info = await getServiceInfo(options); + + // An exited container never becomes healthy, so stop + // retrying instead of spending the whole budget on it. + if (info?.State === "exited" || info?.State === "dead") { + throw new AbortError( + errorTitle ?? + `Service ${chalk.cyan(service)} exited`, + ); + } + + if (info?.Health !== "healthy") { throw new Error( errorTitle ?? `Service ${chalk.cyan(service)} is not healthy`, ); } }, - { retries: 100, minTimeout: 500, factor: 1.1 }, + { + retries: 100, + minTimeout: 500, + maxTimeout: SERVICE_HEALTH_MAX_INTERVAL, + factor: 1.1, + }, ); task.title = healthyTitle ?? `Service ${chalk.cyan(service)} is ready`; @@ -482,7 +542,7 @@ export const deployAuthority = async (options: { * @param options * @returns address of the application */ -export const deployApplication = async (options: { +export type DeployApplicationOptions = { consensus?: Address; epochLength: number; name: string; @@ -492,12 +552,19 @@ export const deployApplication = async (options: { snapshotPath: string; withdrawalConfig?: WithdrawalConfig; claimStagingPeriod: number; -}): Promise => { +}; + +/** + * Assemble the `cartesi-rollups-cli deploy application` arguments. + * Kept separate from the call so it can be tested without a node. + */ +export const buildDeployApplicationArgs = ( + options: DeployApplicationOptions, +): string[] => { const { consensus, epochLength, name, - projectName, prt, salt, snapshotPath, @@ -505,7 +572,6 @@ export const deployApplication = async (options: { claimStagingPeriod, } = options; - // app deploy args const deployArgs = [name, snapshotPath]; if (consensus) { @@ -520,14 +586,11 @@ export const deployApplication = async (options: { if (prt) { deployArgs.push("--prt"); - } else { - // Claim staging period (Authority/Quorum only) - deployArgs.push( - "--claim-staging-period", - claimStagingPeriod.toString(), - ); } + // the node takes this on the whole deploy command, PRT included + deployArgs.push("--claim-staging-period", claimStagingPeriod.toString()); + if (withdrawalConfig) { deployArgs.push( "--withdrawal-config", @@ -537,6 +600,15 @@ export const deployApplication = async (options: { deployArgs.push("--json"); + return deployArgs; +}; + +export const deployApplication = async ( + options: DeployApplicationOptions, +): Promise => { + const { projectName } = options; + const deployArgs = buildDeployApplicationArgs(options); + // deploy application const { stdout } = await execa("docker", [ "compose", diff --git a/apps/cli/src/validations.ts b/apps/cli/src/validations.ts index 6495a28c..83770d9a 100644 --- a/apps/cli/src/validations.ts +++ b/apps/cli/src/validations.ts @@ -69,7 +69,7 @@ const getContractCheckers = (options: ContractCheckersOptions) => { return { address: this.getAddress(), abi: etherPortalConfig.abi, - functionName: "getInputBox", + functionName: "version", args: [], blockNumber: options.blockNumber, }; @@ -87,7 +87,7 @@ const getContractCheckers = (options: ContractCheckersOptions) => { return { address: this.getAddress(), abi: erc20PortalConfig.abi, - functionName: "getInputBox", + functionName: "version", args: [], blockNumber: options.blockNumber, }; @@ -105,7 +105,7 @@ const getContractCheckers = (options: ContractCheckersOptions) => { return { address: this.getAddress(), abi: erc721PortalConfig.abi, - functionName: "getInputBox", + functionName: "version", args: [], blockNumber: options.blockNumber, }; @@ -123,7 +123,7 @@ const getContractCheckers = (options: ContractCheckersOptions) => { return { address: this.getAddress(), abi: erc1155SinglePortalConfig.abi, - functionName: "getInputBox", + functionName: "version", args: [], blockNumber: options.blockNumber, }; @@ -141,7 +141,7 @@ const getContractCheckers = (options: ContractCheckersOptions) => { return { address: this.getAddress(), abi: erc1155BatchPortalConfig.abi, - functionName: "getInputBox", + functionName: "version", args: [], blockNumber: options.blockNumber, }; @@ -177,6 +177,9 @@ const getContractCheckers = (options: ContractCheckersOptions) => { functionName: "calculateDaveAppAddress", args: [ zeroHash, + 0n, + zeroAddress, + [], { accountsDriveStartIndex: 0n, guardian: zeroAddress, diff --git a/apps/cli/tests/integration/config.ts b/apps/cli/tests/integration/config.ts index 57ebadce..e936e112 100644 --- a/apps/cli/tests/integration/config.ts +++ b/apps/cli/tests/integration/config.ts @@ -4,7 +4,16 @@ import path from "node:path"; import tmp from "tmp"; import { DEFAULT_SDK_IMAGE, DEFAULT_SDK_VERSION } from "../../src/config.js"; -export const TEST_SDK = `${DEFAULT_SDK_IMAGE}:${DEFAULT_SDK_VERSION}`; +// CARTESI_TEST_SDK lets CI and local runs point the suite at an unreleased +// SDK image, which is how changes are verified against the matching node. +export const TEST_SDK = + process.env.CARTESI_TEST_SDK ?? + `${DEFAULT_SDK_IMAGE}:${DEFAULT_SDK_VERSION}`; + +// compose parameterises only the image tag, so this is a tag and not a full +// reference; CARTESI_TEST_RUNTIME_VERSION selects an unreleased build. +export const TEST_RUNTIME_VERSION = + process.env.CARTESI_TEST_RUNTIME_VERSION ?? DEFAULT_SDK_VERSION; /** * Ensures the required Docker image is available locally. @@ -80,6 +89,14 @@ export async function createTemporaryCartesiApplication(): Promise<{ console.log(`✓ Temporary Cartesi application created at: ${appDir}`); + // Build with the same image the snapshot readers use. Without this the + // build falls back to the released SDK while the readers use TEST_SDK, + // and a snapshot written by one emulator is rejected by the other. + fs.appendFileSync( + path.join(appDir, "cartesi.toml"), + `\nsdk = "${TEST_SDK}"\n`, + ); + // Change directory into the created application process.chdir(appDir); diff --git a/apps/cli/tests/integration/exec/cartesi-machine-stored-hash.test.ts b/apps/cli/tests/integration/exec/cartesi-machine-stored-hash.test.ts index 31686b6d..f9732325 100644 --- a/apps/cli/tests/integration/exec/cartesi-machine-stored-hash.test.ts +++ b/apps/cli/tests/integration/exec/cartesi-machine-stored-hash.test.ts @@ -3,6 +3,7 @@ import { writeFileSync } from "node:fs"; import path from "node:path"; import tmp from "tmp"; import { isHash } from "viem"; +import { getMachineHash } from "../../../src/base"; import { cartesiMachineStoredHash } from "../../../src/exec"; import { createTemporaryCartesiApplication, @@ -10,6 +11,7 @@ import { TEST_SDK, } from "../config"; +let appDir: string; let machineDir: string; let cleanupTempApplication: () => void; @@ -17,6 +19,7 @@ beforeAll( async () => { await setupIntegrationTests(); const result = await createTemporaryCartesiApplication(); + appDir = result.appDir; machineDir = result.machineDir; cleanupTempApplication = result.cleanup; }, @@ -41,6 +44,25 @@ describe("cartesi-machine-stored-hash", () => { expect(isHash(machineHash!)).toBeTrue(); }); + it("should read the project snapshot with the project sdk image", async () => { + // the temporary application pins sdk = TEST_SDK, which may differ from + // the default SDK whose emulator cannot load the snapshot + const expected = await cartesiMachineStoredHash.computeHash(".", { + forceDocker: true, + image: TEST_SDK, + cwd: machineDir, + }); + expect(isHash(expected ?? "")).toBeTrue(); + + const cwd = process.cwd(); + try { + process.chdir(appDir); + expect(await getMachineHash()).toBe(expected); + } finally { + process.chdir(cwd); + } + }); + it("should return undefined for a non-existent machine directory", async () => { const machineDir = path.join("random", ".cartesi", "image"); diff --git a/apps/cli/tests/integration/node-startup.test.ts b/apps/cli/tests/integration/node-startup.test.ts new file mode 100644 index 00000000..0983fbbf --- /dev/null +++ b/apps/cli/tests/integration/node-startup.test.ts @@ -0,0 +1,68 @@ +import { afterAll, beforeAll, describe, expect, it } from "bun:test"; +import { execa } from "execa"; +import getPort, { portNumbers } from "get-port"; +import tmp from "tmp"; +import { + startEnvironment, + stopEnvironment, + waitHealthyEnvironment, +} from "../../src/exec/rollups.js"; +import { TEST_RUNTIME_VERSION } from "./config.js"; + +// The node starts its PRT service on every run and refuses to boot without a +// signer, so a healthy environment is the regression test for the CLI always +// setting CARTESI_PRT_AUTH_MNEMONIC, not only under --prt. No machine snapshot +// is needed: the node serves /livez before any application is deployed. +describe("rollups node startup", () => { + const projectName = `cli-node-startup-${process.pid}`; + let port: number; + let directory: tmp.DirResult; + let originalCwd: string; + + beforeAll(async () => { + originalCwd = process.cwd(); + directory = tmp.dirSync({ unsafeCleanup: true }); + // compose files are written relative to the working directory + process.chdir(directory.name); + port = await getPort({ port: portNumbers(16751, 16761) }); + }); + + afterAll(async () => { + try { + await stopEnvironment({ projectName }); + } finally { + process.chdir(originalCwd); + directory.removeCallback(); + } + }); + + it("should reach a healthy node without prt", async () => { + await startEnvironment({ + blockTime: 1, + defaultBlock: "latest", + detach: true, + dryRun: false, + port, + projectName, + prt: false, + runtimeVersion: TEST_RUNTIME_VERSION, + services: [], + verbose: false, + }); + + // throws when any base service fails to become healthy + await waitHealthyEnvironment({ port, projectName, services: [] }); + + const { stdout } = await execa("docker", [ + "compose", + "--project-name", + projectName, + "logs", + "rollups_node", + ]); + + // the node logs the identity it derived from the PRT mnemonic, which + // proves the signer was used and not merely that the node booted + expect(stdout).toContain("PRT submitter identity"); + }, 300_000); +}); diff --git a/apps/cli/tests/unit/compose/node.test.ts b/apps/cli/tests/unit/compose/node.test.ts index 5c48c2a3..b80a8354 100644 --- a/apps/cli/tests/unit/compose/node.test.ts +++ b/apps/cli/tests/unit/compose/node.test.ts @@ -13,8 +13,12 @@ describe("Compose node service", () => { describe("Node allowed environment variables", () => { it("should match the exact fixed list of allowed variable names", () => { expect(nodeAllowedEnvironmentVariables).toEqual([ + "CARTESI_AUTH_KIND", "CARTESI_AUTH_MNEMONIC", "CARTESI_AUTH_MNEMONIC_ACCOUNT_INDEX", + "CARTESI_AUTH_MNEMONIC_FILE", + "CARTESI_AUTH_PRIVATE_KEY", + "CARTESI_AUTH_PRIVATE_KEY_FILE", "CARTESI_BLOCKCHAIN_DEFAULT_BLOCK", "CARTESI_BLOCKCHAIN_HTTP_AUTHORIZATION", "CARTESI_BLOCKCHAIN_HTTP_ENDPOINT", @@ -34,6 +38,12 @@ describe("Compose node service", () => { "CARTESI_LOG_LEVEL_PRT", "CARTESI_LOG_LEVEL_VALIDATOR", "CARTESI_JSONRPC_MACHINE_LOG_LEVEL", + "CARTESI_PRT_AUTH_KIND", + "CARTESI_PRT_AUTH_MNEMONIC", + "CARTESI_PRT_AUTH_MNEMONIC_ACCOUNT_INDEX", + "CARTESI_PRT_AUTH_MNEMONIC_FILE", + "CARTESI_PRT_AUTH_PRIVATE_KEY", + "CARTESI_PRT_AUTH_PRIVATE_KEY_FILE", "CARTESI_SNAPSHOTS_DIR", ]); }); @@ -56,6 +66,23 @@ describe("Compose node service", () => { expect(result).toEqual({}); }); + it("should pass the claimer auth variables through and drop the kms key id", () => { + const result = getNodeAllowedVariables({ + CARTESI_AUTH_KIND: "private_key", + CARTESI_AUTH_PRIVATE_KEY: "0xkey", + CARTESI_PRT_AUTH_AWS_KMS_KEY_ID: "alias/signer", + }); + + expect(result).toStrictEqual({ + CARTESI_AUTH_KIND: "private_key", + CARTESI_AUTH_PRIVATE_KEY: "0xkey", + }); + // the aws kind cannot be configured through CARTESI_ variables + expect(result).not.toHaveProperty( + "CARTESI_PRT_AUTH_AWS_KMS_KEY_ID", + ); + }); + it("should return only allowed variables from the input", () => { const result = getNodeAllowedVariables({ CARTESI_LOG_LEVEL: "debug", @@ -188,7 +215,7 @@ describe("Compose node service", () => { const env = compose.services?.rollups_node?.environment ?? {}; - expect(Object.keys(env)).toHaveLength(10); + expect(Object.keys(env)).toHaveLength(11); expect(env).toHaveProperty( "CARTESI_AUTH_MNEMONIC", @@ -227,6 +254,50 @@ describe("Compose node service", () => { ); }); + it("should set PRT auth even when not running with prt", () => { + const compose = buildNodeCompose(baseOptions); + + const env = compose.services?.rollups_node?.environment ?? {}; + + // the node starts PRT on every run, so the signer is always needed + expect(env).toHaveProperty( + "CARTESI_PRT_AUTH_MNEMONIC", + "test test test test test test test test test test test junk", + ); + }); + + it("should pass the devnet mnemonic as PRT auth when running with prt", () => { + const compose = buildNodeCompose({ ...baseOptions, prt: true }); + + const env = compose.services?.rollups_node?.environment ?? {}; + + // the node defaults CARTESI_PRT_AUTH_KIND to "mnemonic" but has no + // default mnemonic, so the PRT service cannot start without this + expect(env).toHaveProperty( + "CARTESI_PRT_AUTH_MNEMONIC", + "test test test test test test test test test test test junk", + ); + expect(env).toHaveProperty( + "CARTESI_AUTH_MNEMONIC", + "test test test test test test test test test test test junk", + ); + }); + + it("should use a custom mnemonic for PRT auth when one is given", () => { + const compose = buildNodeCompose({ + ...baseOptions, + prt: true, + mnemonic: "custom mnemonic", + }); + + const env = compose.services?.rollups_node?.environment ?? {}; + + expect(env).toHaveProperty( + "CARTESI_PRT_AUTH_MNEMONIC", + "custom mnemonic", + ); + }); + it("should pass cartesiEnvironmentVariables that are in the allow-list to the service", () => { const compose = buildNodeCompose({ ...baseOptions, diff --git a/apps/cli/tests/unit/exec/rollups.test.ts b/apps/cli/tests/unit/exec/rollups.test.ts new file mode 100644 index 00000000..e9a153af --- /dev/null +++ b/apps/cli/tests/unit/exec/rollups.test.ts @@ -0,0 +1,172 @@ +import { describe, expect, it } from "bun:test"; +import { + buildDeployApplicationArgs, + parseApplications, +} from "../../../src/exec/rollups"; + +/** A single entry as `cartesi-rollups-cli app list` prints it. */ +const appListEntry = (overrides: Record = {}) => ({ + name: "echo", + iapplication_address: "0x1234567890abcdef1234567890abcdef12345678", + iconsensus_address: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", + template_hash: + "0x0000000000000000000000000000000000000000000000000000000000000001", + epoch_length: "0x2d0", + status: "OK", + reason: null, + enabled: true, + ...overrides, +}); + +const stdout = (entries: unknown[]) => JSON.stringify(entries); + +describe("parseApplications", () => { + it("should map the node payload onto a deployment", () => { + const [deployment] = parseApplications(stdout([appListEntry()])); + + expect(deployment).toEqual({ + name: "echo", + address: "0x1234567890abcdef1234567890abcdef12345678", + consensus: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", + templateHash: + "0x0000000000000000000000000000000000000000000000000000000000000001", + epochLength: 720, + status: "OK", + reason: undefined, + enabled: true, + }); + }); + + it("should decode the hex encoded epoch length", () => { + const [deployment] = parseApplications( + stdout([appListEntry({ epoch_length: "0x1" })]), + ); + + expect(deployment.epochLength).toBe(1); + }); + + // The status is cast, never validated, so every value the type claims has + // to survive parsing unchanged. + it.each([ + "OK", + "FAILED", + "DIVERGED", + "CORRUPTED", + "GUEST_EXCEPTION", + "MACHINE_HALTED", + "MCYCLE_OVERFLOW", + "UNEXPECTED_YIELD", + "INVALID_OUTPUTS_ROOT", + ])("should preserve the %s application status", (status) => { + const [deployment] = parseApplications( + stdout([appListEntry({ status })]), + ); + + expect(deployment.status).toBe(status); + }); + + it("should expose the reason reported alongside a terminal status", () => { + const [deployment] = parseApplications( + stdout([ + appListEntry({ + status: "GUEST_EXCEPTION", + reason: "guest exception at input 3", + }), + ]), + ); + + expect(deployment.status).toBe("GUEST_EXCEPTION"); + expect(deployment.reason).toBe("guest exception at input 3"); + }); + + it("should leave the reason undefined when the node reports null", () => { + const [deployment] = parseApplications( + stdout([appListEntry({ reason: null })]), + ); + + expect(deployment.reason).toBeUndefined(); + }); + + it("should leave the reason undefined when the node omits it", () => { + const { reason, ...withoutReason } = appListEntry(); + + const [deployment] = parseApplications(stdout([withoutReason])); + + expect(deployment.reason).toBeUndefined(); + }); + + it("should map every application the node returns", () => { + const deployments = parseApplications( + stdout([ + appListEntry({ name: "first" }), + appListEntry({ name: "second", status: "MACHINE_HALTED" }), + ]), + ); + + expect(deployments).toHaveLength(2); + expect(deployments.map((d) => d.name)).toEqual(["first", "second"]); + expect(deployments[1].status).toBe("MACHINE_HALTED"); + }); + + it("should return no deployments when none are registered", () => { + expect(parseApplications(stdout([]))).toEqual([]); + }); + + // getDeployments wraps this in a try/catch and turns a throw into [], + // which is how an unreachable node surfaces to callers. + it("should throw when the output is not valid json", () => { + expect(() => parseApplications("not json")).toThrow(); + }); +}); + +describe("buildDeployApplicationArgs", () => { + const base = { + claimStagingPeriod: 720, + epochLength: 10, + name: "echo", + projectName: "cartesi", + snapshotPath: ".cartesi/image", + }; + + it("should pass the claim staging period without prt", () => { + const args = buildDeployApplicationArgs(base); + + expect(args).toContain("--claim-staging-period"); + expect(args[args.indexOf("--claim-staging-period") + 1]).toBe("720"); + expect(args).not.toContain("--prt"); + }); + + // the node takes --claim-staging-period on the whole deploy command, so a + // value passed alongside --prt must not be dropped + it("should pass the claim staging period with prt", () => { + const args = buildDeployApplicationArgs({ ...base, prt: true }); + + expect(args).toContain("--prt"); + expect(args).toContain("--claim-staging-period"); + expect(args[args.indexOf("--claim-staging-period") + 1]).toBe("720"); + }); + + it("should use the epoch length when no consensus is given", () => { + const args = buildDeployApplicationArgs(base); + + expect(args).toContain("--epoch-length"); + expect(args).not.toContain("--consensus"); + }); + + it("should use the consensus instead of the epoch length when given", () => { + const args = buildDeployApplicationArgs({ + ...base, + consensus: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", + }); + + expect(args).toContain("--consensus"); + expect(args).not.toContain("--epoch-length"); + }); + + it("should start with the name and snapshot path and end with --json", () => { + const args = buildDeployApplicationArgs(base); + + expect(args.slice(0, 2)).toEqual(["echo", ".cartesi/image"]); + expect(args.at(-1)).toBe("--json"); + }); +}); diff --git a/apps/cli/tests/unit/validations.test.ts b/apps/cli/tests/unit/validations.test.ts index 3634fa20..651207aa 100644 --- a/apps/cli/tests/unit/validations.test.ts +++ b/apps/cli/tests/unit/validations.test.ts @@ -102,36 +102,42 @@ describe("validations", () => { expect(mockReadContract).toHaveBeenCalledWith( expect.objectContaining({ address: inputBoxConfig.address, + functionName: "getDeploymentBlockNumber", }), ); expect(mockReadContract).toHaveBeenCalledWith( expect.objectContaining({ address: etherPortalConfig.address, + functionName: "version", }), ); expect(mockReadContract).toHaveBeenCalledWith( expect.objectContaining({ address: erc20PortalConfig.address, + functionName: "version", }), ); expect(mockReadContract).toHaveBeenCalledWith( expect.objectContaining({ address: erc721PortalConfig.address, + functionName: "version", }), ); expect(mockReadContract).toHaveBeenCalledWith( expect.objectContaining({ address: erc1155SinglePortalConfig.address, + functionName: "version", }), ); expect(mockReadContract).toHaveBeenCalledWith( expect.objectContaining({ address: erc1155BatchPortalConfig.address, + functionName: "version", }), ); @@ -272,6 +278,7 @@ describe("validations", () => { expect(mockReadContract).toHaveBeenCalledWith( expect.objectContaining({ address: daveAppFactoryAddress[sepolia.id], + functionName: "calculateDaveAppAddress", }), ); }); diff --git a/apps/cli/wagmi.config.ts b/apps/cli/wagmi.config.ts index 3c8b493e..004e3f70 100644 --- a/apps/cli/wagmi.config.ts +++ b/apps/cli/wagmi.config.ts @@ -1,11 +1,12 @@ -import hardhatDeploy from "@sunodo/wagmi-plugin-hardhat-deploy"; +import { prtInternals, rollupsContracts } from "@cartesi/wagmi-plugin"; import { defineConfig } from "@wagmi/cli"; export default defineConfig({ out: "src/contracts.ts", plugins: [ - hardhatDeploy({ - directory: "node_modules/@cartesi/devnet/deployments", - }), + // the PRT internals are reached through the interfaces they implement, + // or have nothing callable at all, and wagmi repeats each ABI type per + // contract member, so generating them is pure weight + rollupsContracts({ prt: true, exclude: prtInternals }), ], }); diff --git a/bun.lock b/bun.lock index ce41d597..28fbc09e 100644 --- a/bun.lock +++ b/bun.lock @@ -45,8 +45,7 @@ "yaml": "^2.8.2", }, "devDependencies": { - "@cartesi/devnet": "2.0.0-alpha.14", - "@sunodo/wagmi-plugin-hardhat-deploy": "^0.4.0", + "@cartesi/wagmi-plugin": "1.0.0-alpha.7", "@types/bun": "^1.3.6", "@types/bytes": "^3.1.5", "@types/fs-extra": "^11.0.4", @@ -145,6 +144,8 @@ "@cartesi/sdk": ["@cartesi/sdk@workspace:packages/sdk"], + "@cartesi/wagmi-plugin": ["@cartesi/wagmi-plugin@1.0.0-alpha.7", "", { "dependencies": { "abitype": "^1.3.0", "modern-tar": "^0.8.4" }, "peerDependencies": { "@wagmi/cli": "^2.10.0", "viem": "^2.0.0" } }, "sha512-oVgBBMxuEd/xa5OSxToGjRf7nOA9OyjXLmCyH8V19DjZ+k9GNFFfZSDsk7bbDHtia1Vnq4nl6aNU6dU0K7po3w=="], + "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.0.14", "", { "dependencies": { "@changesets/config": "3.1.2", "@changesets/get-version-range-type": "0.4.0", "@changesets/git": "3.0.4", "@changesets/should-skip-package": "0.1.2", "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3", "detect-indent": "6.1.0", "fs-extra": "7.0.1", "lodash.startcase": "4.4.0", "outdent": "0.5.0", "prettier": "2.8.8", "resolve-from": "5.0.0", "semver": "7.7.2" } }, "sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA=="], "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.9", "", { "dependencies": { "@changesets/errors": "0.2.0", "@changesets/get-dependents-graph": "2.1.3", "@changesets/should-skip-package": "0.1.2", "@changesets/types": "6.1.0", "@manypkg/get-packages": "1.1.3", "semver": "7.7.2" } }, "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ=="], @@ -351,8 +352,6 @@ "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], - "@sunodo/wagmi-plugin-hardhat-deploy": ["@sunodo/wagmi-plugin-hardhat-deploy@0.4.0", "", { "peerDependencies": { "@wagmi/core": ">=2.0.0", "abitype": ">=0.10.0", "typescript": ">=5.0.4", "wagmi": ">=2.0.0" }, "optionalPeers": ["@wagmi/core", "typescript", "wagmi"] }, "sha512-kZvhu6ZGNpIvXbV8uZIwUspyEKhXuP8B0TKsM5Pve1eEvwtGZuNwKgW2BZXLZCwjGIRXBK9g/48p+Iy7RRBAww=="], - "@tsconfig/node10": ["@tsconfig/node10@1.0.12", "", {}, "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ=="], "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="], @@ -389,7 +388,7 @@ "@wagmi/cli": ["@wagmi/cli@2.10.0", "", { "dependencies": { "abitype": "^1.1.1", "bundle-require": "^5.1.0", "cac": "^6.7.14", "change-case": "^5.4.4", "chokidar": "4.0.1", "dedent": "^0.7.0", "dotenv": "^16.3.1", "dotenv-expand": "^10.0.0", "esbuild": "~0.25.4", "escalade": "3.2.0", "fdir": "^6.1.1", "nanospinner": "1.2.2", "pathe": "^1.1.2", "picocolors": "^1.0.0", "picomatch": "^3.0.0", "prettier": "^3.0.3", "viem": "2.x", "zod": "^4.1.11" }, "peerDependencies": { "typescript": ">=5.7.3" }, "optionalPeers": ["typescript"], "bin": { "wagmi": "dist/esm/cli.js" } }, "sha512-2tYt6Bp1q26mWexH+XE6dMpPB5/Gp/3OVtE2SeeJ/gNHKLZmVF/TuoZR75mpJKTpofyvpz/fnuMCkUxzbc/kRw=="], - "abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], + "abitype": ["abitype@1.3.0", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-fk6Te+bojIFrMvMZrnOO+SxCB+RUksTGOzq/60ZRvs1L+BVzvi2bqt9L3W/17ZLdZsyM1FuYf65P5nlmoiH1Bg=="], "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], @@ -809,7 +808,7 @@ "mnemonist": ["mnemonist@0.39.6", "", { "dependencies": { "obliterator": "^2.0.1" } }, "sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA=="], - "modern-tar": ["modern-tar@0.7.3", "", {}, "sha512-4W79zekKGyYU4JXVmB78DOscMFaJth2gGhgfTl2alWE4rNe3nf4N2pqenQ0rEtIewrnD79M687Ouba3YGTLOvg=="], + "modern-tar": ["modern-tar@0.7.7", "", {}, "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ=="], "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], @@ -819,8 +818,6 @@ "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "nanospinner": ["nanospinner@1.2.2", "", { "dependencies": { "picocolors": "^1.1.1" } }, "sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA=="], "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], @@ -907,8 +904,6 @@ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], @@ -1017,8 +1012,6 @@ "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "7.0.6", "signal-exit": "4.1.0" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], @@ -1167,10 +1160,12 @@ "zod-validation-error": ["zod-validation-error@3.5.4", "", { "peerDependencies": { "zod": "^3.24.4" } }, "sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw=="], - "@cartesi/cli/@cartesi/devnet": ["@cartesi/devnet@2.0.0-alpha.14", "", {}, "sha512-BPcFh1NBauZlpoL6HedBL88xvi5D+PweAUlxKSxNEiBCE8sJDx8TaTiwKtnSH27l6r4NuuZTeClgWBOq8e+Taw=="], + "@cartesi/devnet/modern-tar": ["modern-tar@0.7.3", "", {}, "sha512-4W79zekKGyYU4JXVmB78DOscMFaJth2gGhgfTl2alWE4rNe3nf4N2pqenQ0rEtIewrnD79M687Ouba3YGTLOvg=="], "@cartesi/mock-verifying-paymaster/viem": ["viem@2.18.4", "", { "dependencies": { "@adraffy/ens-normalize": "1.10.0", "@noble/curves": "1.4.0", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0", "abitype": "1.0.5", "isows": "1.0.4", "webauthn-p256": "0.0.5", "ws": "8.17.1" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-JGdN+PgBnZMbm7fc9o0SfHvL0CKyfrlhBUtaz27V+PeHO43Kgc9Zd4WyIbM8Brafq4TvVcnriRFW/FVGOzwEJw=="], + "@cartesi/wagmi-plugin/modern-tar": ["modern-tar@0.8.4", "", {}, "sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g=="], + "@changesets/apply-release-plan/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "4.0.0", "universalify": "0.1.2" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], "@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], @@ -1219,6 +1214,8 @@ "@types/through/@types/node": ["@types/node@24.5.1", "", { "dependencies": { "undici-types": "7.12.0" } }, "sha512-/SQdmUP2xa+1rdx7VwB9yPq8PaKej8TD5cQ+XfKDPWWC+VDJU4rvVVagXqKUzhKjtFoNA8rXDJAkCxQPAe00+Q=="], + "@wagmi/cli/abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], + "@wagmi/cli/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], "ajv/fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], @@ -1257,9 +1254,9 @@ "ox/@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.1", "", {}, "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ=="], - "path-type/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], + "ox/abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], - "permissionless/viem": ["viem@2.43.5", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.11.1", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-QuJpuEMEPM3EreN+vX4mVY68Sci0+zDxozYfbh/WfV+SSy/Gthm74PH8XmitXdty1xY54uTCJ+/Gbbd1IiMPSA=="], + "path-type/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], "pino/process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], @@ -1285,6 +1282,8 @@ "tsup/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "viem/abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], + "wait-port/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "wait-port/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], @@ -1371,8 +1370,6 @@ "ora/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "permissionless/viem/ox": ["ox@0.11.1", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-1l1gOLAqg0S0xiN1dH5nkPna8PucrZgrIJOfS49MLNiMevxu07Iz4ZjuJS9N+xifvT+PsZyIptS7WHM8nC+0+A=="], - "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "1.0.3" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "spawndamnit/cross-spawn/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -1453,8 +1450,6 @@ "ora/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "permissionless/viem/ox/@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.1", "", {}, "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ=="], - "spawndamnit/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "wait-port/chalk/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],