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`
5 changes: 5 additions & 0 deletions .changeset/tame-cobras-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cartesi/cli": patch
---

changes to run to work better with detached stdin (no shell)
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) {
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
159 changes: 120 additions & 39 deletions apps/cli/src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,48 @@ 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,
host,
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: {
build?: CommandUnknownOpts;
deployment?: RollupsDeployment;
epochLength: number;
log?: CommandUnknownOpts;
projectName: string;
prt?: boolean;
salt: number;
}) => {
const { build, epochLength, log, projectName, prt } = options;

// keep track of last deployment
let lastDeployment: RollupsDeployment | undefined;
let salt = 0;

// deploy for the first time
const hash = getMachineHash();
if (hash) {
lastDeployment = await deploy({
epochLength,
hash,
projectName,
prt,
salt: numberToHex(salt++, { size: 32 }),
});
} else {
console.warn(
chalk.yellow("machine snapshot not found, waiting for build"),
);
}
let lastDeployment = options.deployment;
let salt = options.salt;

while (true) {
try {
Expand Down Expand Up @@ -165,6 +163,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 +221,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 +296,20 @@ export const createRunCommand = () => {
port: portNumbers(PREFERRED_PORT, PREFERRED_PORT + 10),
}));

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

// if TTY is not attached, run on foreground (not detached)
const detach = process.stdin.isTTY;

// run compose environment (detached)
const { address, config } = await startEnvironment({
const { cmd, config } = await startEnvironment({
blockTime,
cpus,
defaultBlock,
detach,
dryRun,
forkConfig,
memory,
port,
projectName,
Expand All @@ -279,6 +318,9 @@ export const createRunCommand = () => {
verbose,
});

// host address
const address = `${host}:${port}`;

if (dryRun && config) {
// just show the docker compose configuration and quit
process.stdout.write(config);
Expand All @@ -297,6 +339,27 @@ export const createRunCommand = () => {
services,
});

// deploy the application
let deployment: RollupsDeployment | undefined;
let salt = 0;
const prt = !authority;
const hash = getMachineHash();
if (hash) {
deployment = await deploy({
epochLength,
hash,
projectName,
prt,
salt: numberToHex(salt++, { size: 32 }),
});
} else {
console.warn(
chalk.yellow(
"machine snapshot not found, waiting for build",
),
);
}

const shutdown = async () => {
progress.start(`${chalk.cyan(projectName)} stopping...`);
try {
Expand All @@ -310,23 +373,41 @@ export const createRunCommand = () => {
process.exit(0);
};

// inhibit SIGINT and SIGTERM, will be handled gracefully by the shell
process.on("SIGINT", () => {});
process.on("SIGTERM", () => {});
if (detach) {
// inhibit SIGINT and SIGTERM, will be handled gracefully by the shell
process.on("SIGINT", () => {});
process.on("SIGTERM", () => {});

const log = program.parent?.commands.find(
(c) => c.name() === "logs",
);
const build = program.parent?.commands.find(
(c) => c.name() === "build",
);
await shell({
build,
epochLength,
log,
projectName,
prt: !authority,
});
await shutdown();
const log = program.parent?.commands.find(
(c) => c.name() === "logs",
);
const build = program.parent?.commands.find(
(c) => c.name() === "build",
);
await shell({
build,
deployment,
epochLength,
log,
projectName,
prt,
salt,
});
await shutdown();
} else {
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
try {
await cmd;
} catch (error: unknown) {
if (error instanceof ExecaError) {
// just continue gracefully
if (error.exitCode === 130) {
return;
}
throw error;
}
}
}
});
};
Loading