Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/shaggy-months-lose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cartesi/cli": patch
---

add anvil fork mode to `cartesi run`
2 changes: 1 addition & 1 deletion apps/cli/biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.1/schema.json",
"root": false,
"extends": "//",
"linter": {
Expand Down
17 changes: 9 additions & 8 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
],
"dependencies": {
"@commander-js/extra-typings": "^14.0.0",
"@inquirer/confirm": "^6.0.4",
"@inquirer/core": "^11.1.1",
"@inquirer/input": "^5.0.4",
"@inquirer/select": "^5.0.4",
"@inquirer/confirm": "^6.0.6",
"@inquirer/core": "^11.1.3",
"@inquirer/input": "^5.0.6",
"@inquirer/select": "^5.0.6",
"@inquirer/type": "^4.0.3",
"bytes": "^3.1.2",
"chalk": "^5.6.2",
Expand All @@ -37,12 +37,13 @@
"semver": "^7.7.4",
"smol-toml": "^1.4.2",
"tmp": "^0.2.5",
"viem": "^2.45.2",
"viem": "^2.46.1",
"yaml": "^2.8.2"
},
"devDependencies": {
"@cartesi/devnet": "2.0.0-alpha.9",
"@cartesi/devnet": "2.0.0-alpha.10",
"@cartesi/rollups": "2.1.1",
"@sunodo/wagmi-plugin-hardhat-deploy": "^0.4.0",
"@types/bun": "^1.3.6",
"@types/bytes": "^3.1.5",
"@types/fs-extra": "^11.0.4",
Expand All @@ -53,9 +54,9 @@
"@types/prompts": "^2.4.9",
"@types/semver": "^7.7.1",
"@types/tmp": "^0.2.6",
"@wagmi/cli": "^2.9.0",
"@wagmi/cli": "^2.10.0",
"npm-run-all": "^4.1.5",
"rimraf": "^6.0.1",
"rimraf": "^6.1.3",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.9.2"
Expand Down
64 changes: 49 additions & 15 deletions apps/cli/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
testNftAddress,
testTokenAddress,
} from "./contracts.js";
import { getApplicationAddress } from "./exec/rollups.js";
import { getApplicationAddress, getForkChainId } from "./exec/rollups.js";
import type { PsResponse } from "./types/docker.js";

export const getContextPath = (...paths: string[]): string => {
Expand Down Expand Up @@ -67,35 +67,69 @@ export type AddressBook = Record<string, Address>;
export const getAddressBook = async (options: {
projectName?: string;
}): Promise<AddressBook> => {
const forkChainId = await getForkChainId(options);
const applicationAddress = await getApplicationAddress(options);

// build rollups contracts address book
const contracts: AddressBook = {
ApplicationFactory: applicationFactoryAddress,
AuthorityFactory: authorityFactoryAddress,
DaveAppFactory: daveAppFactoryAddress,
// this contract has different addresses on each of the supported chains
const chainDaveAppFactoryAddress =
forkChainId !== undefined
? daveAppFactoryAddress[
forkChainId as keyof typeof daveAppFactoryAddress
]
: daveAppFactoryAddress[31337];
if (!chainDaveAppFactoryAddress) {
Copy link
Contributor

@brunomenezes brunomenezes Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tuler, should you do this kind of validation also on compose/anvil.ts and compose/node.ts? As this getAddressBook would only run when a address-book command is issued to the CLI.

The validation is about whether the chain ID of the fork is supported or not, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, added check as well to node.ts

throw new Error(`Unsupported fork chain ${forkChainId}`);
}

// contracts that are present only on live chains, with equal addresses on all of them
const forkContracts: AddressBook = {
EntryPointV06: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
EntryPointV07: "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
ERC1155BatchPortal: erc1155BatchPortalAddress,
ERC1155SinglePortal: erc1155SinglePortalAddress,
ERC20Portal: erc20PortalAddress,
ERC721Portal: erc721PortalAddress,
EtherPortal: etherPortalAddress,
InputBox: inputBoxAddress,
EntryPointV08: "0x4337084d9e255ff0702461cf8895ce9e3b5ff108",
EntryPointV09: "0x433709009B8330FDa32311DF1C2AFA402eD8D009",
LightAccountFactory: "0x00004EC70002a32400f8ae005A26081065620D20",
SelfHostedApplicationFactory: selfHostedApplicationFactoryAddress,
SimpleAccountFactory: "0x9406Cc6185a346906296840746125a0E44976454",
SmartAccountFactory: "0x000000a56Aaca3e9a4C479ea6b6CD0DbcB6634F5",
KernelFactoryV2: "0x5de4839a76cf55d0c90e2061ef4386d962E15ae3",
KernelFactoryV3: "0x6723b44Abeec4E71eBE3232BD5B455805baDD22f",
KernelFactoryV3_1: "0xaac5D4240AF87249B3f71BC8E4A2cae074A3E419",
VerifyingPaymasterV06: "0x28ec0633192d0cBd9E1156CE05D5FdACAcB93947",
VerifyingPaymasterV07: "0xc5c97885C67F7361aBAfD2B95067a5bBdA603608",
};

// contracts that are present only on devnet state
const devnetContracts: AddressBook = {
TestToken: testTokenAddress,
TestNFT: testNftAddress,
TestMultiToken: testMultiTokenAddress,
VerifyingPaymasterV06: "0x28ec0633192d0cBd9E1156CE05D5FdACAcB93947",
VerifyingPaymasterV07: "0xc5c97885C67F7361aBAfD2B95067a5bBdA603608",
};

// contracts that are present on both devnet and live chains
const commonContracts: AddressBook = {
ApplicationFactory: applicationFactoryAddress,
AuthorityFactory: authorityFactoryAddress,
DaveAppFactory: chainDaveAppFactoryAddress,
ERC1155BatchPortal: erc1155BatchPortalAddress,
ERC1155SinglePortal: erc1155SinglePortalAddress,
ERC20Portal: erc20PortalAddress,
ERC721Portal: erc721PortalAddress,
EtherPortal: etherPortalAddress,
InputBox: inputBoxAddress,
SelfHostedApplicationFactory: selfHostedApplicationFactoryAddress,
};

// gather all contracts, depending whether is fork or devnet
const contracts: AddressBook =
forkChainId !== undefined
? {
...commonContracts,
...forkContracts,
}
: {
...commonContracts,
...devnetContracts,
};

if (applicationAddress) {
contracts.Application = applicationAddress;
}
Expand Down
53 changes: 51 additions & 2 deletions apps/cli/src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,32 @@ import chalk from "chalk";
import { ExecaError } from "execa";
import getPort, { portNumbers } from "get-port";
import ora from "ora";
import { type Address, type Hex, numberToHex } from "viem";
import {
type Address,
createPublicClient,
type Hex,
http,
numberToHex,
} from "viem";
import { getMachineHash, getProjectName } from "../base.js";
import { DEFAULT_SDK_VERSION, PREFERRED_PORT } from "../config.js";
import {
AVAILABLE_SERVICES,
type RollupsDeployment,
deployApplication,
removeApplication,
type RollupsDeployment,
startEnvironment,
stopEnvironment,
waitHealthyEnvironment,
} from "../exec/rollups.js";
import { keySelect } from "../prompts.js";

export type ForkConfig = {
blockNumber?: bigint;
chainId: number;
url: string;
};

const commaSeparatedList = (value: string) => value.split(",");

const shell = async (options: {
Expand Down Expand Up @@ -165,6 +177,32 @@ const deploy = async (options: {
return application;
};

const configureFork = async (options: {
forkUrl?: string;
forkBlockNumber?: number;
}): Promise<ForkConfig | undefined> => {
if (!options.forkUrl) {
return undefined;
}

const url = options.forkUrl;

// create a client to upstream so we can query it
const client = createPublicClient({
transport: http(url),
});

// use explicit fork-block-number or query from upstream
const blockNumber = options.forkBlockNumber
? BigInt(options.forkBlockNumber)
: await client.getBlockNumber();

// need to query fork chainId if forkUrl is specified
const chainId = await client.getChainId();

return { blockNumber, chainId, url };
};

export const createRunCommand = () => {
return new Command("run")
.description("Run a local cartesi node for the application.")
Expand Down Expand Up @@ -197,6 +235,13 @@ export const createRunCommand = () => {
.default("latest"),
)
.option("--dry-run", "show the docker compose configuration", false)
.option("--fork-url <url>", "RPC URL to fork from")
.addOption(
new Option(
"--fork-block-number <number>",
"block number to fork from",
).argParser(Number),
)
.addOption(
new Option(
"--memory <number>",
Expand Down Expand Up @@ -265,12 +310,16 @@ export const createRunCommand = () => {
port: portNumbers(PREFERRED_PORT, PREFERRED_PORT + 10),
}));

// configure optional anvil fork
const forkConfig = await configureFork(options);

// run compose environment (detached)
const { address, config } = await startEnvironment({
blockTime,
cpus,
defaultBlock,
dryRun,
forkConfig,
memory,
port,
projectName,
Expand Down
28 changes: 26 additions & 2 deletions apps/cli/src/compose/anvil.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
import type { ForkConfig } from "../commands/run.js";
import type { ComposeFile, Config, Service } from "../types/compose.js";
import { DEFAULT_HEALTHCHECK } from "./common.js";

type ServiceOptions = {
imageTag?: string;
blockTime?: number;
forkConfig?: ForkConfig;
};

// Anvil service
const service = (options?: ServiceOptions): Service => {
const blockTime = options?.blockTime ?? 2;
const imageTag = options?.imageTag ?? "latest";
const forkConfig = options?.forkConfig;

// command for fork and command for load-state local (non-fork)
const command = forkConfig
? [
"anvil",
"--chain-id",
"31337",
"--block-time",
blockTime.toString(),
"--fork-url",
forkConfig.url,
...(forkConfig.blockNumber !== undefined
? ["--fork-block-number", forkConfig.blockNumber.toString()]
: []),
]
: ["devnet", "--block-time", blockTime.toString()];

// in case of forked network service is ready only when it responds with target block number
const test = forkConfig?.blockNumber
? ["CMD", "eth_isready", forkConfig.blockNumber?.toString()]
: ["CMD", "eth_isready"];

return {
image: `cartesi/sdk:${imageTag}`,
command: ["devnet", "--block-time", blockTime.toString()],
command,
healthcheck: {
...DEFAULT_HEALTHCHECK,
test: ["CMD", "eth_isready"],
test,
},
environment: {
ANVIL_IP_ADDR: "0.0.0.0",
Expand Down
13 changes: 11 additions & 2 deletions apps/cli/src/compose/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import type { ComposeFile, Config, Service } from "../types/compose.js";
import { DEFAULT_HEALTHCHECK } from "./common.js";

type ServiceOptions = {
cpus?: number;
databaseHost?: string;
databasePort?: number;
databasePassword: string;
defaultBlock?: "latest" | "safe" | "pending" | "finalized";
cpus?: number;
forkChainId?: number;
logLevel?: "info" | "debug" | "warn" | "error" | "fatal";
memory?: number;
mnemonic?: string;
Expand All @@ -30,6 +31,13 @@ const service = (options: ServiceOptions): Service => {
const mnemonic =
options.mnemonic ??
"test test test test test test test test test test test junk";
const chainId = (options.forkChainId ??
31337) as keyof typeof daveAppFactoryAddress;

const chainDaveAppFactoryAddress = daveAppFactoryAddress[chainId];
if (!chainDaveAppFactoryAddress) {
throw new Error(`Unsupported fork chain ${chainId}`);
}

return {
image: `cartesi/rollups-runtime:${imageTag}`,
Expand Down Expand Up @@ -63,7 +71,8 @@ const service = (options: ServiceOptions): Service => {
CARTESI_BLOCKCHAIN_HTTP_ENDPOINT: "http://anvil:8545",
CARTESI_BLOCKCHAIN_ID: anvil.id.toString(),
CARTESI_BLOCKCHAIN_WS_ENDPOINT: "ws://anvil:8545",
CARTESI_CONTRACTS_DAVE_APP_FACTORY_ADDRESS: daveAppFactoryAddress,
CARTESI_CONTRACTS_DAVE_APP_FACTORY_ADDRESS:
chainDaveAppFactoryAddress,
CARTESI_CONTRACTS_INPUT_BOX_ADDRESS: inputBoxAddress,
CARTESI_CONTRACTS_SELF_HOSTED_APPLICATION_FACTORY_ADDRESS:
selfHostedApplicationFactoryAddress,
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class InvalidStringArrayError extends Error {
*/
const DEFAULT_FORMAT = "ext2";
const DEFAULT_RAM = "128Mi";
export const DEFAULT_SDK_VERSION = "0.12.0-alpha.31";
export const DEFAULT_SDK_VERSION = "0.12.0-alpha.33";
export const DEFAULT_SDK_IMAGE = "cartesi/sdk";
export const PREFERRED_PORT = 6751;

Expand Down
Loading