From c750bdacfc44a7ea02cee303e7fcb511cff3e4d0 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 5 Feb 2026 17:26:05 +0000 Subject: [PATCH 01/21] feat: add Docker support for running upgrade scripts Add Dockerfile and CI workflow to enable running orbit-actions commands in a containerized environment without requiring local installation of Foundry and Node.js. - Add Dockerfile with Node 18, Foundry, and pre-installed dependencies - Add smoke tests to verify tools and scripts are accessible - Add GitHub Actions workflow to build and test Docker image on PRs - Update README with Docker usage instructions --- .dockerignore | 30 +++++++++ .github/workflows/test-docker.yml | 30 +++++++++ Dockerfile | 30 +++++++++ README.md | 59 ++++++++++++++++++ package.json | 1 + test/docker/test-docker.bash | 100 ++++++++++++++++++++++++++++++ 6 files changed, 250 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/test-docker.yml create mode 100644 Dockerfile create mode 100755 test/docker/test-docker.bash diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..20bf558b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +# Dependencies (will be installed fresh in container) +node_modules/ + +# Build artifacts (will be built fresh in container) +out/ +cache_forge/ +cache/ +artifacts/ +typechain-types/ + +# Environment files (contain secrets) +.env +.env.* +!.env.example + +# Git +.git/ +.gitignore + +# IDE +.vscode/ +.idea/ + +# Test artifacts +broadcast/ +coverage/ + +# Docker +Dockerfile +.dockerignore diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml new file mode 100644 index 00000000..0d19a7c7 --- /dev/null +++ b/.github/workflows/test-docker.yml @@ -0,0 +1,30 @@ +name: Test Docker + +on: + pull_request: + workflow_dispatch: + +jobs: + test-docker: + name: Test Docker Image + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + load: true + tags: orbit-actions:test + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run Docker smoke tests + run: ./test/docker/test-docker.bash + env: + DOCKER_IMAGE: orbit-actions:test diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..bbf14796 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM node:18-slim + +# Install dependencies for Foundry and git +RUN apt-get update && apt-get install -y \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install Foundry +RUN curl -L https://foundry.paradigm.xyz | bash +ENV PATH="/root/.foundry/bin:${PATH}" +RUN foundryup + +# Enable Yarn via corepack +RUN corepack enable && corepack prepare yarn@stable --activate + +# Set working directory +WORKDIR /app + +# Copy package files first for better caching +COPY package.json yarn.lock ./ + +# Install dependencies +RUN yarn install --frozen-lockfile + +# Copy the rest of the repository +COPY . . + +# Build contracts +RUN forge build diff --git a/README.md b/README.md index 19be6454..d3d05e17 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,65 @@ For token bridge related operations, these are the additional requirements: yarn install ``` +## Using Docker + +The orbit actions are also available via docker. + +### Build the image + +```bash +docker build -t orbit-actions . +``` + +### Run commands + +Pass the command you want to run directly to Docker: + +```bash +# Check contract versions +docker run --rm \ + -e INBOX_ADDRESS=0xYourInboxAddress \ + -e INFURA_KEY=your_infura_key \ + orbit-actions \ + yarn orbit:contracts:version --network arb1 + +# Run forge script +docker run --rm \ + --env-file orbit.env \ + -v $(pwd)/broadcast:/app/broadcast \ + orbit-actions \ + forge script --sender 0xYourAddress --rpc-url $PARENT_CHAIN_RPC --broadcast \ + scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol -vvv + +# Run cast commands +docker run --rm \ + orbit-actions \ + cast call --rpc-url https://arb1.arbitrum.io/rpc 0xYourRollup "wasmModuleRoot()" +``` + +### Environment variables + +Create an `orbit.env` file with your configuration and pass it using `--env-file`: + +```bash +PARENT_CHAIN_RPC=https://arb1.arbitrum.io/rpc +INBOX_ADDRESS=0x... +PROXY_ADMIN_ADDRESS=0x... +PARENT_UPGRADE_EXECUTOR_ADDRESS=0x... +``` + +### Getting output artifacts + +Mount a volume to retrieve broadcast artifacts: + +```bash +docker run --rm \ + --env-file orbit.env \ + -v $(pwd)/broadcast:/app/broadcast \ + orbit-actions \ + forge script ... +``` + ## Check Version and Upgrade Path Run the follow command to check the version of Nitro contracts deployed on the parent chain of your Orbit chain. diff --git a/package.json b/package.json index 74e3f3ba..9c12cdb1 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "test:gas-check": "forge snapshot --check --tolerance 1 --match-path \"test/unit/**/*.t.sol\"", "test:sigs": "./test/signatures/test-sigs.bash", "test:storage": "./test/storage/test-storage.bash", + "test:docker": "./test/docker/test-docker.bash", "orbit:contracts:version": "hardhat run scripts/orbit-versioner/orbitVersioner.ts", "gas-snapshot": "forge snapshot --match-path \"test/unit/**/*.t.sol\"", "fix": "yarn format; yarn test:sigs; yarn test:storage; yarn gas-snapshot" diff --git a/test/docker/test-docker.bash b/test/docker/test-docker.bash new file mode 100755 index 00000000..25f54431 --- /dev/null +++ b/test/docker/test-docker.bash @@ -0,0 +1,100 @@ +#!/bin/bash +set -euo pipefail + +# Docker smoke tests for orbit-actions +# Verifies that all required tools and scripts are accessible in the Docker image + +IMAGE_NAME="${DOCKER_IMAGE:-orbit-actions:test}" + +echo "=== Docker Smoke Tests ===" +echo "Image: $IMAGE_NAME" +echo "" + +# Track failures +FAILURES=0 + +run_test() { + local name="$1" + shift + echo -n "Testing $name... " + if "$@" > /dev/null 2>&1; then + echo "OK" + else + echo "FAILED" + FAILURES=$((FAILURES + 1)) + fi +} + +# Test 1: Tools are installed +echo "--- Tool Availability ---" +run_test "forge" docker run --rm "$IMAGE_NAME" forge --version +run_test "cast" docker run --rm "$IMAGE_NAME" cast --version +run_test "yarn" docker run --rm "$IMAGE_NAME" yarn --version +run_test "node" docker run --rm "$IMAGE_NAME" node --version + +# Test 2: Dependencies are installed +echo "" +echo "--- Dependencies ---" +run_test "node_modules exists" docker run --rm "$IMAGE_NAME" test -d node_modules +run_test "forge dependencies" docker run --rm "$IMAGE_NAME" test -d node_modules/@arbitrum + +# Test 3: Contracts compile +echo "" +echo "--- Contract Compilation ---" +run_test "contracts built" docker run --rm "$IMAGE_NAME" test -d out + +# Test 4: Scripts are accessible +echo "" +echo "--- Script Accessibility ---" + +DEPLOY_SCRIPTS=( + "scripts/foundry/contract-upgrades/1.2.1/DeployNitroContracts1Point2Point1UpgradeAction.s.sol" + "scripts/foundry/contract-upgrades/2.1.0/DeployNitroContracts2Point1Point0UpgradeAction.s.sol" + "scripts/foundry/contract-upgrades/2.1.2/DeployNitroContracts2Point1Point2UpgradeAction.s.sol" + "scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol" + "scripts/foundry/arbos-upgrades/at-timestamp/DeployUpgradeArbOSVersionAtTimestampAction.s.sol" +) + +for script in "${DEPLOY_SCRIPTS[@]}"; do + script_name=$(basename "$script") + run_test "$script_name exists" docker run --rm "$IMAGE_NAME" test -f "$script" +done + +EXECUTE_SCRIPTS=( + "scripts/foundry/contract-upgrades/1.2.1/ExecuteNitroContracts1Point2Point1Upgrade.s.sol" + "scripts/foundry/contract-upgrades/2.1.0/ExecuteNitroContracts2Point1Point0Upgrade.s.sol" + "scripts/foundry/contract-upgrades/2.1.2/ExecuteNitroContracts2Point1Point2Upgrade.s.sol" + "scripts/foundry/contract-upgrades/2.1.3/ExecuteNitroContracts2Point1Point3Upgrade.s.sol" +) + +for script in "${EXECUTE_SCRIPTS[@]}"; do + script_name=$(basename "$script") + run_test "$script_name exists" docker run --rm "$IMAGE_NAME" test -f "$script" +done + +# Test 5: Yarn scripts work +echo "" +echo "--- Yarn Scripts ---" +run_test "yarn orbit:contracts:version --help" docker run --rm "$IMAGE_NAME" yarn orbit:contracts:version --help + +# Test 6: Unit tests pass +echo "" +echo "--- Unit Tests ---" +echo "Running unit tests inside container..." +if docker run --rm "$IMAGE_NAME" yarn test:unit; then + echo "Unit tests: OK" +else + echo "Unit tests: FAILED" + FAILURES=$((FAILURES + 1)) +fi + +# Summary +echo "" +echo "=== Summary ===" +if [ $FAILURES -eq 0 ]; then + echo "All tests passed!" + exit 0 +else + echo "$FAILURES test(s) failed" + exit 1 +fi From 1f0f4f378dd587fd2aa561da21b64a22f3872ce6 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 5 Feb 2026 17:32:14 +0000 Subject: [PATCH 02/21] chore: add .DS_Store to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8e35eb36..973fff40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules .env +.DS_Store # Hardhat files /cache From eea7623101dce1276aa379b8b8a3abb977232f9a Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 5 Feb 2026 17:37:10 +0000 Subject: [PATCH 03/21] fix: use Yarn Classic (v1) in Dockerfile for lockfile compatibility --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index bbf14796..6e502b1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,8 +11,8 @@ RUN curl -L https://foundry.paradigm.xyz | bash ENV PATH="/root/.foundry/bin:${PATH}" RUN foundryup -# Enable Yarn via corepack -RUN corepack enable && corepack prepare yarn@stable --activate +# Install Yarn Classic (v1) - matches the repo's yarn.lock format +RUN npm install -g --force yarn@1.22.22 # Set working directory WORKDIR /app @@ -20,8 +20,8 @@ WORKDIR /app # Copy package files first for better caching COPY package.json yarn.lock ./ -# Install dependencies -RUN yarn install --frozen-lockfile +# Install dependencies (using --ignore-scripts like CI does, then forge install separately) +RUN yarn install --frozen-lockfile --ignore-scripts # Copy the rest of the repository COPY . . From c5948fafb940d21b006e9249fa5d7404f2c4dcb2 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 5 Feb 2026 17:38:38 +0000 Subject: [PATCH 04/21] fix: add submodule checkout for Docker build The Docker build requires lib/ (forge-std, arbitrum-sdk) which comes from git submodules. Update CI to checkout with submodules and document the prerequisite for local builds. --- .github/workflows/test-docker.yml | 2 ++ README.md | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 0d19a7c7..eea090e4 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -11,6 +11,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/README.md b/README.md index d3d05e17..09227ba2 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,14 @@ The orbit actions are also available via docker. ### Build the image +First, ensure submodules are initialized (required for Foundry dependencies): + +```bash +git submodule update --init --recursive +``` + +Then build the image: + ```bash docker build -t orbit-actions . ``` From 4c8edb87fef5f35cb4f9f9ae43db72d375628ade Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 5 Feb 2026 17:40:22 +0000 Subject: [PATCH 05/21] fix: use forge install instead of git submodules for Docker build prereq --- .github/workflows/test-docker.yml | 8 +++++++- README.md | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index eea090e4..04a7f78a 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -11,8 +11,14 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 with: - submodules: recursive + version: stable + + - name: Install forge dependencies + run: forge install - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/README.md b/README.md index 09227ba2..93753919 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,10 @@ The orbit actions are also available via docker. ### Build the image -First, ensure submodules are initialized (required for Foundry dependencies): +First, ensure Foundry dependencies are installed: ```bash -git submodule update --init --recursive +forge install ``` Then build the image: From 9cb07c5754dbfe8c71f795b9a56f0b7794f6e927 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Thu, 5 Feb 2026 17:47:43 +0000 Subject: [PATCH 06/21] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 93753919..90383e73 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ yarn install ## Using Docker -The orbit actions are also available via docker. +The Orbit actions are also available via Docker. ### Build the image From c445d7d2709b6db32f017d3cc84fca056dbaa05b Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Fri, 6 Feb 2026 17:05:48 +0000 Subject: [PATCH 07/21] feat: add browsable CLI for upgrade scripts Add a path-based CLI that allows users to browse and execute scripts from the foundry directory: - Browse directories: `docker run orbit-actions contract-upgrades/1.2.1` - View files: `docker run orbit-actions contract-upgrades/1.2.1/README.md` - Run upgrades: `docker run orbit-actions contract-upgrades/1.2.1/deploy-execute-verify` Structure: - entrypoint.sh: thin shim that sources .env and delegates to router - bin/router: path parsing, directory listing, command dispatch - bin/contract-upgrade: deploy, execute, deploy-execute-verify commands - bin/arbos-upgrade: deploy, execute, verify, deploy-execute-verify commands - lib/common.sh: shared utilities for auth parsing and forge helpers Upgrade commands read configuration from mounted .env file and accept auth flags (--deploy-key, --execute-key, --ledger, etc.) for signing. --- Dockerfile | 9 +- bin/arbos-upgrade | 320 +++++++++++++++++++++++++++++++++++ bin/contract-upgrade | 281 ++++++++++++++++++++++++++++++ bin/router | 188 ++++++++++++++++++++ entrypoint.sh | 4 + lib/common.sh | 135 +++++++++++++++ test/docker/test-docker.bash | 132 +++++++++++---- 7 files changed, 1036 insertions(+), 33 deletions(-) create mode 100644 bin/arbos-upgrade create mode 100644 bin/contract-upgrade create mode 100644 bin/router create mode 100755 entrypoint.sh create mode 100644 lib/common.sh diff --git a/Dockerfile b/Dockerfile index 6e502b1f..9d306176 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,10 @@ FROM node:18-slim -# Install dependencies for Foundry and git +# Install dependencies for Foundry, git, and jq (for JSON parsing in upgrade scripts) RUN apt-get update && apt-get install -y \ curl \ git \ + jq \ && rm -rf /var/lib/apt/lists/* # Install Foundry @@ -28,3 +29,9 @@ COPY . . # Build contracts RUN forge build + +# Make scripts executable +RUN chmod +x /app/entrypoint.sh /app/bin/* /app/lib/* + +# Set entrypoint for command routing +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/bin/arbos-upgrade b/bin/arbos-upgrade new file mode 100644 index 00000000..3cccd9a5 --- /dev/null +++ b/bin/arbos-upgrade @@ -0,0 +1,320 @@ +#!/bin/bash +set -euo pipefail + +# ArbOS upgrades script for orbit-actions +# Handles deploy, execute, verify, and deploy-execute-verify commands + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../lib/common.sh" + +ARBOS_DIR="/app/scripts/foundry/arbos-upgrades/at-timestamp" + +# ============================================================================= +# Commands +# ============================================================================= + +cmd_deploy() { + local version="$1" + shift + + parse_auth_args "$@" + + require_env CHILD_CHAIN_RPC + + export ARBOS_VERSION="$version" + + local deploy_script="$ARBOS_DIR/DeployUpgradeArbOSVersionAtTimestampAction.s.sol" + if [[ ! -f "$deploy_script" ]]; then + die "Deploy script not found: $deploy_script" + fi + + log "Running: $(basename "$deploy_script") for ArbOS $version" + + local cmd="forge script $deploy_script --rpc-url $CHILD_CHAIN_RPC --slow -vvv" + + if [[ -n "$AUTH_ARGS" ]]; then + cmd="$cmd --broadcast $AUTH_ARGS" + fi + + eval $cmd +} + +cmd_execute() { + shift # version not needed for execute + + parse_auth_args "$@" + + require_env CHILD_CHAIN_RPC + require_env CHILD_UPGRADE_EXECUTOR_ADDRESS + require_env UPGRADE_ACTION_ADDRESS + + log "Executing ArbOS upgrade action: $UPGRADE_ACTION_ADDRESS" + + local perform_calldata="0xb0a75d36" + + if [[ -z "$AUTH_ARGS" ]]; then + # No auth - output calldata for multisig + local execute_calldata + execute_calldata=$(cast calldata "execute(address,bytes)" "$UPGRADE_ACTION_ADDRESS" "$perform_calldata") + + log "Calldata for UpgradeExecutor.execute():" + echo "" + echo "To: $CHILD_UPGRADE_EXECUTOR_ADDRESS" + echo "Calldata: $execute_calldata" + echo "" + log "Submit this to your multisig/Safe to execute the upgrade" + else + local cast_cmd="cast send $CHILD_UPGRADE_EXECUTOR_ADDRESS \"execute(address,bytes)\" $UPGRADE_ACTION_ADDRESS $perform_calldata --rpc-url $CHILD_CHAIN_RPC $AUTH_ARGS" + + eval $cast_cmd + + log "ArbOS upgrade scheduled successfully" + fi +} + +cmd_verify() { + shift # version not needed for verify + + require_env CHILD_CHAIN_RPC + + log "Checking ArbOS upgrade status..." + + local scheduled + scheduled=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ + "0x000000000000000000000000000000000000006b" \ + "getScheduledUpgrade()(uint64,uint64)" 2>/dev/null || echo "N/A") + log "Scheduled upgrade (version, timestamp): $scheduled" + + local current_raw + current_raw=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ + "0x0000000000000000000000000000000000000064" \ + "arbOSVersion()(uint64)" 2>/dev/null || echo "0") + local current_version=$((current_raw - 55)) + log "Current ArbOS version: $current_version" +} + +cmd_deploy_execute_verify() { + local version="$1" + shift + + parse_deploy_execute_auth "$@" + + log "ArbOS version: $version" + + local deploy_script="$ARBOS_DIR/DeployUpgradeArbOSVersionAtTimestampAction.s.sol" + if [[ ! -f "$deploy_script" ]]; then + die "Deploy script not found: $deploy_script" + fi + + # Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set + local skip_deploy=false + if [[ -n "${UPGRADE_ACTION_ADDRESS:-}" ]]; then + skip_deploy=true + log "Using existing action from .env: $UPGRADE_ACTION_ADDRESS" + fi + + # Validate required env vars + require_env CHILD_CHAIN_RPC + require_env CHILD_UPGRADE_EXECUTOR_ADDRESS + require_env SCHEDULE_TIMESTAMP + + export ARBOS_VERSION="$version" + + # Validate auth + local deploy_auth=$(get_deploy_auth) + local execute_auth=$(get_execute_auth) + + if [[ "$skip_deploy" != "true" && "$DRY_RUN" != "true" && -z "$deploy_auth" ]]; then + die "Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive" + fi + if [[ "$SKIP_EXECUTE" != "true" && "$DRY_RUN" != "true" && -z "$execute_auth" ]]; then + die "Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive" + fi + + log "Scheduled timestamp: $SCHEDULE_TIMESTAMP" + + # Step 1: Deploy + local chain_id="" + if [[ "$skip_deploy" != "true" ]]; then + chain_id=$(get_chain_id "$CHILD_CHAIN_RPC") + log "Target chain ID: $chain_id" + log "Step 1: Deploying ArbOS upgrade action..." + + local forge_cmd="forge script $deploy_script --rpc-url $CHILD_CHAIN_RPC --slow -vvv" + + if [[ "$DRY_RUN" != "true" ]]; then + forge_cmd="$forge_cmd --broadcast $deploy_auth" + fi + if [[ "$VERIFY_CONTRACTS" == "true" ]]; then + forge_cmd="$forge_cmd --verify" + fi + + eval $forge_cmd + + if [[ "$DRY_RUN" != "true" ]]; then + UPGRADE_ACTION_ADDRESS=$(parse_action_address "$deploy_script" "$chain_id") + log "Deployed action at: $UPGRADE_ACTION_ADDRESS" + else + log "Dry run - no action deployed" + if [[ "$SKIP_EXECUTE" != "true" ]]; then + log "Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step" + return 0 + fi + fi + else + log "Step 1: Skipped deploy" + fi + + # Step 2: Execute via cast send + if [[ "$SKIP_EXECUTE" != "true" ]]; then + log "Step 2: Executing ArbOS upgrade..." + + local perform_calldata="0xb0a75d36" + + if [[ "$DRY_RUN" == "true" ]]; then + local execute_calldata + execute_calldata=$(cast calldata "execute(address,bytes)" "$UPGRADE_ACTION_ADDRESS" "$perform_calldata") + + log "Dry run - calldata for UpgradeExecutor.execute():" + echo "" + echo "To: $CHILD_UPGRADE_EXECUTOR_ADDRESS" + echo "Calldata: $execute_calldata" + echo "" + log "Submit this to your multisig/Safe to execute the upgrade" + else + local cast_cmd="cast send $CHILD_UPGRADE_EXECUTOR_ADDRESS \"execute(address,bytes)\" $UPGRADE_ACTION_ADDRESS $perform_calldata --rpc-url $CHILD_CHAIN_RPC" + + if [[ -n "$EXECUTE_KEY" ]]; then + cast_cmd="$cast_cmd --private-key $EXECUTE_KEY" + elif [[ -n "$EXECUTE_ACCOUNT" ]]; then + cast_cmd="$cast_cmd --account $EXECUTE_ACCOUNT" + elif [[ "$EXECUTE_LEDGER" == "true" ]]; then + cast_cmd="$cast_cmd --ledger" + elif [[ "$EXECUTE_INTERACTIVE" == "true" ]]; then + cast_cmd="$cast_cmd --interactive" + fi + + eval $cast_cmd + + log "ArbOS upgrade scheduled successfully" + fi + else + log "Step 2: Skipped execute" + fi + + # Step 3: Verify + if [[ "$DRY_RUN" != "true" && "$SKIP_EXECUTE" != "true" ]]; then + log "Step 3: Verifying scheduled upgrade..." + + local scheduled + scheduled=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ + "0x000000000000000000000000000000000000006b" \ + "getScheduledUpgrade()(uint64,uint64)" 2>/dev/null || echo "N/A") + log "Scheduled upgrade (version, timestamp): $scheduled" + + local current_raw + current_raw=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ + "0x0000000000000000000000000000000000000064" \ + "arbOSVersion()(uint64)" 2>/dev/null || echo "0") + local current_version=$((current_raw - 55)) + log "Current ArbOS version: $current_version" + fi + + log "Done" +} + +# ============================================================================= +# Main +# ============================================================================= + +show_help() { + cat <<'EOF' +Usage: arbos-upgrade [options] + +Commands: + deploy Run deploy script only + execute Execute upgrade action (schedule the upgrade) + verify Check scheduled upgrade status + deploy-execute-verify Full upgrade flow (deploy, execute, verify) + +Options for deploy: + --private-key KEY Private key + --account NAME Keystore account + --ledger Use Ledger + --interactive Prompt for key + +Options for deploy-execute-verify: + --deploy-key KEY Private key for deploy step + --deploy-account NAME Keystore account for deploy + --deploy-ledger Use Ledger for deploy + --execute-key KEY Private key for execute step + --execute-account NAME Keystore account for execute + --execute-ledger Use Ledger for execute + --dry-run, -n Simulate without broadcasting + --skip-execute Deploy only + --verify, -v Verify on block explorer + +Required .env variables: + CHILD_CHAIN_RPC Child chain RPC URL + CHILD_UPGRADE_EXECUTOR_ADDRESS UpgradeExecutor address + SCHEDULE_TIMESTAMP Unix timestamp for upgrade + +Optional .env variables: + ARBOS_VERSION ArbOS version (alternative to positional arg) + UPGRADE_ACTION_ADDRESS Pre-deployed action (skips deploy) +EOF +} + +main() { + if [[ $# -lt 1 ]]; then + show_help + exit 1 + fi + + local version="$1" + shift + + if [[ "$version" == "--help" || "$version" == "-h" ]]; then + show_help + exit 0 + fi + + # Version can also come from env + if [[ -z "$version" && -n "${ARBOS_VERSION:-}" ]]; then + version="$ARBOS_VERSION" + fi + + if [[ -z "$version" ]]; then + die "ArbOS version required" + fi + + local command="${1:-deploy-execute-verify}" + if [[ $# -gt 0 ]]; then + shift + fi + + case "$command" in + deploy) + cmd_deploy "$version" "$@" + ;; + execute) + cmd_execute "$version" "$@" + ;; + verify) + cmd_verify "$version" "$@" + ;; + deploy-execute-verify) + cmd_deploy_execute_verify "$version" "$@" + ;; + --help|-h) + show_help + ;; + *) + die "Unknown command: $command + +Commands: deploy, execute, verify, deploy-execute-verify" + ;; + esac +} + +main "$@" diff --git a/bin/contract-upgrade b/bin/contract-upgrade new file mode 100644 index 00000000..f2344552 --- /dev/null +++ b/bin/contract-upgrade @@ -0,0 +1,281 @@ +#!/bin/bash +set -euo pipefail + +# Contract upgrades script for orbit-actions +# Handles deploy, execute, and deploy-execute-verify commands + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../lib/common.sh" + +CONTRACTS_DIR="/app/scripts/foundry/contract-upgrades" + +# ============================================================================= +# Commands +# ============================================================================= + +cmd_deploy() { + local version_dir="$1" + shift + + parse_auth_args "$@" + + require_env PARENT_CHAIN_RPC + + local deploy_script=$(find "$version_dir" -maxdepth 1 -name "Deploy*.s.sol" -type f 2>/dev/null | head -1) + if [[ -z "$deploy_script" ]]; then + die "No deploy script found in $version_dir" + fi + + log "Running: $(basename "$deploy_script")" + + local cmd="forge script $deploy_script --rpc-url $PARENT_CHAIN_RPC --slow -vvv --skip-simulation" + + if [[ -n "$AUTH_ARGS" ]]; then + cmd="$cmd --broadcast $AUTH_ARGS" + fi + + eval $cmd +} + +cmd_execute() { + local version_dir="$1" + shift + + parse_auth_args "$@" + + require_env PARENT_CHAIN_RPC + require_env UPGRADE_ACTION_ADDRESS + + local execute_script=$(find "$version_dir" -maxdepth 1 -name "Execute*.s.sol" -type f 2>/dev/null | head -1) + if [[ -z "$execute_script" ]]; then + die "No execute script found in $version_dir" + fi + + log "Running: $(basename "$execute_script")" + + local cmd="forge script $execute_script --rpc-url $PARENT_CHAIN_RPC -vvv" + + if [[ -n "$AUTH_ARGS" ]]; then + cmd="$cmd --broadcast $AUTH_ARGS" + fi + + eval $cmd +} + +cmd_deploy_execute_verify() { + local version_dir="$1" + local version=$(basename "$version_dir") + shift + + parse_deploy_execute_auth "$@" + + # Find scripts + local deploy_script=$(find "$version_dir" -maxdepth 1 -name "Deploy*.s.sol" -type f 2>/dev/null | head -1) + local execute_script=$(find "$version_dir" -maxdepth 1 -name "Execute*.s.sol" -type f 2>/dev/null | head -1) + + if [[ -z "$deploy_script" ]]; then + die "No deploy script found in $version_dir" + fi + if [[ -z "$execute_script" ]]; then + die "No execute script found in $version_dir" + fi + + log "Version: $version" + log "Deploy script: $(basename "$deploy_script")" + log "Execute script: $(basename "$execute_script")" + + # Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set + local skip_deploy=false + if [[ -n "${UPGRADE_ACTION_ADDRESS:-}" ]]; then + skip_deploy=true + log "Using existing action from .env: $UPGRADE_ACTION_ADDRESS" + fi + + # Validate required env vars + require_env PARENT_CHAIN_RPC + require_env INBOX_ADDRESS + require_env PROXY_ADMIN_ADDRESS + require_env PARENT_UPGRADE_EXECUTOR_ADDRESS + + # Validate auth + local deploy_auth=$(get_deploy_auth) + local execute_auth=$(get_execute_auth) + + if [[ "$skip_deploy" != "true" && "$DRY_RUN" != "true" && -z "$deploy_auth" ]]; then + die "Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive" + fi + if [[ "$SKIP_EXECUTE" != "true" && "$DRY_RUN" != "true" && -z "$execute_auth" ]]; then + die "Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive" + fi + + local chain_id + chain_id=$(get_chain_id "$PARENT_CHAIN_RPC") + log "Target chain ID: $chain_id" + + # Step 1: Deploy + if [[ "$skip_deploy" != "true" ]]; then + log "Step 1: Deploying upgrade action..." + + local forge_cmd="forge script $deploy_script --rpc-url $PARENT_CHAIN_RPC --slow -vvv --skip-simulation" + + if [[ "$DRY_RUN" != "true" ]]; then + forge_cmd="$forge_cmd --broadcast $deploy_auth" + fi + if [[ "$VERIFY_CONTRACTS" == "true" ]]; then + forge_cmd="$forge_cmd --verify" + fi + + eval $forge_cmd + + if [[ "$DRY_RUN" != "true" ]]; then + UPGRADE_ACTION_ADDRESS=$(parse_action_address "$deploy_script" "$chain_id") + log "Deployed action at: $UPGRADE_ACTION_ADDRESS" + else + log "Dry run - no action deployed" + if [[ "$SKIP_EXECUTE" != "true" ]]; then + log "Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step" + return 0 + fi + fi + else + log "Step 1: Skipped deploy" + fi + + export UPGRADE_ACTION_ADDRESS + + # Step 2: Execute + if [[ "$SKIP_EXECUTE" != "true" ]]; then + log "Step 2: Executing upgrade..." + + local forge_cmd="forge script $execute_script --rpc-url $PARENT_CHAIN_RPC -vvv" + + if [[ "$DRY_RUN" != "true" ]]; then + forge_cmd="$forge_cmd --broadcast $execute_auth" + fi + + eval $forge_cmd + + if [[ "$DRY_RUN" == "true" ]]; then + log "Dry run - upgrade not executed" + else + log "Upgrade executed successfully" + fi + else + log "Step 2: Skipped execute" + fi + + # Step 3: Verify + if [[ "$DRY_RUN" != "true" && "$SKIP_EXECUTE" != "true" ]]; then + log "Step 3: Verifying upgrade..." + + case "$version" in + 2.1.2|2.1.3) + local bridge + bridge=$(cast call --rpc-url "$PARENT_CHAIN_RPC" "$INBOX_ADDRESS" "bridge()(address)" 2>/dev/null || echo "") + if [[ -n "$bridge" && "$bridge" != "0x" ]]; then + local decimals + decimals=$(cast call --rpc-url "$PARENT_CHAIN_RPC" "$bridge" "nativeTokenDecimals()(uint8)" 2>/dev/null || echo "N/A") + log "Verification: nativeTokenDecimals = $decimals" + fi + ;; + 1.2.1|2.1.0) + if [[ -n "${ROLLUP:-}" ]]; then + local wasm_root + wasm_root=$(cast call --rpc-url "$PARENT_CHAIN_RPC" "$ROLLUP" "wasmModuleRoot()(bytes32)" 2>/dev/null || echo "N/A") + log "Verification: wasmModuleRoot = $wasm_root" + else + log "Verification: Set ROLLUP in .env to check wasmModuleRoot" + fi + ;; + esac + fi + + log "Done" +} + +# ============================================================================= +# Main +# ============================================================================= + +show_help() { + cat <<'EOF' +Usage: contract-upgrade [options] + +Commands: + deploy Run deploy script only + execute Run execute script only + deploy-execute-verify Full upgrade flow (deploy, execute, verify) + +Options for deploy/execute: + --private-key KEY Private key + --account NAME Keystore account + --ledger Use Ledger + --interactive Prompt for key + +Options for deploy-execute-verify: + --deploy-key KEY Private key for deploy step + --deploy-account NAME Keystore account for deploy + --deploy-ledger Use Ledger for deploy + --execute-key KEY Private key for execute step + --execute-account NAME Keystore account for execute + --execute-ledger Use Ledger for execute + --dry-run, -n Simulate without broadcasting + --skip-execute Deploy only + --verify, -v Verify on block explorer + +Required .env variables: + PARENT_CHAIN_RPC Parent chain RPC URL + INBOX_ADDRESS Inbox contract address + PROXY_ADMIN_ADDRESS ProxyAdmin address + PARENT_UPGRADE_EXECUTOR_ADDRESS UpgradeExecutor address + +Optional .env variables: + UPGRADE_ACTION_ADDRESS Pre-deployed action (skips deploy) + ROLLUP Rollup address (for verification) +EOF +} + +main() { + if [[ $# -lt 2 ]]; then + show_help + exit 1 + fi + + local version="$1" + local command="$2" + shift 2 + + if [[ "$version" == "--help" || "$version" == "-h" ]]; then + show_help + exit 0 + fi + + local version_dir="$CONTRACTS_DIR/$version" + if [[ ! -d "$version_dir" ]]; then + die "Unknown version: $version + +Available versions: $(ls "$CONTRACTS_DIR" 2>/dev/null | tr '\n' ' ' || echo "none found")" + fi + + case "$command" in + deploy) + cmd_deploy "$version_dir" "$@" + ;; + execute) + cmd_execute "$version_dir" "$@" + ;; + deploy-execute-verify) + cmd_deploy_execute_verify "$version_dir" "$@" + ;; + --help|-h) + show_help + ;; + *) + die "Unknown command: $command + +Commands: deploy, execute, deploy-execute-verify" + ;; + esac +} + +main "$@" diff --git a/bin/router b/bin/router new file mode 100644 index 00000000..609873a8 --- /dev/null +++ b/bin/router @@ -0,0 +1,188 @@ +#!/bin/bash +set -euo pipefail + +# ============================================================================= +# orbit-actions router +# Handles path parsing, directory listing, file viewing, and command dispatch +# ============================================================================= + +SCRIPTS_DIR="/app/scripts/foundry" + +# ============================================================================= +# Utility Functions +# ============================================================================= + +die() { + echo "Error: $1" >&2 + exit 1 +} + +# ============================================================================= +# Directory Listing +# ============================================================================= + +list_directory() { + local dir="$1" + local rel_path="${dir#$SCRIPTS_DIR/}" + # Normalize: remove leading ./ + rel_path="${rel_path#./}" + + # List actual contents + ls -1 "$dir" + + # Add virtual commands based on directory type + case "$rel_path" in + contract-upgrades/[0-9]*) + echo "---" + echo "deploy (run Deploy script)" + echo "execute (run Execute script)" + echo "deploy-execute-verify (full upgrade flow)" + ;; + arbos-upgrades/at-timestamp) + echo "---" + echo "deploy (run Deploy script)" + echo "execute (execute upgrade action)" + echo "verify (check upgrade status)" + echo "deploy-execute-verify (full upgrade flow)" + ;; + esac +} + +# ============================================================================= +# Help +# ============================================================================= + +show_help() { + cat <<'EOF' +Usage: docker run orbit-actions [path] [args...] + +Browse and execute scripts from the foundry scripts directory. + +Browsing: + . List top-level directories + contract-upgrades List available versions + contract-upgrades/1.2.1 List version contents + commands + contract-upgrades/1.2.1/env-templates List env templates + +Viewing files: + contract-upgrades/1.2.1/README.md View README + contract-upgrades/1.2.1/env-templates/.env.example View env template + contract-upgrades/2.1.0/.env.sample View env sample + +Running upgrade scripts: + contract-upgrades//deploy [--private-key KEY] + contract-upgrades//execute [--private-key KEY] + contract-upgrades//deploy-execute-verify [--deploy-key KEY] [--execute-key KEY] + + arbos-upgrades/at-timestamp/deploy [--private-key KEY] + arbos-upgrades/at-timestamp/deploy-execute-verify [--deploy-key KEY] [--execute-key KEY] + +Options for deploy-execute-verify: + --deploy-key KEY Private key for deploy step + --deploy-account NAME Keystore account for deploy + --deploy-ledger Use Ledger for deploy + --execute-key KEY Private key for execute step + --execute-account NAME Keystore account for execute + --execute-ledger Use Ledger for execute + --dry-run, -n Simulate without broadcasting + --skip-execute Deploy only + --verify, -v Verify on block explorer + +Passthrough commands: + forge ... Run forge directly + cast ... Run cast directly + yarn ... Run yarn directly + +Examples: + docker run orbit-actions contract-upgrades/1.2.1 + docker run orbit-actions contract-upgrades/1.2.1/README.md + docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/deploy-execute-verify --dry-run +EOF +} + +# ============================================================================= +# Main Router +# ============================================================================= + +main() { + # No args - list top level + if [[ $# -eq 0 ]]; then + ls -1 "$SCRIPTS_DIR" + exit 0 + fi + + local path="$1" + shift + + # Help + if [[ "$path" == "help" || "$path" == "--help" || "$path" == "-h" ]]; then + show_help + exit 0 + fi + + # Passthrough: forge, cast, yarn, node, bash, etc. + if command -v "$path" &>/dev/null && [[ ! -e "$SCRIPTS_DIR/$path" ]]; then + exec "$path" "$@" + fi + + # Parse path for virtual commands + local full_path="$SCRIPTS_DIR/$path" + local parent_path=$(dirname "$full_path") + local basename=$(basename "$path") + + # Check for virtual commands (deploy, execute, deploy-execute-verify) + if [[ ! -e "$full_path" && -d "$parent_path" ]]; then + local rel_parent="${parent_path#$SCRIPTS_DIR/}" + # Normalize: remove leading ./ + rel_parent="${rel_parent#./}" + + case "$rel_parent" in + contract-upgrades/[0-9]*) + local version=$(basename "$rel_parent") + case "$basename" in + deploy|execute|deploy-execute-verify) + exec /app/bin/contract-upgrade "$version" "$basename" "$@" + ;; + esac + ;; + arbos-upgrades/at-timestamp) + case "$basename" in + deploy|deploy-execute-verify) + # These commands need a version argument + local version="${1:-}" + if [[ -z "$version" ]]; then + echo "Error: ArbOS version required" >&2 + echo "Usage: arbos-upgrades/at-timestamp/$basename [options]" >&2 + exit 1 + fi + shift + exec /app/bin/arbos-upgrade "$version" "$basename" "$@" + ;; + execute|verify) + # These commands don't need a version argument + exec /app/bin/arbos-upgrade "" "$basename" "$@" + ;; + esac + ;; + esac + fi + + # Directory - list contents + if [[ -d "$full_path" ]]; then + list_directory "$full_path" + exit 0 + fi + + # Regular file - cat it + if [[ -f "$full_path" ]]; then + cat "$full_path" + exit 0 + fi + + # Not found + die "Not found: $path + +Use 'help' to see available commands." +} + +main "$@" diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000..965dfa52 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Source .env if mounted, then delegate to router +[[ -f /app/.env ]] && set -a && source /app/.env && set +a +exec /app/bin/router "$@" diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 00000000..9a4ac560 --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,135 @@ +#!/bin/bash +# Shared utilities for upgrade scripts + +# ============================================================================= +# Utility Functions +# ============================================================================= + +die() { + echo "Error: $1" >&2 + exit 1 +} + +log() { + echo "[orbit-actions] $1" +} + +require_env() { + local name="$1" + local value="${!name:-}" + if [[ -z "$value" ]]; then + die "Required env var not set: $name (check your .env file)" + fi +} + +# ============================================================================= +# Auth Helpers +# ============================================================================= + +# Build forge/cast auth args from CLI flags +# Sets global: AUTH_ARGS +parse_auth_args() { + AUTH_ARGS="" + while [[ $# -gt 0 ]]; do + case "$1" in + --private-key|--account) + AUTH_ARGS="$1 $2" + shift 2 + ;; + --ledger|--interactive) + AUTH_ARGS="$1" + shift + ;; + *) + shift + ;; + esac + done +} + +# Build auth args for deploy step (from --deploy-* flags) +get_deploy_auth() { + if [[ -n "${DEPLOY_KEY:-}" ]]; then + echo "--private-key $DEPLOY_KEY" + elif [[ -n "${DEPLOY_ACCOUNT:-}" ]]; then + echo "--account $DEPLOY_ACCOUNT" + elif [[ "${DEPLOY_LEDGER:-}" == "true" ]]; then + echo "--ledger" + elif [[ "${DEPLOY_INTERACTIVE:-}" == "true" ]]; then + echo "--interactive" + fi +} + +# Build auth args for execute step (from --execute-* flags) +get_execute_auth() { + if [[ -n "${EXECUTE_KEY:-}" ]]; then + echo "--private-key $EXECUTE_KEY" + elif [[ -n "${EXECUTE_ACCOUNT:-}" ]]; then + echo "--account $EXECUTE_ACCOUNT" + elif [[ "${EXECUTE_LEDGER:-}" == "true" ]]; then + echo "--ledger" + elif [[ "${EXECUTE_INTERACTIVE:-}" == "true" ]]; then + echo "--interactive" + fi +} + +# Parse --deploy-* and --execute-* flags into variables +parse_deploy_execute_auth() { + DEPLOY_KEY="" + DEPLOY_ACCOUNT="" + DEPLOY_LEDGER=false + DEPLOY_INTERACTIVE=false + EXECUTE_KEY="" + EXECUTE_ACCOUNT="" + EXECUTE_LEDGER=false + EXECUTE_INTERACTIVE=false + DRY_RUN=false + SKIP_EXECUTE=false + VERIFY_CONTRACTS=false + REMAINING_ARGS=() + + while [[ $# -gt 0 ]]; do + case "$1" in + --deploy-key) DEPLOY_KEY="$2"; shift 2 ;; + --deploy-account) DEPLOY_ACCOUNT="$2"; shift 2 ;; + --deploy-ledger) DEPLOY_LEDGER=true; shift ;; + --deploy-interactive) DEPLOY_INTERACTIVE=true; shift ;; + --execute-key) EXECUTE_KEY="$2"; shift 2 ;; + --execute-account) EXECUTE_ACCOUNT="$2"; shift 2 ;; + --execute-ledger) EXECUTE_LEDGER=true; shift ;; + --execute-interactive) EXECUTE_INTERACTIVE=true; shift ;; + --dry-run|-n) DRY_RUN=true; shift ;; + --skip-execute) SKIP_EXECUTE=true; shift ;; + --verify|-v) VERIFY_CONTRACTS=true; shift ;; + *) REMAINING_ARGS+=("$1"); shift ;; + esac + done +} + +# ============================================================================= +# Forge Script Helpers +# ============================================================================= + +get_chain_id() { + local rpc="$1" + cast chain-id --rpc-url "$rpc" +} + +parse_action_address() { + local script_path="$1" + local chain_id="$2" + local script_name=$(basename "$script_path") + local broadcast_file="/app/broadcast/${script_name}/${chain_id}/run-latest.json" + + if [[ ! -f "$broadcast_file" ]]; then + die "Broadcast file not found: $broadcast_file" + fi + + local address=$(jq -r '.transactions | map(select(.transactionType == "CREATE")) | last | .contractAddress' "$broadcast_file") + + if [[ -z "$address" || "$address" == "null" ]]; then + die "Could not parse action address from broadcast file" + fi + + echo "$address" +} diff --git a/test/docker/test-docker.bash b/test/docker/test-docker.bash index 25f54431..03167239 100755 --- a/test/docker/test-docker.bash +++ b/test/docker/test-docker.bash @@ -25,8 +25,8 @@ run_test() { fi } -# Test 1: Tools are installed -echo "--- Tool Availability ---" +# Test 1: Tools are installed (passthrough) +echo "--- Tool Passthrough ---" run_test "forge" docker run --rm "$IMAGE_NAME" forge --version run_test "cast" docker run --rm "$IMAGE_NAME" cast --version run_test "yarn" docker run --rm "$IMAGE_NAME" yarn --version @@ -43,41 +43,85 @@ echo "" echo "--- Contract Compilation ---" run_test "contracts built" docker run --rm "$IMAGE_NAME" test -d out -# Test 4: Scripts are accessible +# Test 4: Browsing - list directories echo "" -echo "--- Script Accessibility ---" - -DEPLOY_SCRIPTS=( - "scripts/foundry/contract-upgrades/1.2.1/DeployNitroContracts1Point2Point1UpgradeAction.s.sol" - "scripts/foundry/contract-upgrades/2.1.0/DeployNitroContracts2Point1Point0UpgradeAction.s.sol" - "scripts/foundry/contract-upgrades/2.1.2/DeployNitroContracts2Point1Point2UpgradeAction.s.sol" - "scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol" - "scripts/foundry/arbos-upgrades/at-timestamp/DeployUpgradeArbOSVersionAtTimestampAction.s.sol" -) - -for script in "${DEPLOY_SCRIPTS[@]}"; do - script_name=$(basename "$script") - run_test "$script_name exists" docker run --rm "$IMAGE_NAME" test -f "$script" -done - -EXECUTE_SCRIPTS=( - "scripts/foundry/contract-upgrades/1.2.1/ExecuteNitroContracts1Point2Point1Upgrade.s.sol" - "scripts/foundry/contract-upgrades/2.1.0/ExecuteNitroContracts2Point1Point0Upgrade.s.sol" - "scripts/foundry/contract-upgrades/2.1.2/ExecuteNitroContracts2Point1Point2Upgrade.s.sol" - "scripts/foundry/contract-upgrades/2.1.3/ExecuteNitroContracts2Point1Point3Upgrade.s.sol" -) - -for script in "${EXECUTE_SCRIPTS[@]}"; do - script_name=$(basename "$script") - run_test "$script_name exists" docker run --rm "$IMAGE_NAME" test -f "$script" -done - -# Test 5: Yarn scripts work +echo "--- Directory Browsing ---" + +# List top level +echo -n "Testing list top level... " +TOP_OUTPUT=$(docker run --rm "$IMAGE_NAME" 2>&1) +if echo "$TOP_OUTPUT" | grep "contract-upgrades" > /dev/null; then + echo "OK" +else + echo "FAILED" + FAILURES=$((FAILURES + 1)) +fi + +# List contract-upgrades versions +echo -n "Testing list contract-upgrades... " +VERSIONS_OUTPUT=$(docker run --rm "$IMAGE_NAME" contract-upgrades 2>&1) +if echo "$VERSIONS_OUTPUT" | grep "1.2.1" > /dev/null && echo "$VERSIONS_OUTPUT" | grep "2.1.0" > /dev/null; then + echo "OK" +else + echo "FAILED" + FAILURES=$((FAILURES + 1)) +fi + +# List version contents (should show virtual commands) +echo -n "Testing list contract-upgrades/1.2.1... " +CONTENTS_OUTPUT=$(docker run --rm "$IMAGE_NAME" contract-upgrades/1.2.1 2>&1) +if echo "$CONTENTS_OUTPUT" | grep "deploy-execute-verify" > /dev/null; then + echo "OK" +else + echo "FAILED" + FAILURES=$((FAILURES + 1)) +fi + +# Test 5: File viewing +echo "" +echo "--- File Viewing ---" + +# View README +echo -n "Testing view README... " +README_OUTPUT=$(docker run --rm "$IMAGE_NAME" contract-upgrades/1.2.1/README.md 2>&1) +if echo "$README_OUTPUT" | grep -i "nitro" > /dev/null; then + echo "OK" +else + echo "FAILED" + FAILURES=$((FAILURES + 1)) +fi + +# View env template (1.2.1 has env-templates/) +echo -n "Testing view env template... " +ENV_OUTPUT=$(docker run --rm "$IMAGE_NAME" contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example 2>&1) +if echo "$ENV_OUTPUT" | grep "INBOX_ADDRESS" > /dev/null; then + echo "OK" +else + echo "FAILED" + FAILURES=$((FAILURES + 1)) +fi + +# View .env.sample (2.1.0+ has .env.sample) +echo -n "Testing view .env.sample... " +SAMPLE_OUTPUT=$(docker run --rm "$IMAGE_NAME" contract-upgrades/2.1.0/.env.sample 2>&1) +if echo "$SAMPLE_OUTPUT" | grep "UPGRADE_ACTION_ADDRESS" > /dev/null; then + echo "OK" +else + echo "FAILED" + FAILURES=$((FAILURES + 1)) +fi + +# Test 6: Help +echo "" +echo "--- Help ---" +run_test "help command" docker run --rm "$IMAGE_NAME" help + +# Test 7: Yarn scripts work echo "" echo "--- Yarn Scripts ---" run_test "yarn orbit:contracts:version --help" docker run --rm "$IMAGE_NAME" yarn orbit:contracts:version --help -# Test 6: Unit tests pass +# Test 8: Unit tests pass echo "" echo "--- Unit Tests ---" echo "Running unit tests inside container..." @@ -88,6 +132,30 @@ else FAILURES=$((FAILURES + 1)) fi +# Test 9: Dry run tests (requires .env file) +echo "" +echo "--- Dry Run Tests ---" + +# Create a temporary .env file for testing arbos +TEMP_ENV=$(mktemp) +cat > "$TEMP_ENV" <&1) +if echo "$DRYRUN_OUTPUT" | grep "Calldata:" > /dev/null; then + echo "OK" +else + echo "FAILED" + FAILURES=$((FAILURES + 1)) +fi + +rm -f "$TEMP_ENV" + # Summary echo "" echo "=== Summary ===" From 15b87cfc5a37167eca3b0d09cfb8181868df8969 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Feb 2026 11:06:01 +0000 Subject: [PATCH 08/21] feat: add Verify scripts for contract upgrades Add Verify*.s.sol Forge scripts to each contract-upgrade version folder, replacing hardcoded verification logic with discoverable scripts. - Add verify command to bin/contract-upgrade and bin/router - Create Verify scripts for 1.2.1, 2.1.0, 2.1.2, 2.1.3 - Update READMEs to reference forge script verification --- bin/contract-upgrade | 48 +++++++++++-------- bin/router | 3 +- .../foundry/contract-upgrades/1.2.1/README.md | 4 +- ...fyNitroContracts1Point2Point1Upgrade.s.sol | 21 ++++++++ .../foundry/contract-upgrades/2.1.0/README.md | 4 +- ...fyNitroContracts2Point1Point0Upgrade.s.sol | 21 ++++++++ .../foundry/contract-upgrades/2.1.2/README.md | 5 +- ...fyNitroContracts2Point1Point2Upgrade.s.sol | 25 ++++++++++ .../foundry/contract-upgrades/2.1.3/README.md | 5 +- ...fyNitroContracts2Point1Point3Upgrade.s.sol | 25 ++++++++++ 10 files changed, 129 insertions(+), 32 deletions(-) create mode 100644 scripts/foundry/contract-upgrades/1.2.1/VerifyNitroContracts1Point2Point1Upgrade.s.sol create mode 100644 scripts/foundry/contract-upgrades/2.1.0/VerifyNitroContracts2Point1Point0Upgrade.s.sol create mode 100644 scripts/foundry/contract-upgrades/2.1.2/VerifyNitroContracts2Point1Point2Upgrade.s.sol create mode 100644 scripts/foundry/contract-upgrades/2.1.3/VerifyNitroContracts2Point1Point3Upgrade.s.sol diff --git a/bin/contract-upgrade b/bin/contract-upgrade index f2344552..b7f9b305 100644 --- a/bin/contract-upgrade +++ b/bin/contract-upgrade @@ -62,6 +62,22 @@ cmd_execute() { eval $cmd } +cmd_verify() { + local version_dir="$1" + shift + + require_env PARENT_CHAIN_RPC + + local verify_script=$(find "$version_dir" -maxdepth 1 -name "Verify*.s.sol" -type f 2>/dev/null | head -1) + if [[ -z "$verify_script" ]]; then + die "No verify script found in $version_dir - check README for manual verification" + fi + + log "Running: $(basename "$verify_script")" + + forge script "$verify_script" --rpc-url "$PARENT_CHAIN_RPC" -vvv +} + cmd_deploy_execute_verify() { local version_dir="$1" local version=$(basename "$version_dir") @@ -168,26 +184,12 @@ cmd_deploy_execute_verify() { if [[ "$DRY_RUN" != "true" && "$SKIP_EXECUTE" != "true" ]]; then log "Step 3: Verifying upgrade..." - case "$version" in - 2.1.2|2.1.3) - local bridge - bridge=$(cast call --rpc-url "$PARENT_CHAIN_RPC" "$INBOX_ADDRESS" "bridge()(address)" 2>/dev/null || echo "") - if [[ -n "$bridge" && "$bridge" != "0x" ]]; then - local decimals - decimals=$(cast call --rpc-url "$PARENT_CHAIN_RPC" "$bridge" "nativeTokenDecimals()(uint8)" 2>/dev/null || echo "N/A") - log "Verification: nativeTokenDecimals = $decimals" - fi - ;; - 1.2.1|2.1.0) - if [[ -n "${ROLLUP:-}" ]]; then - local wasm_root - wasm_root=$(cast call --rpc-url "$PARENT_CHAIN_RPC" "$ROLLUP" "wasmModuleRoot()(bytes32)" 2>/dev/null || echo "N/A") - log "Verification: wasmModuleRoot = $wasm_root" - else - log "Verification: Set ROLLUP in .env to check wasmModuleRoot" - fi - ;; - esac + local verify_script=$(find "$version_dir" -maxdepth 1 -name "Verify*.s.sol" -type f 2>/dev/null | head -1) + if [[ -n "$verify_script" ]]; then + forge script "$verify_script" --rpc-url "$PARENT_CHAIN_RPC" -vvv + else + log "No Verify script found - check README for manual verification" + fi fi log "Done" @@ -204,6 +206,7 @@ Usage: contract-upgrade [options] Commands: deploy Run deploy script only execute Run execute script only + verify Run verify script only deploy-execute-verify Full upgrade flow (deploy, execute, verify) Options for deploy/execute: @@ -264,6 +267,9 @@ Available versions: $(ls "$CONTRACTS_DIR" 2>/dev/null | tr '\n' ' ' || echo "non execute) cmd_execute "$version_dir" "$@" ;; + verify) + cmd_verify "$version_dir" "$@" + ;; deploy-execute-verify) cmd_deploy_execute_verify "$version_dir" "$@" ;; @@ -273,7 +279,7 @@ Available versions: $(ls "$CONTRACTS_DIR" 2>/dev/null | tr '\n' ' ' || echo "non *) die "Unknown command: $command -Commands: deploy, execute, deploy-execute-verify" +Commands: deploy, execute, verify, deploy-execute-verify" ;; esac } diff --git a/bin/router b/bin/router index 609873a8..8b847226 100644 --- a/bin/router +++ b/bin/router @@ -36,6 +36,7 @@ list_directory() { echo "---" echo "deploy (run Deploy script)" echo "execute (run Execute script)" + echo "verify (run Verify script)" echo "deploy-execute-verify (full upgrade flow)" ;; arbos-upgrades/at-timestamp) @@ -140,7 +141,7 @@ main() { contract-upgrades/[0-9]*) local version=$(basename "$rel_parent") case "$basename" in - deploy|execute|deploy-execute-verify) + deploy|execute|verify|deploy-execute-verify) exec /app/bin/contract-upgrade "$version" "$basename" "$@" ;; esac diff --git a/scripts/foundry/contract-upgrades/1.2.1/README.md b/scripts/foundry/contract-upgrades/1.2.1/README.md index 4c9b75c2..78d7a63b 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/README.md +++ b/scripts/foundry/contract-upgrades/1.2.1/README.md @@ -57,7 +57,7 @@ forge script --sender $EXECUTOR --rpc-url $PARENT_CHAIN_RPC --broadcast ./Execut ``` If you have a multisig as executor, you can still run the above command without broadcasting to get the payload for the multisig transaction. -4. That's it, upgrade has been performed. You can make sure it has successfully executed by checking wasm module root: +4. That's it, upgrade has been performed. You can verify by running: ```bash -cast call --rpc-url $PARENT_CHAIN_RPC $ROLLUP "wasmModuleRoot()" +forge script --rpc-url $PARENT_CHAIN_RPC VerifyNitroContracts1Point2Point1Upgrade -vvv ``` diff --git a/scripts/foundry/contract-upgrades/1.2.1/VerifyNitroContracts1Point2Point1Upgrade.s.sol b/scripts/foundry/contract-upgrades/1.2.1/VerifyNitroContracts1Point2Point1Upgrade.s.sol new file mode 100644 index 00000000..00457948 --- /dev/null +++ b/scripts/foundry/contract-upgrades/1.2.1/VerifyNitroContracts1Point2Point1Upgrade.s.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "forge-std/Script.sol"; + +interface IRollupCore { + function wasmModuleRoot() external view returns (bytes32); +} + +/** + * @title VerifyNitroContracts1Point2Point1Upgrade + * @notice Verifies the upgrade to Nitro Contracts 1.2.1 by checking the wasmModuleRoot + */ +contract VerifyNitroContracts1Point2Point1Upgrade is Script { + function run() public view { + address rollup = vm.envAddress("ROLLUP"); + bytes32 wasmRoot = IRollupCore(rollup).wasmModuleRoot(); + console.log("wasmModuleRoot:"); + console.logBytes32(wasmRoot); + } +} diff --git a/scripts/foundry/contract-upgrades/2.1.0/README.md b/scripts/foundry/contract-upgrades/2.1.0/README.md index 99c055f5..1762eabe 100644 --- a/scripts/foundry/contract-upgrades/2.1.0/README.md +++ b/scripts/foundry/contract-upgrades/2.1.0/README.md @@ -68,10 +68,10 @@ forge script --sender $EXECUTOR --rpc-url $PARENT_CHAIN_RPC --broadcast ExecuteN If you have a multisig as executor, you can still run the above command without broadcasting to get the payload for the multisig transaction. -4. That's it, upgrade has been performed. You can make sure it has successfully executed by checking wasm module root: +4. That's it, upgrade has been performed. You can verify by running: ```bash -cast call --rpc-url $PARENT_CHAIN_RPC $ROLLUP "wasmModuleRoot()" +forge script --rpc-url $PARENT_CHAIN_RPC VerifyNitroContracts2Point1Point0Upgrade -vvv ``` ## FAQ diff --git a/scripts/foundry/contract-upgrades/2.1.0/VerifyNitroContracts2Point1Point0Upgrade.s.sol b/scripts/foundry/contract-upgrades/2.1.0/VerifyNitroContracts2Point1Point0Upgrade.s.sol new file mode 100644 index 00000000..4f2a8a63 --- /dev/null +++ b/scripts/foundry/contract-upgrades/2.1.0/VerifyNitroContracts2Point1Point0Upgrade.s.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "forge-std/Script.sol"; + +interface IRollupCore { + function wasmModuleRoot() external view returns (bytes32); +} + +/** + * @title VerifyNitroContracts2Point1Point0Upgrade + * @notice Verifies the upgrade to Nitro Contracts 2.1.0 by checking the wasmModuleRoot + */ +contract VerifyNitroContracts2Point1Point0Upgrade is Script { + function run() public view { + address rollup = vm.envAddress("ROLLUP"); + bytes32 wasmRoot = IRollupCore(rollup).wasmModuleRoot(); + console.log("wasmModuleRoot:"); + console.logBytes32(wasmRoot); + } +} diff --git a/scripts/foundry/contract-upgrades/2.1.2/README.md b/scripts/foundry/contract-upgrades/2.1.2/README.md index 16b04a6c..9d16797f 100644 --- a/scripts/foundry/contract-upgrades/2.1.2/README.md +++ b/scripts/foundry/contract-upgrades/2.1.2/README.md @@ -73,11 +73,10 @@ forge script --sender $EXECUTOR --rpc-url $PARENT_CHAIN_RPC --broadcast ExecuteN If you have a multisig as executor, you can still run the above command without broadcasting to get the payload for the multisig transaction. -4. That's it, upgrade has been performed. You can make sure it has successfully executed by checking the native token decimals. +4. That's it, upgrade has been performed. You can verify by running: ```bash -# should return 18 -cast call --rpc-url $PARENT_CHAIN_RPC $BRIDGE "nativeTokenDecimals()(uint8)" +forge script --rpc-url $PARENT_CHAIN_RPC VerifyNitroContracts2Point1Point2Upgrade -vvv ``` ## FAQ diff --git a/scripts/foundry/contract-upgrades/2.1.2/VerifyNitroContracts2Point1Point2Upgrade.s.sol b/scripts/foundry/contract-upgrades/2.1.2/VerifyNitroContracts2Point1Point2Upgrade.s.sol new file mode 100644 index 00000000..4e7b1691 --- /dev/null +++ b/scripts/foundry/contract-upgrades/2.1.2/VerifyNitroContracts2Point1Point2Upgrade.s.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "forge-std/Script.sol"; + +interface IInbox { + function bridge() external view returns (address); +} + +interface IBridge { + function nativeTokenDecimals() external view returns (uint8); +} + +/** + * @title VerifyNitroContracts2Point1Point2Upgrade + * @notice Verifies the upgrade to Nitro Contracts 2.1.2 by checking nativeTokenDecimals + */ +contract VerifyNitroContracts2Point1Point2Upgrade is Script { + function run() public view { + address inbox = vm.envAddress("INBOX_ADDRESS"); + address bridge = IInbox(inbox).bridge(); + uint8 decimals = IBridge(bridge).nativeTokenDecimals(); + console.log("nativeTokenDecimals:", decimals); + } +} diff --git a/scripts/foundry/contract-upgrades/2.1.3/README.md b/scripts/foundry/contract-upgrades/2.1.3/README.md index af8e4087..adff9181 100644 --- a/scripts/foundry/contract-upgrades/2.1.3/README.md +++ b/scripts/foundry/contract-upgrades/2.1.3/README.md @@ -72,11 +72,10 @@ forge script --sender $EXECUTOR --rpc-url $PARENT_CHAIN_RPC --broadcast ExecuteN If you have a multisig as executor, you can still run the above command without broadcasting to get the payload for the multisig transaction. -4. That's it, upgrade has been performed. You can make sure it has successfully executed by checking the native token decimals. +4. That's it, upgrade has been performed. You can verify by running: ```bash -# should return 18 -cast call --rpc-url $PARENT_CHAIN_RPC $BRIDGE "nativeTokenDecimals()(uint8)" +forge script --rpc-url $PARENT_CHAIN_RPC VerifyNitroContracts2Point1Point3Upgrade -vvv ``` ## FAQ diff --git a/scripts/foundry/contract-upgrades/2.1.3/VerifyNitroContracts2Point1Point3Upgrade.s.sol b/scripts/foundry/contract-upgrades/2.1.3/VerifyNitroContracts2Point1Point3Upgrade.s.sol new file mode 100644 index 00000000..353130df --- /dev/null +++ b/scripts/foundry/contract-upgrades/2.1.3/VerifyNitroContracts2Point1Point3Upgrade.s.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import "forge-std/Script.sol"; + +interface IInbox { + function bridge() external view returns (address); +} + +interface IBridge { + function nativeTokenDecimals() external view returns (uint8); +} + +/** + * @title VerifyNitroContracts2Point1Point3Upgrade + * @notice Verifies the upgrade to Nitro Contracts 2.1.3 by checking nativeTokenDecimals + */ +contract VerifyNitroContracts2Point1Point3Upgrade is Script { + function run() public view { + address inbox = vm.envAddress("INBOX_ADDRESS"); + address bridge = IInbox(inbox).bridge(); + uint8 decimals = IBridge(bridge).nativeTokenDecimals(); + console.log("nativeTokenDecimals:", decimals); + } +} From a8adfeb8e243ada18fefbf46eb3455a32f9951c0 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Feb 2026 11:23:10 +0000 Subject: [PATCH 09/21] ci: add Docker Hub publishing workflow Publish offchainlabs/chain-actions image to Docker Hub: - On push to main: tag as latest - On release tags (v*): tag as version (e.g., 1.2.3, 1.2) - Manual trigger: tag with branch name (for testing) Requires DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets. --- .github/workflows/publish-docker.yml | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/publish-docker.yml diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml new file mode 100644 index 00000000..d5e2d674 --- /dev/null +++ b/.github/workflows/publish-docker.yml @@ -0,0 +1,53 @@ +name: Publish Docker + +on: + push: + branches: [main] + tags: ['v*'] + workflow_dispatch: + +jobs: + publish: + name: Build and Push Docker Image + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + + - name: Install forge dependencies + run: forge install + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: offchainlabs/chain-actions + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=ref,event=branch + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From 4d391f93351512bde9043ec6125461a3797c4ed8 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Feb 2026 13:04:34 +0000 Subject: [PATCH 10/21] refactor: rewrite CLI in TypeScript Replace bash CLI (bin/router, bin/contract-upgrade, bin/arbos-upgrade, lib/common.sh) with TypeScript implementation in src/cli/. - Add commander for argument parsing - Add execa for subprocess execution - Update Dockerfile to use node entrypoint directly - Remove entrypoint.sh (no longer needed) --- .gitignore | 3 + Dockerfile | 8 +- bin/arbos-upgrade | 320 --------------------------- bin/contract-upgrade | 287 ------------------------ bin/router | 189 ---------------- entrypoint.sh | 4 - lib/common.sh | 135 ----------- package.json | 9 + src/cli/commands/arbos-upgrade.ts | 303 +++++++++++++++++++++++++ src/cli/commands/contract-upgrade.ts | 269 ++++++++++++++++++++++ src/cli/index.ts | 29 +++ src/cli/router.ts | 227 +++++++++++++++++++ src/cli/utils/auth.ts | 110 +++++++++ src/cli/utils/env.ts | 84 +++++++ src/cli/utils/forge.ts | 167 ++++++++++++++ src/cli/utils/log.ts | 20 ++ tsconfig.cli.json | 9 + yarn.lock | 106 ++++++++- 18 files changed, 1337 insertions(+), 942 deletions(-) delete mode 100644 bin/arbos-upgrade delete mode 100644 bin/contract-upgrade delete mode 100644 bin/router delete mode 100755 entrypoint.sh delete mode 100644 lib/common.sh create mode 100644 src/cli/commands/arbos-upgrade.ts create mode 100644 src/cli/commands/contract-upgrade.ts create mode 100644 src/cli/index.ts create mode 100644 src/cli/router.ts create mode 100644 src/cli/utils/auth.ts create mode 100644 src/cli/utils/env.ts create mode 100644 src/cli/utils/forge.ts create mode 100644 src/cli/utils/log.ts create mode 100644 tsconfig.cli.json diff --git a/.gitignore b/.gitignore index 973fff40..e00e0e6f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ node_modules .env .DS_Store +# TypeScript build output +/dist + # Hardhat files /cache /artifacts diff --git a/Dockerfile b/Dockerfile index 9d306176..b8fe17f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,8 +30,8 @@ COPY . . # Build contracts RUN forge build -# Make scripts executable -RUN chmod +x /app/entrypoint.sh /app/bin/* /app/lib/* +# Build CLI +RUN yarn build:cli -# Set entrypoint for command routing -ENTRYPOINT ["/app/entrypoint.sh"] +# Direct node entrypoint (no shell wrapper) +ENTRYPOINT ["node", "/app/dist/cli/index.js"] diff --git a/bin/arbos-upgrade b/bin/arbos-upgrade deleted file mode 100644 index 3cccd9a5..00000000 --- a/bin/arbos-upgrade +++ /dev/null @@ -1,320 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# ArbOS upgrades script for orbit-actions -# Handles deploy, execute, verify, and deploy-execute-verify commands - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/../lib/common.sh" - -ARBOS_DIR="/app/scripts/foundry/arbos-upgrades/at-timestamp" - -# ============================================================================= -# Commands -# ============================================================================= - -cmd_deploy() { - local version="$1" - shift - - parse_auth_args "$@" - - require_env CHILD_CHAIN_RPC - - export ARBOS_VERSION="$version" - - local deploy_script="$ARBOS_DIR/DeployUpgradeArbOSVersionAtTimestampAction.s.sol" - if [[ ! -f "$deploy_script" ]]; then - die "Deploy script not found: $deploy_script" - fi - - log "Running: $(basename "$deploy_script") for ArbOS $version" - - local cmd="forge script $deploy_script --rpc-url $CHILD_CHAIN_RPC --slow -vvv" - - if [[ -n "$AUTH_ARGS" ]]; then - cmd="$cmd --broadcast $AUTH_ARGS" - fi - - eval $cmd -} - -cmd_execute() { - shift # version not needed for execute - - parse_auth_args "$@" - - require_env CHILD_CHAIN_RPC - require_env CHILD_UPGRADE_EXECUTOR_ADDRESS - require_env UPGRADE_ACTION_ADDRESS - - log "Executing ArbOS upgrade action: $UPGRADE_ACTION_ADDRESS" - - local perform_calldata="0xb0a75d36" - - if [[ -z "$AUTH_ARGS" ]]; then - # No auth - output calldata for multisig - local execute_calldata - execute_calldata=$(cast calldata "execute(address,bytes)" "$UPGRADE_ACTION_ADDRESS" "$perform_calldata") - - log "Calldata for UpgradeExecutor.execute():" - echo "" - echo "To: $CHILD_UPGRADE_EXECUTOR_ADDRESS" - echo "Calldata: $execute_calldata" - echo "" - log "Submit this to your multisig/Safe to execute the upgrade" - else - local cast_cmd="cast send $CHILD_UPGRADE_EXECUTOR_ADDRESS \"execute(address,bytes)\" $UPGRADE_ACTION_ADDRESS $perform_calldata --rpc-url $CHILD_CHAIN_RPC $AUTH_ARGS" - - eval $cast_cmd - - log "ArbOS upgrade scheduled successfully" - fi -} - -cmd_verify() { - shift # version not needed for verify - - require_env CHILD_CHAIN_RPC - - log "Checking ArbOS upgrade status..." - - local scheduled - scheduled=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ - "0x000000000000000000000000000000000000006b" \ - "getScheduledUpgrade()(uint64,uint64)" 2>/dev/null || echo "N/A") - log "Scheduled upgrade (version, timestamp): $scheduled" - - local current_raw - current_raw=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ - "0x0000000000000000000000000000000000000064" \ - "arbOSVersion()(uint64)" 2>/dev/null || echo "0") - local current_version=$((current_raw - 55)) - log "Current ArbOS version: $current_version" -} - -cmd_deploy_execute_verify() { - local version="$1" - shift - - parse_deploy_execute_auth "$@" - - log "ArbOS version: $version" - - local deploy_script="$ARBOS_DIR/DeployUpgradeArbOSVersionAtTimestampAction.s.sol" - if [[ ! -f "$deploy_script" ]]; then - die "Deploy script not found: $deploy_script" - fi - - # Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set - local skip_deploy=false - if [[ -n "${UPGRADE_ACTION_ADDRESS:-}" ]]; then - skip_deploy=true - log "Using existing action from .env: $UPGRADE_ACTION_ADDRESS" - fi - - # Validate required env vars - require_env CHILD_CHAIN_RPC - require_env CHILD_UPGRADE_EXECUTOR_ADDRESS - require_env SCHEDULE_TIMESTAMP - - export ARBOS_VERSION="$version" - - # Validate auth - local deploy_auth=$(get_deploy_auth) - local execute_auth=$(get_execute_auth) - - if [[ "$skip_deploy" != "true" && "$DRY_RUN" != "true" && -z "$deploy_auth" ]]; then - die "Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive" - fi - if [[ "$SKIP_EXECUTE" != "true" && "$DRY_RUN" != "true" && -z "$execute_auth" ]]; then - die "Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive" - fi - - log "Scheduled timestamp: $SCHEDULE_TIMESTAMP" - - # Step 1: Deploy - local chain_id="" - if [[ "$skip_deploy" != "true" ]]; then - chain_id=$(get_chain_id "$CHILD_CHAIN_RPC") - log "Target chain ID: $chain_id" - log "Step 1: Deploying ArbOS upgrade action..." - - local forge_cmd="forge script $deploy_script --rpc-url $CHILD_CHAIN_RPC --slow -vvv" - - if [[ "$DRY_RUN" != "true" ]]; then - forge_cmd="$forge_cmd --broadcast $deploy_auth" - fi - if [[ "$VERIFY_CONTRACTS" == "true" ]]; then - forge_cmd="$forge_cmd --verify" - fi - - eval $forge_cmd - - if [[ "$DRY_RUN" != "true" ]]; then - UPGRADE_ACTION_ADDRESS=$(parse_action_address "$deploy_script" "$chain_id") - log "Deployed action at: $UPGRADE_ACTION_ADDRESS" - else - log "Dry run - no action deployed" - if [[ "$SKIP_EXECUTE" != "true" ]]; then - log "Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step" - return 0 - fi - fi - else - log "Step 1: Skipped deploy" - fi - - # Step 2: Execute via cast send - if [[ "$SKIP_EXECUTE" != "true" ]]; then - log "Step 2: Executing ArbOS upgrade..." - - local perform_calldata="0xb0a75d36" - - if [[ "$DRY_RUN" == "true" ]]; then - local execute_calldata - execute_calldata=$(cast calldata "execute(address,bytes)" "$UPGRADE_ACTION_ADDRESS" "$perform_calldata") - - log "Dry run - calldata for UpgradeExecutor.execute():" - echo "" - echo "To: $CHILD_UPGRADE_EXECUTOR_ADDRESS" - echo "Calldata: $execute_calldata" - echo "" - log "Submit this to your multisig/Safe to execute the upgrade" - else - local cast_cmd="cast send $CHILD_UPGRADE_EXECUTOR_ADDRESS \"execute(address,bytes)\" $UPGRADE_ACTION_ADDRESS $perform_calldata --rpc-url $CHILD_CHAIN_RPC" - - if [[ -n "$EXECUTE_KEY" ]]; then - cast_cmd="$cast_cmd --private-key $EXECUTE_KEY" - elif [[ -n "$EXECUTE_ACCOUNT" ]]; then - cast_cmd="$cast_cmd --account $EXECUTE_ACCOUNT" - elif [[ "$EXECUTE_LEDGER" == "true" ]]; then - cast_cmd="$cast_cmd --ledger" - elif [[ "$EXECUTE_INTERACTIVE" == "true" ]]; then - cast_cmd="$cast_cmd --interactive" - fi - - eval $cast_cmd - - log "ArbOS upgrade scheduled successfully" - fi - else - log "Step 2: Skipped execute" - fi - - # Step 3: Verify - if [[ "$DRY_RUN" != "true" && "$SKIP_EXECUTE" != "true" ]]; then - log "Step 3: Verifying scheduled upgrade..." - - local scheduled - scheduled=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ - "0x000000000000000000000000000000000000006b" \ - "getScheduledUpgrade()(uint64,uint64)" 2>/dev/null || echo "N/A") - log "Scheduled upgrade (version, timestamp): $scheduled" - - local current_raw - current_raw=$(cast call --rpc-url "$CHILD_CHAIN_RPC" \ - "0x0000000000000000000000000000000000000064" \ - "arbOSVersion()(uint64)" 2>/dev/null || echo "0") - local current_version=$((current_raw - 55)) - log "Current ArbOS version: $current_version" - fi - - log "Done" -} - -# ============================================================================= -# Main -# ============================================================================= - -show_help() { - cat <<'EOF' -Usage: arbos-upgrade [options] - -Commands: - deploy Run deploy script only - execute Execute upgrade action (schedule the upgrade) - verify Check scheduled upgrade status - deploy-execute-verify Full upgrade flow (deploy, execute, verify) - -Options for deploy: - --private-key KEY Private key - --account NAME Keystore account - --ledger Use Ledger - --interactive Prompt for key - -Options for deploy-execute-verify: - --deploy-key KEY Private key for deploy step - --deploy-account NAME Keystore account for deploy - --deploy-ledger Use Ledger for deploy - --execute-key KEY Private key for execute step - --execute-account NAME Keystore account for execute - --execute-ledger Use Ledger for execute - --dry-run, -n Simulate without broadcasting - --skip-execute Deploy only - --verify, -v Verify on block explorer - -Required .env variables: - CHILD_CHAIN_RPC Child chain RPC URL - CHILD_UPGRADE_EXECUTOR_ADDRESS UpgradeExecutor address - SCHEDULE_TIMESTAMP Unix timestamp for upgrade - -Optional .env variables: - ARBOS_VERSION ArbOS version (alternative to positional arg) - UPGRADE_ACTION_ADDRESS Pre-deployed action (skips deploy) -EOF -} - -main() { - if [[ $# -lt 1 ]]; then - show_help - exit 1 - fi - - local version="$1" - shift - - if [[ "$version" == "--help" || "$version" == "-h" ]]; then - show_help - exit 0 - fi - - # Version can also come from env - if [[ -z "$version" && -n "${ARBOS_VERSION:-}" ]]; then - version="$ARBOS_VERSION" - fi - - if [[ -z "$version" ]]; then - die "ArbOS version required" - fi - - local command="${1:-deploy-execute-verify}" - if [[ $# -gt 0 ]]; then - shift - fi - - case "$command" in - deploy) - cmd_deploy "$version" "$@" - ;; - execute) - cmd_execute "$version" "$@" - ;; - verify) - cmd_verify "$version" "$@" - ;; - deploy-execute-verify) - cmd_deploy_execute_verify "$version" "$@" - ;; - --help|-h) - show_help - ;; - *) - die "Unknown command: $command - -Commands: deploy, execute, verify, deploy-execute-verify" - ;; - esac -} - -main "$@" diff --git a/bin/contract-upgrade b/bin/contract-upgrade deleted file mode 100644 index b7f9b305..00000000 --- a/bin/contract-upgrade +++ /dev/null @@ -1,287 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Contract upgrades script for orbit-actions -# Handles deploy, execute, and deploy-execute-verify commands - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/../lib/common.sh" - -CONTRACTS_DIR="/app/scripts/foundry/contract-upgrades" - -# ============================================================================= -# Commands -# ============================================================================= - -cmd_deploy() { - local version_dir="$1" - shift - - parse_auth_args "$@" - - require_env PARENT_CHAIN_RPC - - local deploy_script=$(find "$version_dir" -maxdepth 1 -name "Deploy*.s.sol" -type f 2>/dev/null | head -1) - if [[ -z "$deploy_script" ]]; then - die "No deploy script found in $version_dir" - fi - - log "Running: $(basename "$deploy_script")" - - local cmd="forge script $deploy_script --rpc-url $PARENT_CHAIN_RPC --slow -vvv --skip-simulation" - - if [[ -n "$AUTH_ARGS" ]]; then - cmd="$cmd --broadcast $AUTH_ARGS" - fi - - eval $cmd -} - -cmd_execute() { - local version_dir="$1" - shift - - parse_auth_args "$@" - - require_env PARENT_CHAIN_RPC - require_env UPGRADE_ACTION_ADDRESS - - local execute_script=$(find "$version_dir" -maxdepth 1 -name "Execute*.s.sol" -type f 2>/dev/null | head -1) - if [[ -z "$execute_script" ]]; then - die "No execute script found in $version_dir" - fi - - log "Running: $(basename "$execute_script")" - - local cmd="forge script $execute_script --rpc-url $PARENT_CHAIN_RPC -vvv" - - if [[ -n "$AUTH_ARGS" ]]; then - cmd="$cmd --broadcast $AUTH_ARGS" - fi - - eval $cmd -} - -cmd_verify() { - local version_dir="$1" - shift - - require_env PARENT_CHAIN_RPC - - local verify_script=$(find "$version_dir" -maxdepth 1 -name "Verify*.s.sol" -type f 2>/dev/null | head -1) - if [[ -z "$verify_script" ]]; then - die "No verify script found in $version_dir - check README for manual verification" - fi - - log "Running: $(basename "$verify_script")" - - forge script "$verify_script" --rpc-url "$PARENT_CHAIN_RPC" -vvv -} - -cmd_deploy_execute_verify() { - local version_dir="$1" - local version=$(basename "$version_dir") - shift - - parse_deploy_execute_auth "$@" - - # Find scripts - local deploy_script=$(find "$version_dir" -maxdepth 1 -name "Deploy*.s.sol" -type f 2>/dev/null | head -1) - local execute_script=$(find "$version_dir" -maxdepth 1 -name "Execute*.s.sol" -type f 2>/dev/null | head -1) - - if [[ -z "$deploy_script" ]]; then - die "No deploy script found in $version_dir" - fi - if [[ -z "$execute_script" ]]; then - die "No execute script found in $version_dir" - fi - - log "Version: $version" - log "Deploy script: $(basename "$deploy_script")" - log "Execute script: $(basename "$execute_script")" - - # Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set - local skip_deploy=false - if [[ -n "${UPGRADE_ACTION_ADDRESS:-}" ]]; then - skip_deploy=true - log "Using existing action from .env: $UPGRADE_ACTION_ADDRESS" - fi - - # Validate required env vars - require_env PARENT_CHAIN_RPC - require_env INBOX_ADDRESS - require_env PROXY_ADMIN_ADDRESS - require_env PARENT_UPGRADE_EXECUTOR_ADDRESS - - # Validate auth - local deploy_auth=$(get_deploy_auth) - local execute_auth=$(get_execute_auth) - - if [[ "$skip_deploy" != "true" && "$DRY_RUN" != "true" && -z "$deploy_auth" ]]; then - die "Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive" - fi - if [[ "$SKIP_EXECUTE" != "true" && "$DRY_RUN" != "true" && -z "$execute_auth" ]]; then - die "Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive" - fi - - local chain_id - chain_id=$(get_chain_id "$PARENT_CHAIN_RPC") - log "Target chain ID: $chain_id" - - # Step 1: Deploy - if [[ "$skip_deploy" != "true" ]]; then - log "Step 1: Deploying upgrade action..." - - local forge_cmd="forge script $deploy_script --rpc-url $PARENT_CHAIN_RPC --slow -vvv --skip-simulation" - - if [[ "$DRY_RUN" != "true" ]]; then - forge_cmd="$forge_cmd --broadcast $deploy_auth" - fi - if [[ "$VERIFY_CONTRACTS" == "true" ]]; then - forge_cmd="$forge_cmd --verify" - fi - - eval $forge_cmd - - if [[ "$DRY_RUN" != "true" ]]; then - UPGRADE_ACTION_ADDRESS=$(parse_action_address "$deploy_script" "$chain_id") - log "Deployed action at: $UPGRADE_ACTION_ADDRESS" - else - log "Dry run - no action deployed" - if [[ "$SKIP_EXECUTE" != "true" ]]; then - log "Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step" - return 0 - fi - fi - else - log "Step 1: Skipped deploy" - fi - - export UPGRADE_ACTION_ADDRESS - - # Step 2: Execute - if [[ "$SKIP_EXECUTE" != "true" ]]; then - log "Step 2: Executing upgrade..." - - local forge_cmd="forge script $execute_script --rpc-url $PARENT_CHAIN_RPC -vvv" - - if [[ "$DRY_RUN" != "true" ]]; then - forge_cmd="$forge_cmd --broadcast $execute_auth" - fi - - eval $forge_cmd - - if [[ "$DRY_RUN" == "true" ]]; then - log "Dry run - upgrade not executed" - else - log "Upgrade executed successfully" - fi - else - log "Step 2: Skipped execute" - fi - - # Step 3: Verify - if [[ "$DRY_RUN" != "true" && "$SKIP_EXECUTE" != "true" ]]; then - log "Step 3: Verifying upgrade..." - - local verify_script=$(find "$version_dir" -maxdepth 1 -name "Verify*.s.sol" -type f 2>/dev/null | head -1) - if [[ -n "$verify_script" ]]; then - forge script "$verify_script" --rpc-url "$PARENT_CHAIN_RPC" -vvv - else - log "No Verify script found - check README for manual verification" - fi - fi - - log "Done" -} - -# ============================================================================= -# Main -# ============================================================================= - -show_help() { - cat <<'EOF' -Usage: contract-upgrade [options] - -Commands: - deploy Run deploy script only - execute Run execute script only - verify Run verify script only - deploy-execute-verify Full upgrade flow (deploy, execute, verify) - -Options for deploy/execute: - --private-key KEY Private key - --account NAME Keystore account - --ledger Use Ledger - --interactive Prompt for key - -Options for deploy-execute-verify: - --deploy-key KEY Private key for deploy step - --deploy-account NAME Keystore account for deploy - --deploy-ledger Use Ledger for deploy - --execute-key KEY Private key for execute step - --execute-account NAME Keystore account for execute - --execute-ledger Use Ledger for execute - --dry-run, -n Simulate without broadcasting - --skip-execute Deploy only - --verify, -v Verify on block explorer - -Required .env variables: - PARENT_CHAIN_RPC Parent chain RPC URL - INBOX_ADDRESS Inbox contract address - PROXY_ADMIN_ADDRESS ProxyAdmin address - PARENT_UPGRADE_EXECUTOR_ADDRESS UpgradeExecutor address - -Optional .env variables: - UPGRADE_ACTION_ADDRESS Pre-deployed action (skips deploy) - ROLLUP Rollup address (for verification) -EOF -} - -main() { - if [[ $# -lt 2 ]]; then - show_help - exit 1 - fi - - local version="$1" - local command="$2" - shift 2 - - if [[ "$version" == "--help" || "$version" == "-h" ]]; then - show_help - exit 0 - fi - - local version_dir="$CONTRACTS_DIR/$version" - if [[ ! -d "$version_dir" ]]; then - die "Unknown version: $version - -Available versions: $(ls "$CONTRACTS_DIR" 2>/dev/null | tr '\n' ' ' || echo "none found")" - fi - - case "$command" in - deploy) - cmd_deploy "$version_dir" "$@" - ;; - execute) - cmd_execute "$version_dir" "$@" - ;; - verify) - cmd_verify "$version_dir" "$@" - ;; - deploy-execute-verify) - cmd_deploy_execute_verify "$version_dir" "$@" - ;; - --help|-h) - show_help - ;; - *) - die "Unknown command: $command - -Commands: deploy, execute, verify, deploy-execute-verify" - ;; - esac -} - -main "$@" diff --git a/bin/router b/bin/router deleted file mode 100644 index 8b847226..00000000 --- a/bin/router +++ /dev/null @@ -1,189 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# ============================================================================= -# orbit-actions router -# Handles path parsing, directory listing, file viewing, and command dispatch -# ============================================================================= - -SCRIPTS_DIR="/app/scripts/foundry" - -# ============================================================================= -# Utility Functions -# ============================================================================= - -die() { - echo "Error: $1" >&2 - exit 1 -} - -# ============================================================================= -# Directory Listing -# ============================================================================= - -list_directory() { - local dir="$1" - local rel_path="${dir#$SCRIPTS_DIR/}" - # Normalize: remove leading ./ - rel_path="${rel_path#./}" - - # List actual contents - ls -1 "$dir" - - # Add virtual commands based on directory type - case "$rel_path" in - contract-upgrades/[0-9]*) - echo "---" - echo "deploy (run Deploy script)" - echo "execute (run Execute script)" - echo "verify (run Verify script)" - echo "deploy-execute-verify (full upgrade flow)" - ;; - arbos-upgrades/at-timestamp) - echo "---" - echo "deploy (run Deploy script)" - echo "execute (execute upgrade action)" - echo "verify (check upgrade status)" - echo "deploy-execute-verify (full upgrade flow)" - ;; - esac -} - -# ============================================================================= -# Help -# ============================================================================= - -show_help() { - cat <<'EOF' -Usage: docker run orbit-actions [path] [args...] - -Browse and execute scripts from the foundry scripts directory. - -Browsing: - . List top-level directories - contract-upgrades List available versions - contract-upgrades/1.2.1 List version contents + commands - contract-upgrades/1.2.1/env-templates List env templates - -Viewing files: - contract-upgrades/1.2.1/README.md View README - contract-upgrades/1.2.1/env-templates/.env.example View env template - contract-upgrades/2.1.0/.env.sample View env sample - -Running upgrade scripts: - contract-upgrades//deploy [--private-key KEY] - contract-upgrades//execute [--private-key KEY] - contract-upgrades//deploy-execute-verify [--deploy-key KEY] [--execute-key KEY] - - arbos-upgrades/at-timestamp/deploy [--private-key KEY] - arbos-upgrades/at-timestamp/deploy-execute-verify [--deploy-key KEY] [--execute-key KEY] - -Options for deploy-execute-verify: - --deploy-key KEY Private key for deploy step - --deploy-account NAME Keystore account for deploy - --deploy-ledger Use Ledger for deploy - --execute-key KEY Private key for execute step - --execute-account NAME Keystore account for execute - --execute-ledger Use Ledger for execute - --dry-run, -n Simulate without broadcasting - --skip-execute Deploy only - --verify, -v Verify on block explorer - -Passthrough commands: - forge ... Run forge directly - cast ... Run cast directly - yarn ... Run yarn directly - -Examples: - docker run orbit-actions contract-upgrades/1.2.1 - docker run orbit-actions contract-upgrades/1.2.1/README.md - docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/deploy-execute-verify --dry-run -EOF -} - -# ============================================================================= -# Main Router -# ============================================================================= - -main() { - # No args - list top level - if [[ $# -eq 0 ]]; then - ls -1 "$SCRIPTS_DIR" - exit 0 - fi - - local path="$1" - shift - - # Help - if [[ "$path" == "help" || "$path" == "--help" || "$path" == "-h" ]]; then - show_help - exit 0 - fi - - # Passthrough: forge, cast, yarn, node, bash, etc. - if command -v "$path" &>/dev/null && [[ ! -e "$SCRIPTS_DIR/$path" ]]; then - exec "$path" "$@" - fi - - # Parse path for virtual commands - local full_path="$SCRIPTS_DIR/$path" - local parent_path=$(dirname "$full_path") - local basename=$(basename "$path") - - # Check for virtual commands (deploy, execute, deploy-execute-verify) - if [[ ! -e "$full_path" && -d "$parent_path" ]]; then - local rel_parent="${parent_path#$SCRIPTS_DIR/}" - # Normalize: remove leading ./ - rel_parent="${rel_parent#./}" - - case "$rel_parent" in - contract-upgrades/[0-9]*) - local version=$(basename "$rel_parent") - case "$basename" in - deploy|execute|verify|deploy-execute-verify) - exec /app/bin/contract-upgrade "$version" "$basename" "$@" - ;; - esac - ;; - arbos-upgrades/at-timestamp) - case "$basename" in - deploy|deploy-execute-verify) - # These commands need a version argument - local version="${1:-}" - if [[ -z "$version" ]]; then - echo "Error: ArbOS version required" >&2 - echo "Usage: arbos-upgrades/at-timestamp/$basename [options]" >&2 - exit 1 - fi - shift - exec /app/bin/arbos-upgrade "$version" "$basename" "$@" - ;; - execute|verify) - # These commands don't need a version argument - exec /app/bin/arbos-upgrade "" "$basename" "$@" - ;; - esac - ;; - esac - fi - - # Directory - list contents - if [[ -d "$full_path" ]]; then - list_directory "$full_path" - exit 0 - fi - - # Regular file - cat it - if [[ -f "$full_path" ]]; then - cat "$full_path" - exit 0 - fi - - # Not found - die "Not found: $path - -Use 'help' to see available commands." -} - -main "$@" diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100755 index 965dfa52..00000000 --- a/entrypoint.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -# Source .env if mounted, then delegate to router -[[ -f /app/.env ]] && set -a && source /app/.env && set +a -exec /app/bin/router "$@" diff --git a/lib/common.sh b/lib/common.sh deleted file mode 100644 index 9a4ac560..00000000 --- a/lib/common.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash -# Shared utilities for upgrade scripts - -# ============================================================================= -# Utility Functions -# ============================================================================= - -die() { - echo "Error: $1" >&2 - exit 1 -} - -log() { - echo "[orbit-actions] $1" -} - -require_env() { - local name="$1" - local value="${!name:-}" - if [[ -z "$value" ]]; then - die "Required env var not set: $name (check your .env file)" - fi -} - -# ============================================================================= -# Auth Helpers -# ============================================================================= - -# Build forge/cast auth args from CLI flags -# Sets global: AUTH_ARGS -parse_auth_args() { - AUTH_ARGS="" - while [[ $# -gt 0 ]]; do - case "$1" in - --private-key|--account) - AUTH_ARGS="$1 $2" - shift 2 - ;; - --ledger|--interactive) - AUTH_ARGS="$1" - shift - ;; - *) - shift - ;; - esac - done -} - -# Build auth args for deploy step (from --deploy-* flags) -get_deploy_auth() { - if [[ -n "${DEPLOY_KEY:-}" ]]; then - echo "--private-key $DEPLOY_KEY" - elif [[ -n "${DEPLOY_ACCOUNT:-}" ]]; then - echo "--account $DEPLOY_ACCOUNT" - elif [[ "${DEPLOY_LEDGER:-}" == "true" ]]; then - echo "--ledger" - elif [[ "${DEPLOY_INTERACTIVE:-}" == "true" ]]; then - echo "--interactive" - fi -} - -# Build auth args for execute step (from --execute-* flags) -get_execute_auth() { - if [[ -n "${EXECUTE_KEY:-}" ]]; then - echo "--private-key $EXECUTE_KEY" - elif [[ -n "${EXECUTE_ACCOUNT:-}" ]]; then - echo "--account $EXECUTE_ACCOUNT" - elif [[ "${EXECUTE_LEDGER:-}" == "true" ]]; then - echo "--ledger" - elif [[ "${EXECUTE_INTERACTIVE:-}" == "true" ]]; then - echo "--interactive" - fi -} - -# Parse --deploy-* and --execute-* flags into variables -parse_deploy_execute_auth() { - DEPLOY_KEY="" - DEPLOY_ACCOUNT="" - DEPLOY_LEDGER=false - DEPLOY_INTERACTIVE=false - EXECUTE_KEY="" - EXECUTE_ACCOUNT="" - EXECUTE_LEDGER=false - EXECUTE_INTERACTIVE=false - DRY_RUN=false - SKIP_EXECUTE=false - VERIFY_CONTRACTS=false - REMAINING_ARGS=() - - while [[ $# -gt 0 ]]; do - case "$1" in - --deploy-key) DEPLOY_KEY="$2"; shift 2 ;; - --deploy-account) DEPLOY_ACCOUNT="$2"; shift 2 ;; - --deploy-ledger) DEPLOY_LEDGER=true; shift ;; - --deploy-interactive) DEPLOY_INTERACTIVE=true; shift ;; - --execute-key) EXECUTE_KEY="$2"; shift 2 ;; - --execute-account) EXECUTE_ACCOUNT="$2"; shift 2 ;; - --execute-ledger) EXECUTE_LEDGER=true; shift ;; - --execute-interactive) EXECUTE_INTERACTIVE=true; shift ;; - --dry-run|-n) DRY_RUN=true; shift ;; - --skip-execute) SKIP_EXECUTE=true; shift ;; - --verify|-v) VERIFY_CONTRACTS=true; shift ;; - *) REMAINING_ARGS+=("$1"); shift ;; - esac - done -} - -# ============================================================================= -# Forge Script Helpers -# ============================================================================= - -get_chain_id() { - local rpc="$1" - cast chain-id --rpc-url "$rpc" -} - -parse_action_address() { - local script_path="$1" - local chain_id="$2" - local script_name=$(basename "$script_path") - local broadcast_file="/app/broadcast/${script_name}/${chain_id}/run-latest.json" - - if [[ ! -f "$broadcast_file" ]]; then - die "Broadcast file not found: $broadcast_file" - fi - - local address=$(jq -r '.transactions | map(select(.transactionType == "CREATE")) | last | .contractAddress' "$broadcast_file") - - if [[ -z "$address" || "$address" == "null" ]]; then - die "Could not parse action address from broadcast file" - fi - - echo "$address" -} diff --git a/package.json b/package.json index 9c12cdb1..69e020fa 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,12 @@ "version": "1.0.0", "repository": "https://github.com/OffchainLabs/blockchain-eng-template.git", "license": "Apache 2.0", + "bin": { + "orbit-actions": "./dist/cli/index.js" + }, "scripts": { + "build:cli": "tsc -p tsconfig.cli.json", + "cli": "ts-node src/cli/index.ts", "prepare": "forge install && cd lib/arbitrum-sdk && yarn", "minimal-publish": "./scripts/publish.bash", "minimal-install": "yarn --ignore-scripts && forge install", @@ -62,5 +67,9 @@ "ts-node": ">=8.0.0", "typechain": "^8.3.0", "typescript": ">=4.5.0" + }, + "dependencies": { + "commander": "^12.0.0", + "execa": "^5.1.1" } } diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts new file mode 100644 index 00000000..6724cacf --- /dev/null +++ b/src/cli/commands/arbos-upgrade.ts @@ -0,0 +1,303 @@ +/** + * ArbOS upgrade commands + */ + +import { Command } from 'commander'; +import * as path from 'path'; +import * as fs from 'fs'; +import { log, die } from '../utils/log'; +import { requireEnv, getEnv, getScriptsDir } from '../utils/env'; +import { parseAuthArgs, createDeployExecuteAuth, getDeployAuth, getExecuteAuth } from '../utils/auth'; +import { + runForgeScript, + runCastSend, + runCastCall, + castCalldata, + getChainId, + parseActionAddress, +} from '../utils/forge'; + +const ARBOS_DIR = path.join(getScriptsDir(), 'arbos-upgrades', 'at-timestamp'); +const DEPLOY_SCRIPT = path.join(ARBOS_DIR, 'DeployUpgradeArbOSVersionAtTimestampAction.s.sol'); + +async function cmdDeploy(version: string, args: string[]): Promise { + const authArgs = parseAuthArgs(args); + + const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); + + if (!fs.existsSync(DEPLOY_SCRIPT)) { + die(`Deploy script not found: ${DEPLOY_SCRIPT}`); + } + + // Export version for the script + process.env.ARBOS_VERSION = version; + + log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`); + + await runForgeScript({ + script: DEPLOY_SCRIPT, + rpcUrl, + authArgs, + broadcast: !!authArgs, + slow: true, + }); +} + +async function cmdExecute(args: string[]): Promise { + const authArgs = parseAuthArgs(args); + + const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); + const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS'); + const actionAddress = requireEnv('UPGRADE_ACTION_ADDRESS'); + + log(`Executing ArbOS upgrade action: ${actionAddress}`); + + const performCalldata = '0xb0a75d36'; // perform() selector + + if (!authArgs) { + // No auth - output calldata for multisig + const executeCalldata = await castCalldata('execute(address,bytes)', actionAddress, performCalldata); + + log('Calldata for UpgradeExecutor.execute():'); + console.log(''); + console.log(`To: ${upgradeExecutor}`); + console.log(`Calldata: ${executeCalldata}`); + console.log(''); + log('Submit this to your multisig/Safe to execute the upgrade'); + } else { + await runCastSend({ + to: upgradeExecutor, + sig: 'execute(address,bytes)', + args: [actionAddress, performCalldata], + rpcUrl, + authArgs, + }); + + log('ArbOS upgrade scheduled successfully'); + } +} + +async function cmdVerify(): Promise { + const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); + + log('Checking ArbOS upgrade status...'); + + const scheduled = await runCastCall({ + to: '0x000000000000000000000000000000000000006b', + sig: 'getScheduledUpgrade()(uint64,uint64)', + rpcUrl, + }); + log(`Scheduled upgrade (version, timestamp): ${scheduled}`); + + const currentRaw = await runCastCall({ + to: '0x0000000000000000000000000000000000000064', + sig: 'arbOSVersion()(uint64)', + rpcUrl, + }); + + let currentVersion: number; + if (currentRaw === 'N/A') { + currentVersion = 0; + } else { + const rawNum = parseInt(currentRaw, 10); + currentVersion = rawNum - 55; + } + + log(`Current ArbOS version: ${currentVersion}`); +} + +async function cmdDeployExecuteVerify( + version: string, + options: { + deployKey?: string; + deployAccount?: string; + deployLedger?: boolean; + deployInteractive?: boolean; + executeKey?: string; + executeAccount?: string; + executeLedger?: boolean; + executeInteractive?: boolean; + dryRun?: boolean; + skipExecute?: boolean; + verify?: boolean; + } +): Promise { + const auth = createDeployExecuteAuth(options); + + log(`ArbOS version: ${version}`); + + if (!fs.existsSync(DEPLOY_SCRIPT)) { + die(`Deploy script not found: ${DEPLOY_SCRIPT}`); + } + + // Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set + let skipDeploy = !!getEnv('UPGRADE_ACTION_ADDRESS'); + let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || ''; + + if (skipDeploy) { + log(`Using existing action from .env: ${upgradeActionAddress}`); + } + + // Validate required env vars + const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); + const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS'); + requireEnv('SCHEDULE_TIMESTAMP'); + + // Export version for the script + process.env.ARBOS_VERSION = version; + + // Validate auth + const deployAuth = getDeployAuth(auth); + const executeAuth = getExecuteAuth(auth); + + if (!skipDeploy && !auth.dryRun && !deployAuth) { + die('Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive'); + } + if (!auth.skipExecute && !auth.dryRun && !executeAuth) { + die('Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive'); + } + + log(`Scheduled timestamp: ${process.env.SCHEDULE_TIMESTAMP}`); + + // Step 1: Deploy + let chainId = ''; + if (!skipDeploy) { + chainId = await getChainId(rpcUrl); + log(`Target chain ID: ${chainId}`); + log('Step 1: Deploying ArbOS upgrade action...'); + + await runForgeScript({ + script: DEPLOY_SCRIPT, + rpcUrl, + authArgs: deployAuth, + broadcast: !auth.dryRun, + verify: auth.verifyContracts, + slow: true, + }); + + if (!auth.dryRun) { + upgradeActionAddress = parseActionAddress(DEPLOY_SCRIPT, chainId); + log(`Deployed action at: ${upgradeActionAddress}`); + } else { + log('Dry run - no action deployed'); + if (!auth.skipExecute) { + log('Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step'); + return; + } + } + } else { + log('Step 1: Skipped deploy'); + } + + // Step 2: Execute via cast send + if (!auth.skipExecute) { + log('Step 2: Executing ArbOS upgrade...'); + + const performCalldata = '0xb0a75d36'; + + if (auth.dryRun) { + const executeCalldata = await castCalldata('execute(address,bytes)', upgradeActionAddress, performCalldata); + + log('Dry run - calldata for UpgradeExecutor.execute():'); + console.log(''); + console.log(`To: ${upgradeExecutor}`); + console.log(`Calldata: ${executeCalldata}`); + console.log(''); + log('Submit this to your multisig/Safe to execute the upgrade'); + } else { + await runCastSend({ + to: upgradeExecutor, + sig: 'execute(address,bytes)', + args: [upgradeActionAddress, performCalldata], + rpcUrl, + authArgs: executeAuth, + }); + + log('ArbOS upgrade scheduled successfully'); + } + } else { + log('Step 2: Skipped execute'); + } + + // Step 3: Verify + if (!auth.dryRun && !auth.skipExecute) { + log('Step 3: Verifying scheduled upgrade...'); + + const scheduled = await runCastCall({ + to: '0x000000000000000000000000000000000000006b', + sig: 'getScheduledUpgrade()(uint64,uint64)', + rpcUrl, + }); + log(`Scheduled upgrade (version, timestamp): ${scheduled}`); + + const currentRaw = await runCastCall({ + to: '0x0000000000000000000000000000000000000064', + sig: 'arbOSVersion()(uint64)', + rpcUrl, + }); + + let currentVersion: number; + if (currentRaw === 'N/A') { + currentVersion = 0; + } else { + const rawNum = parseInt(currentRaw, 10); + currentVersion = rawNum - 55; + } + + log(`Current ArbOS version: ${currentVersion}`); + } + + log('Done'); +} + +export function createArbosUpgradeCommand(): Command { + const cmd = new Command('arbos-upgrade') + .description('ArbOS upgrade operations') + .argument('', 'ArbOS version number') + .argument('[command]', 'Command: deploy, execute, verify, deploy-execute-verify', 'deploy-execute-verify') + .option('--private-key ', 'Private key (for deploy/execute)') + .option('--account ', 'Keystore account (for deploy/execute)') + .option('--ledger', 'Use Ledger (for deploy/execute)') + .option('--interactive', 'Prompt for key (for deploy/execute)') + .option('--deploy-key ', 'Private key for deploy step') + .option('--deploy-account ', 'Keystore account for deploy') + .option('--deploy-ledger', 'Use Ledger for deploy') + .option('--deploy-interactive', 'Prompt for key for deploy') + .option('--execute-key ', 'Private key for execute step') + .option('--execute-account ', 'Keystore account for execute') + .option('--execute-ledger', 'Use Ledger for execute') + .option('--execute-interactive', 'Prompt for key for execute') + .option('-n, --dry-run', 'Simulate without broadcasting') + .option('--skip-execute', 'Deploy only') + .option('-v, --verify', 'Verify on block explorer') + .action(async (version: string, command: string, options) => { + // Build args array from remaining options for simple commands + const args: string[] = []; + if (options.privateKey) args.push('--private-key', options.privateKey); + if (options.account) args.push('--account', options.account); + if (options.ledger) args.push('--ledger'); + if (options.interactive) args.push('--interactive'); + + switch (command) { + case 'deploy': + await cmdDeploy(version, args); + break; + case 'execute': + await cmdExecute(args); + break; + case 'verify': + await cmdVerify(); + break; + case 'deploy-execute-verify': + await cmdDeployExecuteVerify(version, options); + break; + default: + die(`Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify`); + } + }); + + return cmd; +} + +// Export individual functions for use by router +export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify }; diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts new file mode 100644 index 00000000..7aa66d17 --- /dev/null +++ b/src/cli/commands/contract-upgrade.ts @@ -0,0 +1,269 @@ +/** + * Contract upgrade commands + */ + +import { Command } from 'commander'; +import * as path from 'path'; +import * as fs from 'fs'; +import { log, die } from '../utils/log'; +import { requireEnv, getEnv, getScriptsDir, getRepoRoot } from '../utils/env'; +import { parseAuthArgs, createDeployExecuteAuth, getDeployAuth, getExecuteAuth } from '../utils/auth'; +import { runForgeScript, getChainId, parseActionAddress, findScript } from '../utils/forge'; + +const CONTRACTS_DIR = path.join(getScriptsDir(), 'contract-upgrades'); + +function getVersionDir(version: string): string { + const versionDir = path.join(CONTRACTS_DIR, version); + if (!fs.existsSync(versionDir)) { + const available = fs.existsSync(CONTRACTS_DIR) + ? fs.readdirSync(CONTRACTS_DIR).filter((f) => !f.startsWith('.')).join(' ') + : 'none found'; + die(`Unknown version: ${version}\n\nAvailable versions: ${available}`); + } + return versionDir; +} + +async function cmdDeploy(version: string, args: string[]): Promise { + const versionDir = getVersionDir(version); + const authArgs = parseAuthArgs(args); + + const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); + + const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/); + if (!deployScript) { + die(`No deploy script found in ${versionDir}`); + } + + log(`Running: ${path.basename(deployScript)}`); + + await runForgeScript({ + script: deployScript, + rpcUrl, + authArgs, + broadcast: !!authArgs, + slow: true, + skipSimulation: true, + }); +} + +async function cmdExecute(version: string, args: string[]): Promise { + const versionDir = getVersionDir(version); + const authArgs = parseAuthArgs(args); + + const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); + requireEnv('UPGRADE_ACTION_ADDRESS'); + + const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/); + if (!executeScript) { + die(`No execute script found in ${versionDir}`); + } + + log(`Running: ${path.basename(executeScript)}`); + + await runForgeScript({ + script: executeScript, + rpcUrl, + authArgs, + broadcast: !!authArgs, + }); +} + +async function cmdVerify(version: string): Promise { + const versionDir = getVersionDir(version); + + const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); + + const verifyScript = findScript(versionDir, /^Verify.*\.s\.sol$/); + if (!verifyScript) { + die(`No verify script found in ${versionDir} - check README for manual verification`); + } + + log(`Running: ${path.basename(verifyScript)}`); + + await runForgeScript({ + script: verifyScript, + rpcUrl, + }); +} + +async function cmdDeployExecuteVerify( + version: string, + options: { + deployKey?: string; + deployAccount?: string; + deployLedger?: boolean; + deployInteractive?: boolean; + executeKey?: string; + executeAccount?: string; + executeLedger?: boolean; + executeInteractive?: boolean; + dryRun?: boolean; + skipExecute?: boolean; + verify?: boolean; + } +): Promise { + const versionDir = getVersionDir(version); + const auth = createDeployExecuteAuth(options); + + const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/); + const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/); + + if (!deployScript) { + die(`No deploy script found in ${versionDir}`); + } + if (!executeScript) { + die(`No execute script found in ${versionDir}`); + } + + log(`Version: ${version}`); + log(`Deploy script: ${path.basename(deployScript)}`); + log(`Execute script: ${path.basename(executeScript)}`); + + // Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set + let skipDeploy = !!getEnv('UPGRADE_ACTION_ADDRESS'); + let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || ''; + + if (skipDeploy) { + log(`Using existing action from .env: ${upgradeActionAddress}`); + } + + // Validate required env vars + const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); + requireEnv('INBOX_ADDRESS'); + requireEnv('PROXY_ADMIN_ADDRESS'); + requireEnv('PARENT_UPGRADE_EXECUTOR_ADDRESS'); + + // Validate auth + const deployAuth = getDeployAuth(auth); + const executeAuth = getExecuteAuth(auth); + + if (!skipDeploy && !auth.dryRun && !deployAuth) { + die('Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive'); + } + if (!auth.skipExecute && !auth.dryRun && !executeAuth) { + die('Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive'); + } + + const chainId = await getChainId(rpcUrl); + log(`Target chain ID: ${chainId}`); + + // Step 1: Deploy + if (!skipDeploy) { + log('Step 1: Deploying upgrade action...'); + + await runForgeScript({ + script: deployScript, + rpcUrl, + authArgs: deployAuth, + broadcast: !auth.dryRun, + verify: auth.verifyContracts, + slow: true, + skipSimulation: true, + }); + + if (!auth.dryRun) { + upgradeActionAddress = parseActionAddress(deployScript, chainId); + log(`Deployed action at: ${upgradeActionAddress}`); + } else { + log('Dry run - no action deployed'); + if (!auth.skipExecute) { + log('Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step'); + return; + } + } + } else { + log('Step 1: Skipped deploy'); + } + + // Export for execute script + process.env.UPGRADE_ACTION_ADDRESS = upgradeActionAddress; + + // Step 2: Execute + if (!auth.skipExecute) { + log('Step 2: Executing upgrade...'); + + await runForgeScript({ + script: executeScript, + rpcUrl, + authArgs: executeAuth, + broadcast: !auth.dryRun, + }); + + if (auth.dryRun) { + log('Dry run - upgrade not executed'); + } else { + log('Upgrade executed successfully'); + } + } else { + log('Step 2: Skipped execute'); + } + + // Step 3: Verify + if (!auth.dryRun && !auth.skipExecute) { + log('Step 3: Verifying upgrade...'); + + const verifyScript = findScript(versionDir, /^Verify.*\.s\.sol$/); + if (verifyScript) { + await runForgeScript({ + script: verifyScript, + rpcUrl, + }); + } else { + log('No Verify script found - check README for manual verification'); + } + } + + log('Done'); +} + +export function createContractUpgradeCommand(): Command { + const cmd = new Command('contract-upgrade') + .description('Contract upgrade operations') + .argument('', 'Contract version (e.g., 1.2.1)') + .argument('', 'Command: deploy, execute, verify, deploy-execute-verify') + .option('--private-key ', 'Private key (for deploy/execute)') + .option('--account ', 'Keystore account (for deploy/execute)') + .option('--ledger', 'Use Ledger (for deploy/execute)') + .option('--interactive', 'Prompt for key (for deploy/execute)') + .option('--deploy-key ', 'Private key for deploy step') + .option('--deploy-account ', 'Keystore account for deploy') + .option('--deploy-ledger', 'Use Ledger for deploy') + .option('--deploy-interactive', 'Prompt for key for deploy') + .option('--execute-key ', 'Private key for execute step') + .option('--execute-account ', 'Keystore account for execute') + .option('--execute-ledger', 'Use Ledger for execute') + .option('--execute-interactive', 'Prompt for key for execute') + .option('-n, --dry-run', 'Simulate without broadcasting') + .option('--skip-execute', 'Deploy only') + .option('-v, --verify', 'Verify on block explorer') + .action(async (version: string, command: string, options) => { + // Build args array from remaining options for simple commands + const args: string[] = []; + if (options.privateKey) args.push('--private-key', options.privateKey); + if (options.account) args.push('--account', options.account); + if (options.ledger) args.push('--ledger'); + if (options.interactive) args.push('--interactive'); + + switch (command) { + case 'deploy': + await cmdDeploy(version, args); + break; + case 'execute': + await cmdExecute(version, args); + break; + case 'verify': + await cmdVerify(version); + break; + case 'deploy-execute-verify': + await cmdDeployExecuteVerify(version, options); + break; + default: + die(`Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify`); + } + }); + + return cmd; +} + +// Export individual functions for use by router +export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify, getVersionDir }; diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 00000000..33b02d1a --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,29 @@ +#!/usr/bin/env node +/** + * orbit-actions CLI entry point + */ + +import { program } from 'commander'; +import { loadEnv } from './utils/env'; +import { router } from './router'; +import { createContractUpgradeCommand } from './commands/contract-upgrade'; +import { createArbosUpgradeCommand } from './commands/arbos-upgrade'; + +// Load .env from repo root (or /app in Docker) +loadEnv(); + +program + .name('orbit-actions') + .description('CLI for Orbit chain upgrade actions') + .argument('[path]', 'Path to browse or command to run') + .argument('[args...]', 'Additional arguments') + .allowUnknownOption(true) + .action(async (pathArg?: string, args?: string[]) => { + await router(pathArg, args); + }); + +// Register subcommands for direct invocation +program.addCommand(createContractUpgradeCommand()); +program.addCommand(createArbosUpgradeCommand()); + +program.parse(); diff --git a/src/cli/router.ts b/src/cli/router.ts new file mode 100644 index 00000000..50fb2870 --- /dev/null +++ b/src/cli/router.ts @@ -0,0 +1,227 @@ +/** + * Path routing for CLI + * Handles directory listing, file viewing, and command dispatch + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { die } from './utils/log'; +import { getScriptsDir } from './utils/env'; +import { + cmdDeploy as contractDeploy, + cmdExecute as contractExecute, + cmdVerify as contractVerify, + cmdDeployExecuteVerify as contractDeployExecuteVerify, +} from './commands/contract-upgrade'; +import { + cmdDeploy as arbosDeploy, + cmdExecute as arbosExecute, + cmdVerify as arbosVerify, + cmdDeployExecuteVerify as arbosDeployExecuteVerify, +} from './commands/arbos-upgrade'; + +const HELP_TEXT = `Usage: orbit-actions [path] [args...] + +Browse and execute scripts from the foundry scripts directory. + +Browsing: + . List top-level directories + contract-upgrades List available versions + contract-upgrades/1.2.1 List version contents + commands + contract-upgrades/1.2.1/env-templates List env templates + +Viewing files: + contract-upgrades/1.2.1/README.md View README + contract-upgrades/1.2.1/env-templates/.env.example View env template + contract-upgrades/2.1.0/.env.sample View env sample + +Running upgrade scripts: + contract-upgrades//deploy [--private-key KEY] + contract-upgrades//execute [--private-key KEY] + contract-upgrades//verify + contract-upgrades//deploy-execute-verify [--deploy-key KEY] [--execute-key KEY] + + arbos-upgrades/at-timestamp/deploy [--private-key KEY] + arbos-upgrades/at-timestamp/execute [--private-key KEY] + arbos-upgrades/at-timestamp/verify + arbos-upgrades/at-timestamp/deploy-execute-verify [--deploy-key KEY] [--execute-key KEY] + +Options for deploy-execute-verify: + --deploy-key KEY Private key for deploy step + --deploy-account NAME Keystore account for deploy + --deploy-ledger Use Ledger for deploy + --execute-key KEY Private key for execute step + --execute-account NAME Keystore account for execute + --execute-ledger Use Ledger for execute + --dry-run, -n Simulate without broadcasting + --skip-execute Deploy only + --verify, -v Verify on block explorer + +Examples: + docker run orbit-actions contract-upgrades/1.2.1 + docker run orbit-actions contract-upgrades/1.2.1/README.md + docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/deploy-execute-verify --dry-run`; + +function listDirectory(dir: string): void { + const scriptsDir = getScriptsDir(); + let relPath = path.relative(scriptsDir, dir); + if (relPath === '.') relPath = ''; + + // List actual contents + const contents = fs.readdirSync(dir); + for (const item of contents) { + // Skip hidden files + if (!item.startsWith('.')) { + console.log(item); + } + } + + // Add virtual commands based on directory type + if (/^contract-upgrades\/[0-9]/.test(relPath)) { + console.log('---'); + console.log('deploy (run Deploy script)'); + console.log('execute (run Execute script)'); + console.log('verify (run Verify script)'); + console.log('deploy-execute-verify (full upgrade flow)'); + } else if (relPath === 'arbos-upgrades/at-timestamp') { + console.log('---'); + console.log('deploy (run Deploy script)'); + console.log('execute (execute upgrade action)'); + console.log('verify (check upgrade status)'); + console.log('deploy-execute-verify (full upgrade flow)'); + } +} + +function parseOptions(args: string[]): Record { + const options: Record = {}; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === '--deploy-key' || arg === '--execute-key' || arg === '--deploy-account' || arg === '--execute-account') { + options[arg.replace(/^--/, '').replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = args[++i] || ''; + } else if (arg === '--deploy-ledger') { + options.deployLedger = true; + } else if (arg === '--execute-ledger') { + options.executeLedger = true; + } else if (arg === '--deploy-interactive') { + options.deployInteractive = true; + } else if (arg === '--execute-interactive') { + options.executeInteractive = true; + } else if (arg === '--dry-run' || arg === '-n') { + options.dryRun = true; + } else if (arg === '--skip-execute') { + options.skipExecute = true; + } else if (arg === '--verify' || arg === '-v') { + options.verify = true; + } else if (arg === '--private-key') { + options.privateKey = args[++i] || ''; + } else if (arg === '--account') { + options.account = args[++i] || ''; + } else if (arg === '--ledger') { + options.ledger = true; + } else if (arg === '--interactive') { + options.interactive = true; + } + } + return options; +} + +export async function router(pathArg?: string, args: string[] = []): Promise { + const scriptsDir = getScriptsDir(); + + // No args - list top level + if (!pathArg) { + const contents = fs.readdirSync(scriptsDir); + for (const item of contents) { + if (!item.startsWith('.')) { + console.log(item); + } + } + return; + } + + // Help + if (pathArg === 'help' || pathArg === '--help' || pathArg === '-h') { + console.log(HELP_TEXT); + return; + } + + const fullPath = path.join(scriptsDir, pathArg); + + // Parse path for virtual commands + const parentPath = path.dirname(fullPath); + const basename = path.basename(pathArg); + + // Check for virtual commands (deploy, execute, deploy-execute-verify) + if (!fs.existsSync(fullPath) && fs.existsSync(parentPath)) { + const relParent = path.relative(scriptsDir, parentPath); + + // Contract upgrades virtual commands + if (/^contract-upgrades\/[0-9]/.test(relParent)) { + const version = path.basename(relParent); + const options = parseOptions(args); + + switch (basename) { + case 'deploy': + await contractDeploy(version, args); + return; + case 'execute': + await contractExecute(version, args); + return; + case 'verify': + await contractVerify(version); + return; + case 'deploy-execute-verify': + await contractDeployExecuteVerify(version, options); + return; + } + } + + // ArbOS upgrades virtual commands + if (relParent === 'arbos-upgrades/at-timestamp') { + switch (basename) { + case 'deploy': + case 'deploy-execute-verify': { + // These commands need a version argument + const version = args[0]; + if (!version) { + console.error(`Error: ArbOS version required`); + console.error(`Usage: arbos-upgrades/at-timestamp/${basename} [options]`); + process.exit(1); + } + const restArgs = args.slice(1); + const restOptions = parseOptions(restArgs); + if (basename === 'deploy') { + await arbosDeploy(version, restArgs); + } else { + await arbosDeployExecuteVerify(version, restOptions); + } + return; + } + case 'execute': + await arbosExecute(args); + return; + case 'verify': + await arbosVerify(); + return; + } + } + } + + // Directory - list contents + if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) { + listDirectory(fullPath); + return; + } + + // Regular file - cat it + if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) { + const content = fs.readFileSync(fullPath, 'utf-8'); + console.log(content); + return; + } + + // Not found + die(`Not found: ${pathArg} + +Use 'help' to see available commands.`); +} diff --git a/src/cli/utils/auth.ts b/src/cli/utils/auth.ts new file mode 100644 index 00000000..496d0478 --- /dev/null +++ b/src/cli/utils/auth.ts @@ -0,0 +1,110 @@ +/** + * Authentication argument parsing utilities + */ + +export interface AuthArgs { + authArgs: string; +} + +export interface DeployExecuteAuth { + deployKey: string; + deployAccount: string; + deployLedger: boolean; + deployInteractive: boolean; + executeKey: string; + executeAccount: string; + executeLedger: boolean; + executeInteractive: boolean; + dryRun: boolean; + skipExecute: boolean; + verifyContracts: boolean; +} + +/** + * Parse simple auth args (--private-key, --account, --ledger, --interactive) + * Returns forge/cast compatible auth string + */ +export function parseAuthArgs(args: string[]): string { + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === '--private-key' || arg === '--account') { + const value = args[i + 1]; + if (value) { + return `${arg} ${value}`; + } + } + if (arg === '--ledger' || arg === '--interactive') { + return arg; + } + } + return ''; +} + +/** + * Get deploy auth args from DeployExecuteAuth + */ +export function getDeployAuth(auth: DeployExecuteAuth): string { + if (auth.deployKey) { + return `--private-key ${auth.deployKey}`; + } + if (auth.deployAccount) { + return `--account ${auth.deployAccount}`; + } + if (auth.deployLedger) { + return '--ledger'; + } + if (auth.deployInteractive) { + return '--interactive'; + } + return ''; +} + +/** + * Get execute auth args from DeployExecuteAuth + */ +export function getExecuteAuth(auth: DeployExecuteAuth): string { + if (auth.executeKey) { + return `--private-key ${auth.executeKey}`; + } + if (auth.executeAccount) { + return `--account ${auth.executeAccount}`; + } + if (auth.executeLedger) { + return '--ledger'; + } + if (auth.executeInteractive) { + return '--interactive'; + } + return ''; +} + +/** + * Create default DeployExecuteAuth from commander options + */ +export function createDeployExecuteAuth(options: { + deployKey?: string; + deployAccount?: string; + deployLedger?: boolean; + deployInteractive?: boolean; + executeKey?: string; + executeAccount?: string; + executeLedger?: boolean; + executeInteractive?: boolean; + dryRun?: boolean; + skipExecute?: boolean; + verify?: boolean; +}): DeployExecuteAuth { + return { + deployKey: options.deployKey || '', + deployAccount: options.deployAccount || '', + deployLedger: options.deployLedger || false, + deployInteractive: options.deployInteractive || false, + executeKey: options.executeKey || '', + executeAccount: options.executeAccount || '', + executeLedger: options.executeLedger || false, + executeInteractive: options.executeInteractive || false, + dryRun: options.dryRun || false, + skipExecute: options.skipExecute || false, + verifyContracts: options.verify || false, + }; +} diff --git a/src/cli/utils/env.ts b/src/cli/utils/env.ts new file mode 100644 index 00000000..65cd06c5 --- /dev/null +++ b/src/cli/utils/env.ts @@ -0,0 +1,84 @@ +/** + * Environment variable utilities + */ + +import * as dotenv from 'dotenv'; +import * as fs from 'fs'; +import * as path from 'path'; +import { die } from './log'; + +/** + * Find the repository root by looking for package.json + */ +function findRepoRoot(): string | null { + let dir = __dirname; + // Walk up from current file location + for (let i = 0; i < 10; i++) { + if (fs.existsSync(path.join(dir, 'package.json'))) { + return dir; + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return null; +} + +/** + * Load .env file from: + * 1. Current working directory + * 2. Repository root + * 3. /app/.env (Docker) + */ +export function loadEnv(): void { + const candidates = [ + path.join(process.cwd(), '.env'), + findRepoRoot() ? path.join(findRepoRoot()!, '.env') : null, + '/app/.env', + ].filter((p): p is string => p !== null); + + for (const envPath of candidates) { + if (fs.existsSync(envPath)) { + dotenv.config({ path: envPath }); + return; + } + } +} + +/** + * Require an environment variable to be set + * @throws Exits process if variable is not set + */ +export function requireEnv(name: string): string { + const value = process.env[name]; + if (!value) { + die(`Required env var not set: ${name} (check your .env file)`); + } + return value; +} + +/** + * Get an optional environment variable + */ +export function getEnv(name: string): string | undefined { + return process.env[name]; +} + +/** + * Get the scripts directory path + */ +export function getScriptsDir(): string { + const repoRoot = findRepoRoot(); + if (repoRoot) { + return path.join(repoRoot, 'scripts', 'foundry'); + } + // Fallback for Docker + return '/app/scripts/foundry'; +} + +/** + * Get the repository root path + */ +export function getRepoRoot(): string { + return findRepoRoot() || '/app'; +} diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts new file mode 100644 index 00000000..c3cb8a15 --- /dev/null +++ b/src/cli/utils/forge.ts @@ -0,0 +1,167 @@ +/** + * Forge/Cast command execution utilities + */ + +import execa from 'execa'; +import * as fs from 'fs'; +import * as path from 'path'; +import { die, log } from './log'; +import { getRepoRoot } from './env'; + +export interface ForgeScriptOptions { + script: string; + rpcUrl: string; + authArgs?: string; + broadcast?: boolean; + verify?: boolean; + slow?: boolean; + skipSimulation?: boolean; + verbosity?: number; +} + +/** + * Run a forge script command + */ +export async function runForgeScript(options: ForgeScriptOptions): Promise { + const args = ['script', options.script, '--rpc-url', options.rpcUrl]; + + if (options.slow) { + args.push('--slow'); + } + + if (options.skipSimulation) { + args.push('--skip-simulation'); + } + + const verbosity = options.verbosity ?? 3; + args.push('-' + 'v'.repeat(verbosity)); + + if (options.broadcast && options.authArgs) { + args.push('--broadcast'); + args.push(...options.authArgs.split(' ').filter(Boolean)); + } + + if (options.verify) { + args.push('--verify'); + } + + log(`Running: forge ${args.slice(0, 2).join(' ')}...`); + + const result = await execa('forge', args, { + stdio: 'inherit', + env: process.env, + }); + + if (result.exitCode !== 0) { + die(`Forge script failed with exit code ${result.exitCode}`); + } +} + +export interface CastSendOptions { + to: string; + sig: string; + args: string[]; + rpcUrl: string; + authArgs?: string; +} + +/** + * Run a cast send command + */ +export async function runCastSend(options: CastSendOptions): Promise { + const args = ['send', options.to, options.sig, ...options.args, '--rpc-url', options.rpcUrl]; + + if (options.authArgs) { + args.push(...options.authArgs.split(' ').filter(Boolean)); + } + + const result = await execa('cast', args, { + stdio: 'inherit', + env: process.env, + }); + + if (result.exitCode !== 0) { + die(`Cast send failed with exit code ${result.exitCode}`); + } +} + +export interface CastCallOptions { + to: string; + sig: string; + rpcUrl: string; +} + +/** + * Run a cast call command and return the result + */ +export async function runCastCall(options: CastCallOptions): Promise { + try { + const result = await execa('cast', ['call', '--rpc-url', options.rpcUrl, options.to, options.sig]); + return result.stdout; + } catch { + return 'N/A'; + } +} + +/** + * Generate calldata using cast + */ +export async function castCalldata(sig: string, ...args: string[]): Promise { + const result = await execa('cast', ['calldata', sig, ...args]); + return result.stdout; +} + +/** + * Get chain ID from RPC URL + */ +export async function getChainId(rpcUrl: string): Promise { + const result = await execa('cast', ['chain-id', '--rpc-url', rpcUrl]); + return result.stdout.trim(); +} + +/** + * Parse the deployed action address from forge broadcast file + */ +export function parseActionAddress(scriptPath: string, chainId: string): string { + const scriptName = path.basename(scriptPath); + const repoRoot = getRepoRoot(); + const broadcastFile = path.join(repoRoot, 'broadcast', scriptName, chainId, 'run-latest.json'); + + if (!fs.existsSync(broadcastFile)) { + die(`Broadcast file not found: ${broadcastFile}`); + } + + const content = JSON.parse(fs.readFileSync(broadcastFile, 'utf-8')); + const createTxs = content.transactions?.filter( + (tx: { transactionType: string }) => tx.transactionType === 'CREATE' + ); + + if (!createTxs || createTxs.length === 0) { + die('Could not parse action address from broadcast file'); + } + + const address = createTxs[createTxs.length - 1]?.contractAddress; + if (!address) { + die('Could not parse action address from broadcast file'); + } + + return address; +} + +/** + * Find a script file by pattern in a directory + */ +export function findScript(dir: string, pattern: RegExp): string | null { + if (!fs.existsSync(dir)) { + return null; + } + + const files = fs.readdirSync(dir); + for (const file of files) { + if (pattern.test(file) && file.endsWith('.s.sol')) { + return path.join(dir, file); + } + } + return null; +} + diff --git a/src/cli/utils/log.ts b/src/cli/utils/log.ts new file mode 100644 index 00000000..1286c19a --- /dev/null +++ b/src/cli/utils/log.ts @@ -0,0 +1,20 @@ +/** + * Logging utilities for CLI + */ + +const PREFIX = '[orbit-actions]'; + +/** + * Log an informational message to stdout + */ +export function log(message: string): void { + console.log(`${PREFIX} ${message}`); +} + +/** + * Log an error message and exit with code 1 + */ +export function die(message: string): never { + console.error(`Error: ${message}`); + process.exit(1); +} diff --git a/tsconfig.cli.json b/tsconfig.cli.json new file mode 100644 index 00000000..8429e32d --- /dev/null +++ b/tsconfig.cli.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true + }, + "include": ["src/**/*"] +} diff --git a/yarn.lock b/yarn.lock index 1cd2598d..15c2575d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2082,6 +2082,11 @@ commander@3.0.2: resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== +commander@^12.0.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + compare-versions@^6.0.0: version "6.1.1" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.1.tgz#7af3cc1099ba37d244b3145a9af5201b629148a9" @@ -2681,7 +2686,43 @@ ethereumjs-util@^7.0.3, ethereumjs-util@^7.1.4: ethereum-cryptography "^0.1.3" rlp "^2.2.4" -"ethers-v5@npm:ethers@^5.7.2", ethers@^5.7.1, ethers@^5.7.2: +"ethers-v5@npm:ethers@^5.7.2": + version "5.7.2" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" + integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== + dependencies: + "@ethersproject/abi" "5.7.0" + "@ethersproject/abstract-provider" "5.7.0" + "@ethersproject/abstract-signer" "5.7.0" + "@ethersproject/address" "5.7.0" + "@ethersproject/base64" "5.7.0" + "@ethersproject/basex" "5.7.0" + "@ethersproject/bignumber" "5.7.0" + "@ethersproject/bytes" "5.7.0" + "@ethersproject/constants" "5.7.0" + "@ethersproject/contracts" "5.7.0" + "@ethersproject/hash" "5.7.0" + "@ethersproject/hdnode" "5.7.0" + "@ethersproject/json-wallets" "5.7.0" + "@ethersproject/keccak256" "5.7.0" + "@ethersproject/logger" "5.7.0" + "@ethersproject/networks" "5.7.1" + "@ethersproject/pbkdf2" "5.7.0" + "@ethersproject/properties" "5.7.0" + "@ethersproject/providers" "5.7.2" + "@ethersproject/random" "5.7.0" + "@ethersproject/rlp" "5.7.0" + "@ethersproject/sha2" "5.7.0" + "@ethersproject/signing-key" "5.7.0" + "@ethersproject/solidity" "5.7.0" + "@ethersproject/strings" "5.7.0" + "@ethersproject/transactions" "5.7.0" + "@ethersproject/units" "5.7.0" + "@ethersproject/wallet" "5.7.0" + "@ethersproject/web" "5.7.1" + "@ethersproject/wordlists" "5.7.0" + +ethers@^5.7.1, ethers@^5.7.2: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -2767,6 +2808,21 @@ evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" +execa@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3017,6 +3073,11 @@ get-port@^3.1.0: resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" integrity sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg== +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-symbol-description@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" @@ -3391,6 +3452,11 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -3612,6 +3678,11 @@ is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: dependencies: call-bind "^1.0.7" +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -3945,6 +4016,11 @@ memorystream@^0.3.1: resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -3975,6 +4051,11 @@ mime-types@^2.1.12: dependencies: mime-db "1.52.0" +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -4132,6 +4213,13 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + number-to-bn@1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/number-to-bn/-/number-to-bn-1.7.0.tgz#bb3623592f7e5f9e0030b1977bd41a0c53fe1ea0" @@ -4177,6 +4265,13 @@ once@1.x, once@^1.3.0: dependencies: wrappy "1" +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + open@^7.4.2: version "7.4.2" resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" @@ -4311,7 +4406,7 @@ path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== -path-key@^3.1.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -4810,7 +4905,7 @@ side-channel@^1.0.4: get-intrinsic "^1.2.4" object-inspect "^1.13.1" -signal-exit@^3.0.2: +signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -5016,6 +5111,11 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-hex-prefix@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz#0c5f155fef1151373377de9dbb588da05500e36f" From c5520f74cfe2e115403979f447e2c47130308e95 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Feb 2026 13:19:12 +0000 Subject: [PATCH 11/21] refactor: remove obvious comments and add named constants - Remove redundant comments that state the obvious - Add named constants for ArbOS precompiles (ARB_OWNER_PUBLIC, ARB_SYS) - Add ARBOS_VERSION_OFFSET constant with explanation - Remove unused AuthArgs interface - Remove unused getRepoRoot import - Fix double findRepoRoot() call in loadEnv() --- src/cli/commands/arbos-upgrade.ts | 36 +++++++++++++--------------- src/cli/commands/contract-upgrade.ts | 15 ++---------- src/cli/index.ts | 6 +---- src/cli/router.ts | 19 --------------- src/cli/utils/auth.ts | 21 ---------------- src/cli/utils/env.ts | 31 ++---------------------- src/cli/utils/forge.ts | 25 ------------------- src/cli/utils/log.ts | 10 -------- 8 files changed, 21 insertions(+), 142 deletions(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 6724cacf..04e41444 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -1,7 +1,3 @@ -/** - * ArbOS upgrade commands - */ - import { Command } from 'commander'; import * as path from 'path'; import * as fs from 'fs'; @@ -20,6 +16,13 @@ import { const ARBOS_DIR = path.join(getScriptsDir(), 'arbos-upgrades', 'at-timestamp'); const DEPLOY_SCRIPT = path.join(ARBOS_DIR, 'DeployUpgradeArbOSVersionAtTimestampAction.s.sol'); +// ArbOS precompile addresses +const ARB_OWNER_PUBLIC = '0x000000000000000000000000000000000000006b'; +const ARB_SYS = '0x0000000000000000000000000000000000000064'; + +// Nitro ArbOS versions are offset by 55 to avoid collision with classic (pre-Nitro) versions +const ARBOS_VERSION_OFFSET = 55; + async function cmdDeploy(version: string, args: string[]): Promise { const authArgs = parseAuthArgs(args); @@ -29,7 +32,7 @@ async function cmdDeploy(version: string, args: string[]): Promise { die(`Deploy script not found: ${DEPLOY_SCRIPT}`); } - // Export version for the script + // Forge script reads this from env process.env.ARBOS_VERSION = version; log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`); @@ -83,14 +86,14 @@ async function cmdVerify(): Promise { log('Checking ArbOS upgrade status...'); const scheduled = await runCastCall({ - to: '0x000000000000000000000000000000000000006b', + to: ARB_OWNER_PUBLIC, sig: 'getScheduledUpgrade()(uint64,uint64)', rpcUrl, }); log(`Scheduled upgrade (version, timestamp): ${scheduled}`); const currentRaw = await runCastCall({ - to: '0x0000000000000000000000000000000000000064', + to: ARB_SYS, sig: 'arbOSVersion()(uint64)', rpcUrl, }); @@ -100,7 +103,7 @@ async function cmdVerify(): Promise { currentVersion = 0; } else { const rawNum = parseInt(currentRaw, 10); - currentVersion = rawNum - 55; + currentVersion = rawNum - ARBOS_VERSION_OFFSET; } log(`Current ArbOS version: ${currentVersion}`); @@ -138,15 +141,13 @@ async function cmdDeployExecuteVerify( log(`Using existing action from .env: ${upgradeActionAddress}`); } - // Validate required env vars const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS'); requireEnv('SCHEDULE_TIMESTAMP'); - // Export version for the script + // Forge script reads this from env process.env.ARBOS_VERSION = version; - // Validate auth const deployAuth = getDeployAuth(auth); const executeAuth = getExecuteAuth(auth); @@ -159,7 +160,6 @@ async function cmdDeployExecuteVerify( log(`Scheduled timestamp: ${process.env.SCHEDULE_TIMESTAMP}`); - // Step 1: Deploy let chainId = ''; if (!skipDeploy) { chainId = await getChainId(rpcUrl); @@ -189,11 +189,10 @@ async function cmdDeployExecuteVerify( log('Step 1: Skipped deploy'); } - // Step 2: Execute via cast send if (!auth.skipExecute) { log('Step 2: Executing ArbOS upgrade...'); - const performCalldata = '0xb0a75d36'; + const performCalldata = '0xb0a75d36'; // perform() selector if (auth.dryRun) { const executeCalldata = await castCalldata('execute(address,bytes)', upgradeActionAddress, performCalldata); @@ -219,19 +218,18 @@ async function cmdDeployExecuteVerify( log('Step 2: Skipped execute'); } - // Step 3: Verify if (!auth.dryRun && !auth.skipExecute) { log('Step 3: Verifying scheduled upgrade...'); const scheduled = await runCastCall({ - to: '0x000000000000000000000000000000000000006b', + to: ARB_OWNER_PUBLIC, sig: 'getScheduledUpgrade()(uint64,uint64)', rpcUrl, }); log(`Scheduled upgrade (version, timestamp): ${scheduled}`); const currentRaw = await runCastCall({ - to: '0x0000000000000000000000000000000000000064', + to: ARB_SYS, sig: 'arbOSVersion()(uint64)', rpcUrl, }); @@ -241,7 +239,7 @@ async function cmdDeployExecuteVerify( currentVersion = 0; } else { const rawNum = parseInt(currentRaw, 10); - currentVersion = rawNum - 55; + currentVersion = rawNum - ARBOS_VERSION_OFFSET; } log(`Current ArbOS version: ${currentVersion}`); @@ -271,7 +269,6 @@ export function createArbosUpgradeCommand(): Command { .option('--skip-execute', 'Deploy only') .option('-v, --verify', 'Verify on block explorer') .action(async (version: string, command: string, options) => { - // Build args array from remaining options for simple commands const args: string[] = []; if (options.privateKey) args.push('--private-key', options.privateKey); if (options.account) args.push('--account', options.account); @@ -299,5 +296,4 @@ export function createArbosUpgradeCommand(): Command { return cmd; } -// Export individual functions for use by router export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify }; diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts index 7aa66d17..fc88618d 100644 --- a/src/cli/commands/contract-upgrade.ts +++ b/src/cli/commands/contract-upgrade.ts @@ -1,12 +1,8 @@ -/** - * Contract upgrade commands - */ - import { Command } from 'commander'; import * as path from 'path'; import * as fs from 'fs'; import { log, die } from '../utils/log'; -import { requireEnv, getEnv, getScriptsDir, getRepoRoot } from '../utils/env'; +import { requireEnv, getEnv, getScriptsDir } from '../utils/env'; import { parseAuthArgs, createDeployExecuteAuth, getDeployAuth, getExecuteAuth } from '../utils/auth'; import { runForgeScript, getChainId, parseActionAddress, findScript } from '../utils/forge'; @@ -127,13 +123,11 @@ async function cmdDeployExecuteVerify( log(`Using existing action from .env: ${upgradeActionAddress}`); } - // Validate required env vars const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); requireEnv('INBOX_ADDRESS'); requireEnv('PROXY_ADMIN_ADDRESS'); requireEnv('PARENT_UPGRADE_EXECUTOR_ADDRESS'); - // Validate auth const deployAuth = getDeployAuth(auth); const executeAuth = getExecuteAuth(auth); @@ -147,7 +141,6 @@ async function cmdDeployExecuteVerify( const chainId = await getChainId(rpcUrl); log(`Target chain ID: ${chainId}`); - // Step 1: Deploy if (!skipDeploy) { log('Step 1: Deploying upgrade action...'); @@ -175,10 +168,9 @@ async function cmdDeployExecuteVerify( log('Step 1: Skipped deploy'); } - // Export for execute script + // Forge script reads this from env process.env.UPGRADE_ACTION_ADDRESS = upgradeActionAddress; - // Step 2: Execute if (!auth.skipExecute) { log('Step 2: Executing upgrade...'); @@ -198,7 +190,6 @@ async function cmdDeployExecuteVerify( log('Step 2: Skipped execute'); } - // Step 3: Verify if (!auth.dryRun && !auth.skipExecute) { log('Step 3: Verifying upgrade...'); @@ -237,7 +228,6 @@ export function createContractUpgradeCommand(): Command { .option('--skip-execute', 'Deploy only') .option('-v, --verify', 'Verify on block explorer') .action(async (version: string, command: string, options) => { - // Build args array from remaining options for simple commands const args: string[] = []; if (options.privateKey) args.push('--private-key', options.privateKey); if (options.account) args.push('--account', options.account); @@ -265,5 +255,4 @@ export function createContractUpgradeCommand(): Command { return cmd; } -// Export individual functions for use by router export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify, getVersionDir }; diff --git a/src/cli/index.ts b/src/cli/index.ts index 33b02d1a..5f97048f 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,7 +1,4 @@ #!/usr/bin/env node -/** - * orbit-actions CLI entry point - */ import { program } from 'commander'; import { loadEnv } from './utils/env'; @@ -9,7 +6,6 @@ import { router } from './router'; import { createContractUpgradeCommand } from './commands/contract-upgrade'; import { createArbosUpgradeCommand } from './commands/arbos-upgrade'; -// Load .env from repo root (or /app in Docker) loadEnv(); program @@ -22,8 +18,8 @@ program await router(pathArg, args); }); -// Register subcommands for direct invocation program.addCommand(createContractUpgradeCommand()); program.addCommand(createArbosUpgradeCommand()); program.parse(); + diff --git a/src/cli/router.ts b/src/cli/router.ts index 50fb2870..d8a9617a 100644 --- a/src/cli/router.ts +++ b/src/cli/router.ts @@ -1,8 +1,3 @@ -/** - * Path routing for CLI - * Handles directory listing, file viewing, and command dispatch - */ - import * as fs from 'fs'; import * as path from 'path'; import { die } from './utils/log'; @@ -67,16 +62,13 @@ function listDirectory(dir: string): void { let relPath = path.relative(scriptsDir, dir); if (relPath === '.') relPath = ''; - // List actual contents const contents = fs.readdirSync(dir); for (const item of contents) { - // Skip hidden files if (!item.startsWith('.')) { console.log(item); } } - // Add virtual commands based on directory type if (/^contract-upgrades\/[0-9]/.test(relPath)) { console.log('---'); console.log('deploy (run Deploy script)'); @@ -128,7 +120,6 @@ function parseOptions(args: string[]): Record { export async function router(pathArg?: string, args: string[] = []): Promise { const scriptsDir = getScriptsDir(); - // No args - list top level if (!pathArg) { const contents = fs.readdirSync(scriptsDir); for (const item of contents) { @@ -139,23 +130,18 @@ export async function router(pathArg?: string, args: string[] = []): Promise p !== null); @@ -45,10 +32,6 @@ export function loadEnv(): void { } } -/** - * Require an environment variable to be set - * @throws Exits process if variable is not set - */ export function requireEnv(name: string): string { const value = process.env[name]; if (!value) { @@ -57,28 +40,18 @@ export function requireEnv(name: string): string { return value; } -/** - * Get an optional environment variable - */ export function getEnv(name: string): string | undefined { return process.env[name]; } -/** - * Get the scripts directory path - */ export function getScriptsDir(): string { const repoRoot = findRepoRoot(); if (repoRoot) { return path.join(repoRoot, 'scripts', 'foundry'); } - // Fallback for Docker return '/app/scripts/foundry'; } -/** - * Get the repository root path - */ export function getRepoRoot(): string { return findRepoRoot() || '/app'; } diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index c3cb8a15..eaac011b 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -1,7 +1,3 @@ -/** - * Forge/Cast command execution utilities - */ - import execa from 'execa'; import * as fs from 'fs'; import * as path from 'path'; @@ -19,9 +15,6 @@ export interface ForgeScriptOptions { verbosity?: number; } -/** - * Run a forge script command - */ export async function runForgeScript(options: ForgeScriptOptions): Promise { const args = ['script', options.script, '--rpc-url', options.rpcUrl]; @@ -65,9 +58,6 @@ export interface CastSendOptions { authArgs?: string; } -/** - * Run a cast send command - */ export async function runCastSend(options: CastSendOptions): Promise { const args = ['send', options.to, options.sig, ...options.args, '--rpc-url', options.rpcUrl]; @@ -91,9 +81,6 @@ export interface CastCallOptions { rpcUrl: string; } -/** - * Run a cast call command and return the result - */ export async function runCastCall(options: CastCallOptions): Promise { try { const result = await execa('cast', ['call', '--rpc-url', options.rpcUrl, options.to, options.sig]); @@ -103,25 +90,16 @@ export async function runCastCall(options: CastCallOptions): Promise { } } -/** - * Generate calldata using cast - */ export async function castCalldata(sig: string, ...args: string[]): Promise { const result = await execa('cast', ['calldata', sig, ...args]); return result.stdout; } -/** - * Get chain ID from RPC URL - */ export async function getChainId(rpcUrl: string): Promise { const result = await execa('cast', ['chain-id', '--rpc-url', rpcUrl]); return result.stdout.trim(); } -/** - * Parse the deployed action address from forge broadcast file - */ export function parseActionAddress(scriptPath: string, chainId: string): string { const scriptName = path.basename(scriptPath); const repoRoot = getRepoRoot(); @@ -148,9 +126,6 @@ export function parseActionAddress(scriptPath: string, chainId: string): string return address; } -/** - * Find a script file by pattern in a directory - */ export function findScript(dir: string, pattern: RegExp): string | null { if (!fs.existsSync(dir)) { return null; diff --git a/src/cli/utils/log.ts b/src/cli/utils/log.ts index 1286c19a..9df2a6c6 100644 --- a/src/cli/utils/log.ts +++ b/src/cli/utils/log.ts @@ -1,19 +1,9 @@ -/** - * Logging utilities for CLI - */ - const PREFIX = '[orbit-actions]'; -/** - * Log an informational message to stdout - */ export function log(message: string): void { console.log(`${PREFIX} ${message}`); } -/** - * Log an error message and exit with code 1 - */ export function die(message: string): never { console.error(`Error: ${message}`); process.exit(1); From c93b86f050eed02588c3ad5f0c57015d118aea7f Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Feb 2026 15:02:11 +0000 Subject: [PATCH 12/21] fix: update docker tests and eslint config for TS CLI - Remove deprecated @typescript-eslint/tslint plugin from eslint config - Update docker tests to use --entrypoint for tool access - Apply prettier formatting to src/cli files - Fix no-implicit-coercion lint errors (!! -> Boolean()) --- .eslintrc.js | 8 +- .../creator-upgrades/1.2.1/output/1.json | 2 +- .../creator-upgrades/1.2.1/output/1337.json | 2 +- .../creator-upgrades/1.2.1/output/42161.json | 2 +- .../creator-upgrades/1.2.2/output/1.json | 2 +- .../1.2.2/output/11155111.json | 2 +- .../creator-upgrades/1.2.2/output/42161.json | 2 +- .../creator-upgrades/1.2.2/output/421614.json | 2 +- .../creator-upgrades/1.2.2/output/42170.json | 2 +- .../creator-upgrades/1.2.2/output/8453.json | 2 +- src/cli/commands/arbos-upgrade.ts | 272 ++++++++++-------- src/cli/commands/contract-upgrade.ts | 244 +++++++++------- src/cli/index.ts | 23 +- src/cli/router.ts | 168 ++++++----- src/cli/utils/auth.ts | 76 ++--- src/cli/utils/env.ts | 44 +-- src/cli/utils/forge.ts | 148 ++++++---- src/cli/utils/log.ts | 8 +- test/docker/test-docker.bash | 22 +- 19 files changed, 559 insertions(+), 472 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 23d5ab1a..cd3e7f3d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -60,7 +60,7 @@ module.exports = { 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', ], - plugins: ['@typescript-eslint', 'prettier', '@typescript-eslint/tslint'], + plugins: ['@typescript-eslint', 'prettier'], rules: { 'no-empty-pattern': 'warn', 'prettier/prettier': ['error', { singleQuote: true }], @@ -77,12 +77,6 @@ module.exports = { caughtErrorsIgnorePattern: '^_', }, ], - '@typescript-eslint/tslint/config': [ - 'error', - { - rules: { 'strict-comparisons': true }, - }, - ], 'no-implicit-coercion': 'error', '@typescript-eslint/no-shadow': ['error'], }, diff --git a/scripts/foundry/creator-upgrades/1.2.1/output/1.json b/scripts/foundry/creator-upgrades/1.2.1/output/1.json index d5cfb46c..bf86fbb5 100644 --- a/scripts/foundry/creator-upgrades/1.2.1/output/1.json +++ b/scripts/foundry/creator-upgrades/1.2.1/output/1.json @@ -5,4 +5,4 @@ "updateBridgeErc20TemplatesCalldata": "0x1bb7c6cc0000000000000000000000007efcb76d0e2e776a298aaa603d433336e5f8b6ab000000000000000000000000383f16fb2809a56fc639c1ee2c93ad2aa7ee130a00000000000000000000000031faaab44e74eb408d1fc69a14806b4b9ca09da2000000000000000000000000302275067251f5fcdb9359bda735fd8f7a4a54c000000000000000000000000019431dc37098877486532250fb3158140717c00c", "updateBridgeEthTemplatesCalldata": "0xd94d6e0a0000000000000000000000001c6accd9d66f3b993928e7439c9a2d67b94a445f000000000000000000000000958985cf2c54f99ba4a599221a8090c1f9cee9a50000000000000000000000001162084c3c6575121146582db5be43189e8cee6b00000000000000000000000013be515e44eefaf3ebefad684f1fbb574ac0a4940000000000000000000000002a6dd4433ffa96dc1755814fc0d9cc83a5f68dec", "updateRollupCreatorTemplatesCalldata": "0xac9a97b40000000000000000000000001135265fe014d3fa32b3507e325642b92affeaeb00000000000000000000000057ea090ac0554d174ae0e2855b460e84a1a7c2210000000000000000000000001d901dd7a5efe421c3c437b147040e5af22e6a430000000000000000000000000ae4dd666748bf0f6db5c149eab1d8ad27820a6a000000000000000000000000660ea1675f7323dc3ba0c8ddfb593225eb01e3c10000000000000000000000006c21303f5986180b1394d2c89f3e883890e2867b0000000000000000000000002b0e04dc90e3fa58165cb41e2834b44a56e766af0000000000000000000000009cad81628ab7d8e239f1a5b497313341578c5f710000000000000000000000002e31291fa573db3dfeae00c9bd1806b73c7185c8" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.1/output/1337.json b/scripts/foundry/creator-upgrades/1.2.1/output/1337.json index 785a6d5d..22ad429a 100644 --- a/scripts/foundry/creator-upgrades/1.2.1/output/1337.json +++ b/scripts/foundry/creator-upgrades/1.2.1/output/1337.json @@ -5,4 +5,4 @@ "updateBridgeErc20TemplatesCalldata": "0x1bb7c6cc0000000000000000000000004e5b65fb12d4165e22f5861d97a33ba45c006114000000000000000000000000457f2a773d9ebd5eadd5d014db162749a1ea92eb0000000000000000000000009df23e34ac13a7145eba1164660e701839197b1b0000000000000000000000009f1ece352ce8d540738ccb38aa3fa3d44d00a2590000000000000000000000000bdad990640a488400565fe6fb1d879ffe12da37", "updateBridgeEthTemplatesCalldata": "0xd94d6e0a000000000000000000000000217788c286797d56cd59af5e493f3699c39cbbe80000000000000000000000006ca66235758bccd08a4d1612662482f08fab93470000000000000000000000000f1f89aaf1c6fdb7ff9d361e4388f5f3997f12a800000000000000000000000060571c8f4b52954a24a5e7306d435e951528d963000000000000000000000000b075b82c7a23e0994df4793422a1f03dbcf9136f", "updateRollupCreatorTemplatesCalldata": "0xac9a97b40000000000000000000000005e36aa9caaf5f708fca5c04d2d4c776a62b2b2580000000000000000000000002766e96f90f9f027835e0c00c04c8119c635ce02000000000000000000000000037b11bb930dbb7c875ce459eeff69fc2e9fd40d0000000000000000000000009c2ed9f57d053fdfaecbf1b6dfd7c97e2e340b84000000000000000000000000f7ec0b16a45dc99ae21bfa8b4b737d1d61ca9fa4000000000000000000000000dfb681cc1f2c180c2131bb4deb46642d6258b0ff000000000000000000000000bd4cc2f69ffd94b5f62dcc5a27c2eb805093fc0d000000000000000000000000a80482dddb7f8b9dcc24a1cd13488e3379a1456800000000000000000000000092f58045ffb1c00a7b9486b9d2a55d316380cb45" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.1/output/42161.json b/scripts/foundry/creator-upgrades/1.2.1/output/42161.json index 507add51..a9e3e652 100644 --- a/scripts/foundry/creator-upgrades/1.2.1/output/42161.json +++ b/scripts/foundry/creator-upgrades/1.2.1/output/42161.json @@ -5,4 +5,4 @@ "updateBridgeErc20TemplatesCalldata": "0x1bb7c6cc0000000000000000000000002a6dd4433ffa96dc1755814fc0d9cc83a5f68dec0000000000000000000000007a299ad29499736994aa3a9afa3f476445faeb2c0000000000000000000000007efcb76d0e2e776a298aaa603d433336e5f8b6ab00000000000000000000000018fd37a4fb9e1f06d9383958afd236771f15a8cb000000000000000000000000302275067251f5fcdb9359bda735fd8f7a4a54c0", "updateBridgeEthTemplatesCalldata": "0xd94d6e0a000000000000000000000000b23214f241bdeb275f7dcbfbb1ea79349101d4b000000000000000000000000018ed2d5bf7c5943bfd20a2995b9879e30c9e8dda0000000000000000000000008f6406781cc955398c45a48dcefeebdb2c8e2caa000000000000000000000000f40c24ba346aa459ed28e196d4a46cf17174bd6c00000000000000000000000013be515e44eefaf3ebefad684f1fbb574ac0a494", "updateRollupCreatorTemplatesCalldata": "0xac9a97b400000000000000000000000019431dc37098877486532250fb3158140717c00c000000000000000000000000b20107bfb36d3b5aca534acafbd8857b10b402a80000000000000000000000005ca988f213efbcb86ed7e2aacb0c15c91e648f8d000000000000000000000000ee9e5546a11cb5b4a86e92da05f2ef75c26e47540000000000000000000000000ae4dd666748bf0f6db5c149eab1d8ad27820a6a000000000000000000000000660ea1675f7323dc3ba0c8ddfb593225eb01e3c10000000000000000000000006c21303f5986180b1394d2c89f3e883890e2867b0000000000000000000000002b0e04dc90e3fa58165cb41e2834b44a56e766af00000000000000000000000090d68b056c411015eae3ec0b98ad94e2c91419f1" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.2/output/1.json b/scripts/foundry/creator-upgrades/1.2.2/output/1.json index d5570972..3011dac7 100644 --- a/scripts/foundry/creator-upgrades/1.2.2/output/1.json +++ b/scripts/foundry/creator-upgrades/1.2.2/output/1.json @@ -3,4 +3,4 @@ "creatorCalldata": "0x99a88ec400000000000000000000000060d9a46f24d5a35b95a78dd3e793e55d94ee0660000000000000000000000000f39a8a43cffa0513cc057d290fa3e7a57dcd8d46", "retryableSenderCalldata": "0x99a88ec4000000000000000000000000a96b7f9e20a1f6e11815d4af08d911b21cb380ec0000000000000000000000008de0fb2651fdd10975088ae61a71cac6d372063d", "to": "0xE60081476E505F14C231a7efa47e607ff50dAEB5" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.2/output/11155111.json b/scripts/foundry/creator-upgrades/1.2.2/output/11155111.json index d2a92dd6..34103571 100644 --- a/scripts/foundry/creator-upgrades/1.2.2/output/11155111.json +++ b/scripts/foundry/creator-upgrades/1.2.2/output/11155111.json @@ -3,4 +3,4 @@ "creatorCalldata": "0x99a88ec40000000000000000000000007edb2dfbeef9417e0454a80c51ee0c034e45a570000000000000000000000000757143a7ed0dc76499607c7e5b0771965ae1fe06", "retryableSenderCalldata": "0x99a88ec40000000000000000000000002e9da5298ce57818caf96735bdcf900215c25d060000000000000000000000005d0e7fd5fca46aca13e475c070aa3e2f8eb01925", "to": "0xE58B76B21A98334CFD7FD6757102efe029E62Ed0" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.2/output/42161.json b/scripts/foundry/creator-upgrades/1.2.2/output/42161.json index f213f5ec..d6db95b3 100644 --- a/scripts/foundry/creator-upgrades/1.2.2/output/42161.json +++ b/scripts/foundry/creator-upgrades/1.2.2/output/42161.json @@ -3,4 +3,4 @@ "creatorCalldata": "0x99a88ec40000000000000000000000002f5624dc8800dfa0a82ac03509ef8bb8e7ac000e00000000000000000000000052d5181dd67ac17176127e670e5baee4d47c6c9e", "retryableSenderCalldata": "0x99a88ec4000000000000000000000000f9fbfc857d51ff51fedd4ea88efc29039871dccf0000000000000000000000009ea06b8753bca071a5c57002ab84598577fb08c1", "to": "0xBE95d0EE267f3E90606537b1C8A6Fb36d2DC1Ce6" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.2/output/421614.json b/scripts/foundry/creator-upgrades/1.2.2/output/421614.json index 4e5695c1..3418ce2e 100644 --- a/scripts/foundry/creator-upgrades/1.2.2/output/421614.json +++ b/scripts/foundry/creator-upgrades/1.2.2/output/421614.json @@ -3,4 +3,4 @@ "creatorCalldata": "0x99a88ec400000000000000000000000056c486d3786fa26cc61473c499a36eb9cc1fbd8e000000000000000000000000ec43416728f656ac5b7d860236bb102e2abe0f88", "retryableSenderCalldata": "0x99a88ec4000000000000000000000000a665705700774a40b73b4e3509ae01c7ef05ba0f0000000000000000000000009308a1264ab831002821971ac5fa342c4f775637", "to": "0x8E112dd87E71Ac9061caA2ccC2513027C3cF5D90" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.2/output/42170.json b/scripts/foundry/creator-upgrades/1.2.2/output/42170.json index 7cee8514..b4895a0e 100644 --- a/scripts/foundry/creator-upgrades/1.2.2/output/42170.json +++ b/scripts/foundry/creator-upgrades/1.2.2/output/42170.json @@ -3,4 +3,4 @@ "creatorCalldata": "0x99a88ec40000000000000000000000008b9d9490a68b1f16ac8a21ddae5fd7ab9d708c140000000000000000000000004998b99dd376a0cfff0e4b7f1ee0056f79910e64", "retryableSenderCalldata": "0x99a88ec4000000000000000000000000841a23c7c4e20515eaf03debd8ab60f12b5cc13e000000000000000000000000e60081476e505f14c231a7efa47e607ff50daeb5", "to": "0x211A5579D21e1938b2B5ff87a3F7896933543E97" -} \ No newline at end of file +} diff --git a/scripts/foundry/creator-upgrades/1.2.2/output/8453.json b/scripts/foundry/creator-upgrades/1.2.2/output/8453.json index 97838476..e74451f7 100644 --- a/scripts/foundry/creator-upgrades/1.2.2/output/8453.json +++ b/scripts/foundry/creator-upgrades/1.2.2/output/8453.json @@ -3,4 +3,4 @@ "creatorCalldata": "0x99a88ec40000000000000000000000004c240987d6fe4fa8c7a0004986e3db563150ca550000000000000000000000004f45074375ac881094003fecebac1f12169ffd96", "retryableSenderCalldata": "0x99a88ec4000000000000000000000000d106ec93d2c1adaa65c4b17ffc7bb166ce30ddae00000000000000000000000052767940bdbac734116fb2a2effeba01bfa82124", "to": "0x413Aa082995f0D7672C4d564624DdeBD221C8D0D" -} \ No newline at end of file +} diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 04e41444..4fa9e229 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -1,9 +1,14 @@ -import { Command } from 'commander'; -import * as path from 'path'; -import * as fs from 'fs'; -import { log, die } from '../utils/log'; -import { requireEnv, getEnv, getScriptsDir } from '../utils/env'; -import { parseAuthArgs, createDeployExecuteAuth, getDeployAuth, getExecuteAuth } from '../utils/auth'; +import { Command } from 'commander' +import * as path from 'path' +import * as fs from 'fs' +import { log, die } from '../utils/log' +import { requireEnv, getEnv, getScriptsDir } from '../utils/env' +import { + parseAuthArgs, + createDeployExecuteAuth, + getDeployAuth, + getExecuteAuth, +} from '../utils/auth' import { runForgeScript, runCastSend, @@ -11,62 +16,69 @@ import { castCalldata, getChainId, parseActionAddress, -} from '../utils/forge'; +} from '../utils/forge' -const ARBOS_DIR = path.join(getScriptsDir(), 'arbos-upgrades', 'at-timestamp'); -const DEPLOY_SCRIPT = path.join(ARBOS_DIR, 'DeployUpgradeArbOSVersionAtTimestampAction.s.sol'); +const ARBOS_DIR = path.join(getScriptsDir(), 'arbos-upgrades', 'at-timestamp') +const DEPLOY_SCRIPT = path.join( + ARBOS_DIR, + 'DeployUpgradeArbOSVersionAtTimestampAction.s.sol' +) // ArbOS precompile addresses -const ARB_OWNER_PUBLIC = '0x000000000000000000000000000000000000006b'; -const ARB_SYS = '0x0000000000000000000000000000000000000064'; +const ARB_OWNER_PUBLIC = '0x000000000000000000000000000000000000006b' +const ARB_SYS = '0x0000000000000000000000000000000000000064' // Nitro ArbOS versions are offset by 55 to avoid collision with classic (pre-Nitro) versions -const ARBOS_VERSION_OFFSET = 55; +const ARBOS_VERSION_OFFSET = 55 async function cmdDeploy(version: string, args: string[]): Promise { - const authArgs = parseAuthArgs(args); + const authArgs = parseAuthArgs(args) - const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') if (!fs.existsSync(DEPLOY_SCRIPT)) { - die(`Deploy script not found: ${DEPLOY_SCRIPT}`); + die(`Deploy script not found: ${DEPLOY_SCRIPT}`) } // Forge script reads this from env - process.env.ARBOS_VERSION = version; + process.env.ARBOS_VERSION = version - log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`); + log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) await runForgeScript({ script: DEPLOY_SCRIPT, rpcUrl, authArgs, - broadcast: !!authArgs, + broadcast: Boolean(authArgs), slow: true, - }); + }) } async function cmdExecute(args: string[]): Promise { - const authArgs = parseAuthArgs(args); + const authArgs = parseAuthArgs(args) - const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); - const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS'); - const actionAddress = requireEnv('UPGRADE_ACTION_ADDRESS'); + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') + const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') + const actionAddress = requireEnv('UPGRADE_ACTION_ADDRESS') - log(`Executing ArbOS upgrade action: ${actionAddress}`); + log(`Executing ArbOS upgrade action: ${actionAddress}`) - const performCalldata = '0xb0a75d36'; // perform() selector + const performCalldata = '0xb0a75d36' // perform() selector if (!authArgs) { // No auth - output calldata for multisig - const executeCalldata = await castCalldata('execute(address,bytes)', actionAddress, performCalldata); - - log('Calldata for UpgradeExecutor.execute():'); - console.log(''); - console.log(`To: ${upgradeExecutor}`); - console.log(`Calldata: ${executeCalldata}`); - console.log(''); - log('Submit this to your multisig/Safe to execute the upgrade'); + const executeCalldata = await castCalldata( + 'execute(address,bytes)', + actionAddress, + performCalldata + ) + + log('Calldata for UpgradeExecutor.execute():') + console.log('') + console.log(`To: ${upgradeExecutor}`) + console.log(`Calldata: ${executeCalldata}`) + console.log('') + log('Submit this to your multisig/Safe to execute the upgrade') } else { await runCastSend({ to: upgradeExecutor, @@ -74,97 +86,101 @@ async function cmdExecute(args: string[]): Promise { args: [actionAddress, performCalldata], rpcUrl, authArgs, - }); + }) - log('ArbOS upgrade scheduled successfully'); + log('ArbOS upgrade scheduled successfully') } } async function cmdVerify(): Promise { - const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') - log('Checking ArbOS upgrade status...'); + log('Checking ArbOS upgrade status...') const scheduled = await runCastCall({ to: ARB_OWNER_PUBLIC, sig: 'getScheduledUpgrade()(uint64,uint64)', rpcUrl, - }); - log(`Scheduled upgrade (version, timestamp): ${scheduled}`); + }) + log(`Scheduled upgrade (version, timestamp): ${scheduled}`) const currentRaw = await runCastCall({ to: ARB_SYS, sig: 'arbOSVersion()(uint64)', rpcUrl, - }); + }) - let currentVersion: number; + let currentVersion: number if (currentRaw === 'N/A') { - currentVersion = 0; + currentVersion = 0 } else { - const rawNum = parseInt(currentRaw, 10); - currentVersion = rawNum - ARBOS_VERSION_OFFSET; + const rawNum = parseInt(currentRaw, 10) + currentVersion = rawNum - ARBOS_VERSION_OFFSET } - log(`Current ArbOS version: ${currentVersion}`); + log(`Current ArbOS version: ${currentVersion}`) } async function cmdDeployExecuteVerify( version: string, options: { - deployKey?: string; - deployAccount?: string; - deployLedger?: boolean; - deployInteractive?: boolean; - executeKey?: string; - executeAccount?: string; - executeLedger?: boolean; - executeInteractive?: boolean; - dryRun?: boolean; - skipExecute?: boolean; - verify?: boolean; + deployKey?: string + deployAccount?: string + deployLedger?: boolean + deployInteractive?: boolean + executeKey?: string + executeAccount?: string + executeLedger?: boolean + executeInteractive?: boolean + dryRun?: boolean + skipExecute?: boolean + verify?: boolean } ): Promise { - const auth = createDeployExecuteAuth(options); + const auth = createDeployExecuteAuth(options) - log(`ArbOS version: ${version}`); + log(`ArbOS version: ${version}`) if (!fs.existsSync(DEPLOY_SCRIPT)) { - die(`Deploy script not found: ${DEPLOY_SCRIPT}`); + die(`Deploy script not found: ${DEPLOY_SCRIPT}`) } // Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set - let skipDeploy = !!getEnv('UPGRADE_ACTION_ADDRESS'); - let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || ''; + const skipDeploy = Boolean(getEnv('UPGRADE_ACTION_ADDRESS')) + let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || '' if (skipDeploy) { - log(`Using existing action from .env: ${upgradeActionAddress}`); + log(`Using existing action from .env: ${upgradeActionAddress}`) } - const rpcUrl = requireEnv('CHILD_CHAIN_RPC'); - const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS'); - requireEnv('SCHEDULE_TIMESTAMP'); + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') + const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') + requireEnv('SCHEDULE_TIMESTAMP') // Forge script reads this from env - process.env.ARBOS_VERSION = version; + process.env.ARBOS_VERSION = version - const deployAuth = getDeployAuth(auth); - const executeAuth = getExecuteAuth(auth); + const deployAuth = getDeployAuth(auth) + const executeAuth = getExecuteAuth(auth) if (!skipDeploy && !auth.dryRun && !deployAuth) { - die('Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive'); + die( + 'Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive' + ) } if (!auth.skipExecute && !auth.dryRun && !executeAuth) { - die('Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive'); + die( + 'Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive' + ) } - log(`Scheduled timestamp: ${process.env.SCHEDULE_TIMESTAMP}`); + log(`Scheduled timestamp: ${process.env.SCHEDULE_TIMESTAMP}`) - let chainId = ''; + let chainId = '' if (!skipDeploy) { - chainId = await getChainId(rpcUrl); - log(`Target chain ID: ${chainId}`); - log('Step 1: Deploying ArbOS upgrade action...'); + chainId = await getChainId(rpcUrl) + log(`Target chain ID: ${chainId}`) + log('Step 1: Deploying ArbOS upgrade action...') await runForgeScript({ script: DEPLOY_SCRIPT, @@ -173,36 +189,40 @@ async function cmdDeployExecuteVerify( broadcast: !auth.dryRun, verify: auth.verifyContracts, slow: true, - }); + }) if (!auth.dryRun) { - upgradeActionAddress = parseActionAddress(DEPLOY_SCRIPT, chainId); - log(`Deployed action at: ${upgradeActionAddress}`); + upgradeActionAddress = parseActionAddress(DEPLOY_SCRIPT, chainId) + log(`Deployed action at: ${upgradeActionAddress}`) } else { - log('Dry run - no action deployed'); + log('Dry run - no action deployed') if (!auth.skipExecute) { - log('Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step'); - return; + log('Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step') + return } } } else { - log('Step 1: Skipped deploy'); + log('Step 1: Skipped deploy') } if (!auth.skipExecute) { - log('Step 2: Executing ArbOS upgrade...'); + log('Step 2: Executing ArbOS upgrade...') - const performCalldata = '0xb0a75d36'; // perform() selector + const performCalldata = '0xb0a75d36' // perform() selector if (auth.dryRun) { - const executeCalldata = await castCalldata('execute(address,bytes)', upgradeActionAddress, performCalldata); - - log('Dry run - calldata for UpgradeExecutor.execute():'); - console.log(''); - console.log(`To: ${upgradeExecutor}`); - console.log(`Calldata: ${executeCalldata}`); - console.log(''); - log('Submit this to your multisig/Safe to execute the upgrade'); + const executeCalldata = await castCalldata( + 'execute(address,bytes)', + upgradeActionAddress, + performCalldata + ) + + log('Dry run - calldata for UpgradeExecutor.execute():') + console.log('') + console.log(`To: ${upgradeExecutor}`) + console.log(`Calldata: ${executeCalldata}`) + console.log('') + log('Submit this to your multisig/Safe to execute the upgrade') } else { await runCastSend({ to: upgradeExecutor, @@ -210,49 +230,53 @@ async function cmdDeployExecuteVerify( args: [upgradeActionAddress, performCalldata], rpcUrl, authArgs: executeAuth, - }); + }) - log('ArbOS upgrade scheduled successfully'); + log('ArbOS upgrade scheduled successfully') } } else { - log('Step 2: Skipped execute'); + log('Step 2: Skipped execute') } if (!auth.dryRun && !auth.skipExecute) { - log('Step 3: Verifying scheduled upgrade...'); + log('Step 3: Verifying scheduled upgrade...') const scheduled = await runCastCall({ to: ARB_OWNER_PUBLIC, sig: 'getScheduledUpgrade()(uint64,uint64)', rpcUrl, - }); - log(`Scheduled upgrade (version, timestamp): ${scheduled}`); + }) + log(`Scheduled upgrade (version, timestamp): ${scheduled}`) const currentRaw = await runCastCall({ to: ARB_SYS, sig: 'arbOSVersion()(uint64)', rpcUrl, - }); + }) - let currentVersion: number; + let currentVersion: number if (currentRaw === 'N/A') { - currentVersion = 0; + currentVersion = 0 } else { - const rawNum = parseInt(currentRaw, 10); - currentVersion = rawNum - ARBOS_VERSION_OFFSET; + const rawNum = parseInt(currentRaw, 10) + currentVersion = rawNum - ARBOS_VERSION_OFFSET } - log(`Current ArbOS version: ${currentVersion}`); + log(`Current ArbOS version: ${currentVersion}`) } - log('Done'); + log('Done') } export function createArbosUpgradeCommand(): Command { const cmd = new Command('arbos-upgrade') .description('ArbOS upgrade operations') .argument('', 'ArbOS version number') - .argument('[command]', 'Command: deploy, execute, verify, deploy-execute-verify', 'deploy-execute-verify') + .argument( + '[command]', + 'Command: deploy, execute, verify, deploy-execute-verify', + 'deploy-execute-verify' + ) .option('--private-key ', 'Private key (for deploy/execute)') .option('--account ', 'Keystore account (for deploy/execute)') .option('--ledger', 'Use Ledger (for deploy/execute)') @@ -269,31 +293,33 @@ export function createArbosUpgradeCommand(): Command { .option('--skip-execute', 'Deploy only') .option('-v, --verify', 'Verify on block explorer') .action(async (version: string, command: string, options) => { - const args: string[] = []; - if (options.privateKey) args.push('--private-key', options.privateKey); - if (options.account) args.push('--account', options.account); - if (options.ledger) args.push('--ledger'); - if (options.interactive) args.push('--interactive'); + const args: string[] = [] + if (options.privateKey) args.push('--private-key', options.privateKey) + if (options.account) args.push('--account', options.account) + if (options.ledger) args.push('--ledger') + if (options.interactive) args.push('--interactive') switch (command) { case 'deploy': - await cmdDeploy(version, args); - break; + await cmdDeploy(version, args) + break case 'execute': - await cmdExecute(args); - break; + await cmdExecute(args) + break case 'verify': - await cmdVerify(); - break; + await cmdVerify() + break case 'deploy-execute-verify': - await cmdDeployExecuteVerify(version, options); - break; + await cmdDeployExecuteVerify(version, options) + break default: - die(`Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify`); + die( + `Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify` + ) } - }); + }) - return cmd; + return cmd } -export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify }; +export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify } diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts index fc88618d..3d416573 100644 --- a/src/cli/commands/contract-upgrade.ts +++ b/src/cli/commands/contract-upgrade.ts @@ -1,148 +1,167 @@ -import { Command } from 'commander'; -import * as path from 'path'; -import * as fs from 'fs'; -import { log, die } from '../utils/log'; -import { requireEnv, getEnv, getScriptsDir } from '../utils/env'; -import { parseAuthArgs, createDeployExecuteAuth, getDeployAuth, getExecuteAuth } from '../utils/auth'; -import { runForgeScript, getChainId, parseActionAddress, findScript } from '../utils/forge'; - -const CONTRACTS_DIR = path.join(getScriptsDir(), 'contract-upgrades'); +import { Command } from 'commander' +import * as path from 'path' +import * as fs from 'fs' +import { log, die } from '../utils/log' +import { requireEnv, getEnv, getScriptsDir } from '../utils/env' +import { + parseAuthArgs, + createDeployExecuteAuth, + getDeployAuth, + getExecuteAuth, +} from '../utils/auth' +import { + runForgeScript, + getChainId, + parseActionAddress, + findScript, +} from '../utils/forge' + +const CONTRACTS_DIR = path.join(getScriptsDir(), 'contract-upgrades') function getVersionDir(version: string): string { - const versionDir = path.join(CONTRACTS_DIR, version); + const versionDir = path.join(CONTRACTS_DIR, version) if (!fs.existsSync(versionDir)) { const available = fs.existsSync(CONTRACTS_DIR) - ? fs.readdirSync(CONTRACTS_DIR).filter((f) => !f.startsWith('.')).join(' ') - : 'none found'; - die(`Unknown version: ${version}\n\nAvailable versions: ${available}`); + ? fs + .readdirSync(CONTRACTS_DIR) + .filter(f => !f.startsWith('.')) + .join(' ') + : 'none found' + die(`Unknown version: ${version}\n\nAvailable versions: ${available}`) } - return versionDir; + return versionDir } async function cmdDeploy(version: string, args: string[]): Promise { - const versionDir = getVersionDir(version); - const authArgs = parseAuthArgs(args); + const versionDir = getVersionDir(version) + const authArgs = parseAuthArgs(args) - const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); + const rpcUrl = requireEnv('PARENT_CHAIN_RPC') - const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/); + const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/) if (!deployScript) { - die(`No deploy script found in ${versionDir}`); + die(`No deploy script found in ${versionDir}`) } - log(`Running: ${path.basename(deployScript)}`); + log(`Running: ${path.basename(deployScript)}`) await runForgeScript({ script: deployScript, rpcUrl, authArgs, - broadcast: !!authArgs, + broadcast: Boolean(authArgs), slow: true, skipSimulation: true, - }); + }) } async function cmdExecute(version: string, args: string[]): Promise { - const versionDir = getVersionDir(version); - const authArgs = parseAuthArgs(args); + const versionDir = getVersionDir(version) + const authArgs = parseAuthArgs(args) - const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); - requireEnv('UPGRADE_ACTION_ADDRESS'); + const rpcUrl = requireEnv('PARENT_CHAIN_RPC') + requireEnv('UPGRADE_ACTION_ADDRESS') - const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/); + const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/) if (!executeScript) { - die(`No execute script found in ${versionDir}`); + die(`No execute script found in ${versionDir}`) } - log(`Running: ${path.basename(executeScript)}`); + log(`Running: ${path.basename(executeScript)}`) await runForgeScript({ script: executeScript, rpcUrl, authArgs, - broadcast: !!authArgs, - }); + broadcast: Boolean(authArgs), + }) } async function cmdVerify(version: string): Promise { - const versionDir = getVersionDir(version); + const versionDir = getVersionDir(version) - const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); + const rpcUrl = requireEnv('PARENT_CHAIN_RPC') - const verifyScript = findScript(versionDir, /^Verify.*\.s\.sol$/); + const verifyScript = findScript(versionDir, /^Verify.*\.s\.sol$/) if (!verifyScript) { - die(`No verify script found in ${versionDir} - check README for manual verification`); + die( + `No verify script found in ${versionDir} - check README for manual verification` + ) } - log(`Running: ${path.basename(verifyScript)}`); + log(`Running: ${path.basename(verifyScript)}`) await runForgeScript({ script: verifyScript, rpcUrl, - }); + }) } async function cmdDeployExecuteVerify( version: string, options: { - deployKey?: string; - deployAccount?: string; - deployLedger?: boolean; - deployInteractive?: boolean; - executeKey?: string; - executeAccount?: string; - executeLedger?: boolean; - executeInteractive?: boolean; - dryRun?: boolean; - skipExecute?: boolean; - verify?: boolean; + deployKey?: string + deployAccount?: string + deployLedger?: boolean + deployInteractive?: boolean + executeKey?: string + executeAccount?: string + executeLedger?: boolean + executeInteractive?: boolean + dryRun?: boolean + skipExecute?: boolean + verify?: boolean } ): Promise { - const versionDir = getVersionDir(version); - const auth = createDeployExecuteAuth(options); + const versionDir = getVersionDir(version) + const auth = createDeployExecuteAuth(options) - const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/); - const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/); + const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/) + const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/) if (!deployScript) { - die(`No deploy script found in ${versionDir}`); + die(`No deploy script found in ${versionDir}`) } if (!executeScript) { - die(`No execute script found in ${versionDir}`); + die(`No execute script found in ${versionDir}`) } - log(`Version: ${version}`); - log(`Deploy script: ${path.basename(deployScript)}`); - log(`Execute script: ${path.basename(executeScript)}`); + log(`Version: ${version}`) + log(`Deploy script: ${path.basename(deployScript)}`) + log(`Execute script: ${path.basename(executeScript)}`) // Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set - let skipDeploy = !!getEnv('UPGRADE_ACTION_ADDRESS'); - let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || ''; + const skipDeploy = Boolean(getEnv('UPGRADE_ACTION_ADDRESS')) + let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || '' if (skipDeploy) { - log(`Using existing action from .env: ${upgradeActionAddress}`); + log(`Using existing action from .env: ${upgradeActionAddress}`) } - const rpcUrl = requireEnv('PARENT_CHAIN_RPC'); - requireEnv('INBOX_ADDRESS'); - requireEnv('PROXY_ADMIN_ADDRESS'); - requireEnv('PARENT_UPGRADE_EXECUTOR_ADDRESS'); + const rpcUrl = requireEnv('PARENT_CHAIN_RPC') + requireEnv('INBOX_ADDRESS') + requireEnv('PROXY_ADMIN_ADDRESS') + requireEnv('PARENT_UPGRADE_EXECUTOR_ADDRESS') - const deployAuth = getDeployAuth(auth); - const executeAuth = getExecuteAuth(auth); + const deployAuth = getDeployAuth(auth) + const executeAuth = getExecuteAuth(auth) if (!skipDeploy && !auth.dryRun && !deployAuth) { - die('Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive'); + die( + 'Deploy auth required. Use --deploy-key, --deploy-account, --deploy-ledger, or --deploy-interactive' + ) } if (!auth.skipExecute && !auth.dryRun && !executeAuth) { - die('Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive'); + die( + 'Execute auth required. Use --execute-key, --execute-account, --execute-ledger, or --execute-interactive' + ) } - const chainId = await getChainId(rpcUrl); - log(`Target chain ID: ${chainId}`); + const chainId = await getChainId(rpcUrl) + log(`Target chain ID: ${chainId}`) if (!skipDeploy) { - log('Step 1: Deploying upgrade action...'); + log('Step 1: Deploying upgrade action...') await runForgeScript({ script: deployScript, @@ -152,66 +171,69 @@ async function cmdDeployExecuteVerify( verify: auth.verifyContracts, slow: true, skipSimulation: true, - }); + }) if (!auth.dryRun) { - upgradeActionAddress = parseActionAddress(deployScript, chainId); - log(`Deployed action at: ${upgradeActionAddress}`); + upgradeActionAddress = parseActionAddress(deployScript, chainId) + log(`Deployed action at: ${upgradeActionAddress}`) } else { - log('Dry run - no action deployed'); + log('Dry run - no action deployed') if (!auth.skipExecute) { - log('Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step'); - return; + log('Note: Set UPGRADE_ACTION_ADDRESS in .env to run execute step') + return } } } else { - log('Step 1: Skipped deploy'); + log('Step 1: Skipped deploy') } // Forge script reads this from env - process.env.UPGRADE_ACTION_ADDRESS = upgradeActionAddress; + process.env.UPGRADE_ACTION_ADDRESS = upgradeActionAddress if (!auth.skipExecute) { - log('Step 2: Executing upgrade...'); + log('Step 2: Executing upgrade...') await runForgeScript({ script: executeScript, rpcUrl, authArgs: executeAuth, broadcast: !auth.dryRun, - }); + }) if (auth.dryRun) { - log('Dry run - upgrade not executed'); + log('Dry run - upgrade not executed') } else { - log('Upgrade executed successfully'); + log('Upgrade executed successfully') } } else { - log('Step 2: Skipped execute'); + log('Step 2: Skipped execute') } if (!auth.dryRun && !auth.skipExecute) { - log('Step 3: Verifying upgrade...'); + log('Step 3: Verifying upgrade...') - const verifyScript = findScript(versionDir, /^Verify.*\.s\.sol$/); + const verifyScript = findScript(versionDir, /^Verify.*\.s\.sol$/) if (verifyScript) { await runForgeScript({ script: verifyScript, rpcUrl, - }); + }) } else { - log('No Verify script found - check README for manual verification'); + log('No Verify script found - check README for manual verification') } } - log('Done'); + log('Done') } export function createContractUpgradeCommand(): Command { const cmd = new Command('contract-upgrade') .description('Contract upgrade operations') .argument('', 'Contract version (e.g., 1.2.1)') - .argument('', 'Command: deploy, execute, verify, deploy-execute-verify') + .argument( + '', + 'Command: deploy, execute, verify, deploy-execute-verify' + ) .option('--private-key ', 'Private key (for deploy/execute)') .option('--account ', 'Keystore account (for deploy/execute)') .option('--ledger', 'Use Ledger (for deploy/execute)') @@ -228,31 +250,39 @@ export function createContractUpgradeCommand(): Command { .option('--skip-execute', 'Deploy only') .option('-v, --verify', 'Verify on block explorer') .action(async (version: string, command: string, options) => { - const args: string[] = []; - if (options.privateKey) args.push('--private-key', options.privateKey); - if (options.account) args.push('--account', options.account); - if (options.ledger) args.push('--ledger'); - if (options.interactive) args.push('--interactive'); + const args: string[] = [] + if (options.privateKey) args.push('--private-key', options.privateKey) + if (options.account) args.push('--account', options.account) + if (options.ledger) args.push('--ledger') + if (options.interactive) args.push('--interactive') switch (command) { case 'deploy': - await cmdDeploy(version, args); - break; + await cmdDeploy(version, args) + break case 'execute': - await cmdExecute(version, args); - break; + await cmdExecute(version, args) + break case 'verify': - await cmdVerify(version); - break; + await cmdVerify(version) + break case 'deploy-execute-verify': - await cmdDeployExecuteVerify(version, options); - break; + await cmdDeployExecuteVerify(version, options) + break default: - die(`Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify`); + die( + `Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify` + ) } - }); + }) - return cmd; + return cmd } -export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify, getVersionDir }; +export { + cmdDeploy, + cmdExecute, + cmdVerify, + cmdDeployExecuteVerify, + getVersionDir, +} diff --git a/src/cli/index.ts b/src/cli/index.ts index 5f97048f..0b009dc6 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,12 +1,12 @@ #!/usr/bin/env node -import { program } from 'commander'; -import { loadEnv } from './utils/env'; -import { router } from './router'; -import { createContractUpgradeCommand } from './commands/contract-upgrade'; -import { createArbosUpgradeCommand } from './commands/arbos-upgrade'; +import { program } from 'commander' +import { loadEnv } from './utils/env' +import { router } from './router' +import { createContractUpgradeCommand } from './commands/contract-upgrade' +import { createArbosUpgradeCommand } from './commands/arbos-upgrade' -loadEnv(); +loadEnv() program .name('orbit-actions') @@ -15,11 +15,10 @@ program .argument('[args...]', 'Additional arguments') .allowUnknownOption(true) .action(async (pathArg?: string, args?: string[]) => { - await router(pathArg, args); - }); + await router(pathArg, args) + }) -program.addCommand(createContractUpgradeCommand()); -program.addCommand(createArbosUpgradeCommand()); - -program.parse(); +program.addCommand(createContractUpgradeCommand()) +program.addCommand(createArbosUpgradeCommand()) +program.parse() diff --git a/src/cli/router.ts b/src/cli/router.ts index d8a9617a..606e9821 100644 --- a/src/cli/router.ts +++ b/src/cli/router.ts @@ -1,19 +1,19 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { die } from './utils/log'; -import { getScriptsDir } from './utils/env'; +import * as fs from 'fs' +import * as path from 'path' +import { die } from './utils/log' +import { getScriptsDir } from './utils/env' import { cmdDeploy as contractDeploy, cmdExecute as contractExecute, cmdVerify as contractVerify, cmdDeployExecuteVerify as contractDeployExecuteVerify, -} from './commands/contract-upgrade'; +} from './commands/contract-upgrade' import { cmdDeploy as arbosDeploy, cmdExecute as arbosExecute, cmdVerify as arbosVerify, cmdDeployExecuteVerify as arbosDeployExecuteVerify, -} from './commands/arbos-upgrade'; +} from './commands/arbos-upgrade' const HELP_TEXT = `Usage: orbit-actions [path] [args...] @@ -55,110 +55,120 @@ Options for deploy-execute-verify: Examples: docker run orbit-actions contract-upgrades/1.2.1 docker run orbit-actions contract-upgrades/1.2.1/README.md - docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/deploy-execute-verify --dry-run`; + docker run -v $(pwd)/.env:/app/.env orbit-actions contract-upgrades/1.2.1/deploy-execute-verify --dry-run` function listDirectory(dir: string): void { - const scriptsDir = getScriptsDir(); - let relPath = path.relative(scriptsDir, dir); - if (relPath === '.') relPath = ''; + const scriptsDir = getScriptsDir() + let relPath = path.relative(scriptsDir, dir) + if (relPath === '.') relPath = '' - const contents = fs.readdirSync(dir); + const contents = fs.readdirSync(dir) for (const item of contents) { if (!item.startsWith('.')) { - console.log(item); + console.log(item) } } if (/^contract-upgrades\/[0-9]/.test(relPath)) { - console.log('---'); - console.log('deploy (run Deploy script)'); - console.log('execute (run Execute script)'); - console.log('verify (run Verify script)'); - console.log('deploy-execute-verify (full upgrade flow)'); + console.log('---') + console.log('deploy (run Deploy script)') + console.log('execute (run Execute script)') + console.log('verify (run Verify script)') + console.log('deploy-execute-verify (full upgrade flow)') } else if (relPath === 'arbos-upgrades/at-timestamp') { - console.log('---'); - console.log('deploy (run Deploy script)'); - console.log('execute (execute upgrade action)'); - console.log('verify (check upgrade status)'); - console.log('deploy-execute-verify (full upgrade flow)'); + console.log('---') + console.log('deploy (run Deploy script)') + console.log('execute (execute upgrade action)') + console.log('verify (check upgrade status)') + console.log('deploy-execute-verify (full upgrade flow)') } } function parseOptions(args: string[]): Record { - const options: Record = {}; + const options: Record = {} for (let i = 0; i < args.length; i++) { - const arg = args[i]; - if (arg === '--deploy-key' || arg === '--execute-key' || arg === '--deploy-account' || arg === '--execute-account') { - options[arg.replace(/^--/, '').replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = args[++i] || ''; + const arg = args[i] + if ( + arg === '--deploy-key' || + arg === '--execute-key' || + arg === '--deploy-account' || + arg === '--execute-account' + ) { + options[ + arg.replace(/^--/, '').replace(/-([a-z])/g, (_, c) => c.toUpperCase()) + ] = args[++i] || '' } else if (arg === '--deploy-ledger') { - options.deployLedger = true; + options.deployLedger = true } else if (arg === '--execute-ledger') { - options.executeLedger = true; + options.executeLedger = true } else if (arg === '--deploy-interactive') { - options.deployInteractive = true; + options.deployInteractive = true } else if (arg === '--execute-interactive') { - options.executeInteractive = true; + options.executeInteractive = true } else if (arg === '--dry-run' || arg === '-n') { - options.dryRun = true; + options.dryRun = true } else if (arg === '--skip-execute') { - options.skipExecute = true; + options.skipExecute = true } else if (arg === '--verify' || arg === '-v') { - options.verify = true; + options.verify = true } else if (arg === '--private-key') { - options.privateKey = args[++i] || ''; + options.privateKey = args[++i] || '' } else if (arg === '--account') { - options.account = args[++i] || ''; + options.account = args[++i] || '' } else if (arg === '--ledger') { - options.ledger = true; + options.ledger = true } else if (arg === '--interactive') { - options.interactive = true; + options.interactive = true } } - return options; + return options } -export async function router(pathArg?: string, args: string[] = []): Promise { - const scriptsDir = getScriptsDir(); +export async function router( + pathArg?: string, + args: string[] = [] +): Promise { + const scriptsDir = getScriptsDir() if (!pathArg) { - const contents = fs.readdirSync(scriptsDir); + const contents = fs.readdirSync(scriptsDir) for (const item of contents) { if (!item.startsWith('.')) { - console.log(item); + console.log(item) } } - return; + return } if (pathArg === 'help' || pathArg === '--help' || pathArg === '-h') { - console.log(HELP_TEXT); - return; + console.log(HELP_TEXT) + return } - const fullPath = path.join(scriptsDir, pathArg); - const parentPath = path.dirname(fullPath); - const basename = path.basename(pathArg); + const fullPath = path.join(scriptsDir, pathArg) + const parentPath = path.dirname(fullPath) + const basename = path.basename(pathArg) if (!fs.existsSync(fullPath) && fs.existsSync(parentPath)) { - const relParent = path.relative(scriptsDir, parentPath); + const relParent = path.relative(scriptsDir, parentPath) if (/^contract-upgrades\/[0-9]/.test(relParent)) { - const version = path.basename(relParent); - const options = parseOptions(args); + const version = path.basename(relParent) + const options = parseOptions(args) switch (basename) { case 'deploy': - await contractDeploy(version, args); - return; + await contractDeploy(version, args) + return case 'execute': - await contractExecute(version, args); - return; + await contractExecute(version, args) + return case 'verify': - await contractVerify(version); - return; + await contractVerify(version) + return case 'deploy-execute-verify': - await contractDeployExecuteVerify(version, options); - return; + await contractDeployExecuteVerify(version, options) + return } } @@ -166,43 +176,45 @@ export async function router(pathArg?: string, args: string[] = []): Promise [options]`); - process.exit(1); + console.error(`Error: ArbOS version required`) + console.error( + `Usage: arbos-upgrades/at-timestamp/${basename} [options]` + ) + process.exit(1) } - const restArgs = args.slice(1); - const restOptions = parseOptions(restArgs); + const restArgs = args.slice(1) + const restOptions = parseOptions(restArgs) if (basename === 'deploy') { - await arbosDeploy(version, restArgs); + await arbosDeploy(version, restArgs) } else { - await arbosDeployExecuteVerify(version, restOptions); + await arbosDeployExecuteVerify(version, restOptions) } - return; + return } case 'execute': - await arbosExecute(args); - return; + await arbosExecute(args) + return case 'verify': - await arbosVerify(); - return; + await arbosVerify() + return } } } if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) { - listDirectory(fullPath); - return; + listDirectory(fullPath) + return } if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) { - const content = fs.readFileSync(fullPath, 'utf-8'); - console.log(content); - return; + const content = fs.readFileSync(fullPath, 'utf-8') + console.log(content) + return } die(`Not found: ${pathArg} -Use 'help' to see available commands.`); +Use 'help' to see available commands.`) } diff --git a/src/cli/utils/auth.ts b/src/cli/utils/auth.ts index dca9a629..7e719674 100644 --- a/src/cli/utils/auth.ts +++ b/src/cli/utils/auth.ts @@ -1,77 +1,77 @@ export interface DeployExecuteAuth { - deployKey: string; - deployAccount: string; - deployLedger: boolean; - deployInteractive: boolean; - executeKey: string; - executeAccount: string; - executeLedger: boolean; - executeInteractive: boolean; - dryRun: boolean; - skipExecute: boolean; - verifyContracts: boolean; + deployKey: string + deployAccount: string + deployLedger: boolean + deployInteractive: boolean + executeKey: string + executeAccount: string + executeLedger: boolean + executeInteractive: boolean + dryRun: boolean + skipExecute: boolean + verifyContracts: boolean } export function parseAuthArgs(args: string[]): string { for (let i = 0; i < args.length; i++) { - const arg = args[i]; + const arg = args[i] if (arg === '--private-key' || arg === '--account') { - const value = args[i + 1]; + const value = args[i + 1] if (value) { - return `${arg} ${value}`; + return `${arg} ${value}` } } if (arg === '--ledger' || arg === '--interactive') { - return arg; + return arg } } - return ''; + return '' } export function getDeployAuth(auth: DeployExecuteAuth): string { if (auth.deployKey) { - return `--private-key ${auth.deployKey}`; + return `--private-key ${auth.deployKey}` } if (auth.deployAccount) { - return `--account ${auth.deployAccount}`; + return `--account ${auth.deployAccount}` } if (auth.deployLedger) { - return '--ledger'; + return '--ledger' } if (auth.deployInteractive) { - return '--interactive'; + return '--interactive' } - return ''; + return '' } export function getExecuteAuth(auth: DeployExecuteAuth): string { if (auth.executeKey) { - return `--private-key ${auth.executeKey}`; + return `--private-key ${auth.executeKey}` } if (auth.executeAccount) { - return `--account ${auth.executeAccount}`; + return `--account ${auth.executeAccount}` } if (auth.executeLedger) { - return '--ledger'; + return '--ledger' } if (auth.executeInteractive) { - return '--interactive'; + return '--interactive' } - return ''; + return '' } export function createDeployExecuteAuth(options: { - deployKey?: string; - deployAccount?: string; - deployLedger?: boolean; - deployInteractive?: boolean; - executeKey?: string; - executeAccount?: string; - executeLedger?: boolean; - executeInteractive?: boolean; - dryRun?: boolean; - skipExecute?: boolean; - verify?: boolean; + deployKey?: string + deployAccount?: string + deployLedger?: boolean + deployInteractive?: boolean + executeKey?: string + executeAccount?: string + executeLedger?: boolean + executeInteractive?: boolean + dryRun?: boolean + skipExecute?: boolean + verify?: boolean }): DeployExecuteAuth { return { deployKey: options.deployKey || '', @@ -85,5 +85,5 @@ export function createDeployExecuteAuth(options: { dryRun: options.dryRun || false, skipExecute: options.skipExecute || false, verifyContracts: options.verify || false, - }; + } } diff --git a/src/cli/utils/env.ts b/src/cli/utils/env.ts index 44e61636..4bc8e22b 100644 --- a/src/cli/utils/env.ts +++ b/src/cli/utils/env.ts @@ -1,57 +1,57 @@ -import * as dotenv from 'dotenv'; -import * as fs from 'fs'; -import * as path from 'path'; -import { die } from './log'; +import * as dotenv from 'dotenv' +import * as fs from 'fs' +import * as path from 'path' +import { die } from './log' function findRepoRoot(): string | null { - let dir = __dirname; + let dir = __dirname for (let i = 0; i < 10; i++) { if (fs.existsSync(path.join(dir, 'package.json'))) { - return dir; + return dir } - const parent = path.dirname(dir); - if (parent === dir) break; - dir = parent; + const parent = path.dirname(dir) + if (parent === dir) break + dir = parent } - return null; + return null } export function loadEnv(): void { - const repoRoot = findRepoRoot(); + const repoRoot = findRepoRoot() const candidates = [ path.join(process.cwd(), '.env'), repoRoot ? path.join(repoRoot, '.env') : null, '/app/.env', - ].filter((p): p is string => p !== null); + ].filter((p): p is string => p !== null) for (const envPath of candidates) { if (fs.existsSync(envPath)) { - dotenv.config({ path: envPath }); - return; + dotenv.config({ path: envPath }) + return } } } export function requireEnv(name: string): string { - const value = process.env[name]; + const value = process.env[name] if (!value) { - die(`Required env var not set: ${name} (check your .env file)`); + die(`Required env var not set: ${name} (check your .env file)`) } - return value; + return value } export function getEnv(name: string): string | undefined { - return process.env[name]; + return process.env[name] } export function getScriptsDir(): string { - const repoRoot = findRepoRoot(); + const repoRoot = findRepoRoot() if (repoRoot) { - return path.join(repoRoot, 'scripts', 'foundry'); + return path.join(repoRoot, 'scripts', 'foundry') } - return '/app/scripts/foundry'; + return '/app/scripts/foundry' } export function getRepoRoot(): string { - return findRepoRoot() || '/app'; + return findRepoRoot() || '/app' } diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts index eaac011b..b4e93473 100644 --- a/src/cli/utils/forge.ts +++ b/src/cli/utils/forge.ts @@ -1,142 +1,168 @@ -import execa from 'execa'; -import * as fs from 'fs'; -import * as path from 'path'; -import { die, log } from './log'; -import { getRepoRoot } from './env'; +import execa from 'execa' +import * as fs from 'fs' +import * as path from 'path' +import { die, log } from './log' +import { getRepoRoot } from './env' export interface ForgeScriptOptions { - script: string; - rpcUrl: string; - authArgs?: string; - broadcast?: boolean; - verify?: boolean; - slow?: boolean; - skipSimulation?: boolean; - verbosity?: number; + script: string + rpcUrl: string + authArgs?: string + broadcast?: boolean + verify?: boolean + slow?: boolean + skipSimulation?: boolean + verbosity?: number } -export async function runForgeScript(options: ForgeScriptOptions): Promise { - const args = ['script', options.script, '--rpc-url', options.rpcUrl]; +export async function runForgeScript( + options: ForgeScriptOptions +): Promise { + const args = ['script', options.script, '--rpc-url', options.rpcUrl] if (options.slow) { - args.push('--slow'); + args.push('--slow') } if (options.skipSimulation) { - args.push('--skip-simulation'); + args.push('--skip-simulation') } - const verbosity = options.verbosity ?? 3; - args.push('-' + 'v'.repeat(verbosity)); + const verbosity = options.verbosity ?? 3 + args.push('-' + 'v'.repeat(verbosity)) if (options.broadcast && options.authArgs) { - args.push('--broadcast'); - args.push(...options.authArgs.split(' ').filter(Boolean)); + args.push('--broadcast') + args.push(...options.authArgs.split(' ').filter(Boolean)) } if (options.verify) { - args.push('--verify'); + args.push('--verify') } - log(`Running: forge ${args.slice(0, 2).join(' ')}...`); + log(`Running: forge ${args.slice(0, 2).join(' ')}...`) const result = await execa('forge', args, { stdio: 'inherit', env: process.env, - }); + }) if (result.exitCode !== 0) { - die(`Forge script failed with exit code ${result.exitCode}`); + die(`Forge script failed with exit code ${result.exitCode}`) } } export interface CastSendOptions { - to: string; - sig: string; - args: string[]; - rpcUrl: string; - authArgs?: string; + to: string + sig: string + args: string[] + rpcUrl: string + authArgs?: string } export async function runCastSend(options: CastSendOptions): Promise { - const args = ['send', options.to, options.sig, ...options.args, '--rpc-url', options.rpcUrl]; + const args = [ + 'send', + options.to, + options.sig, + ...options.args, + '--rpc-url', + options.rpcUrl, + ] if (options.authArgs) { - args.push(...options.authArgs.split(' ').filter(Boolean)); + args.push(...options.authArgs.split(' ').filter(Boolean)) } const result = await execa('cast', args, { stdio: 'inherit', env: process.env, - }); + }) if (result.exitCode !== 0) { - die(`Cast send failed with exit code ${result.exitCode}`); + die(`Cast send failed with exit code ${result.exitCode}`) } } export interface CastCallOptions { - to: string; - sig: string; - rpcUrl: string; + to: string + sig: string + rpcUrl: string } export async function runCastCall(options: CastCallOptions): Promise { try { - const result = await execa('cast', ['call', '--rpc-url', options.rpcUrl, options.to, options.sig]); - return result.stdout; + const result = await execa('cast', [ + 'call', + '--rpc-url', + options.rpcUrl, + options.to, + options.sig, + ]) + return result.stdout } catch { - return 'N/A'; + return 'N/A' } } -export async function castCalldata(sig: string, ...args: string[]): Promise { - const result = await execa('cast', ['calldata', sig, ...args]); - return result.stdout; +export async function castCalldata( + sig: string, + ...args: string[] +): Promise { + const result = await execa('cast', ['calldata', sig, ...args]) + return result.stdout } export async function getChainId(rpcUrl: string): Promise { - const result = await execa('cast', ['chain-id', '--rpc-url', rpcUrl]); - return result.stdout.trim(); + const result = await execa('cast', ['chain-id', '--rpc-url', rpcUrl]) + return result.stdout.trim() } -export function parseActionAddress(scriptPath: string, chainId: string): string { - const scriptName = path.basename(scriptPath); - const repoRoot = getRepoRoot(); - const broadcastFile = path.join(repoRoot, 'broadcast', scriptName, chainId, 'run-latest.json'); +export function parseActionAddress( + scriptPath: string, + chainId: string +): string { + const scriptName = path.basename(scriptPath) + const repoRoot = getRepoRoot() + const broadcastFile = path.join( + repoRoot, + 'broadcast', + scriptName, + chainId, + 'run-latest.json' + ) if (!fs.existsSync(broadcastFile)) { - die(`Broadcast file not found: ${broadcastFile}`); + die(`Broadcast file not found: ${broadcastFile}`) } - const content = JSON.parse(fs.readFileSync(broadcastFile, 'utf-8')); + const content = JSON.parse(fs.readFileSync(broadcastFile, 'utf-8')) const createTxs = content.transactions?.filter( (tx: { transactionType: string }) => tx.transactionType === 'CREATE' - ); + ) if (!createTxs || createTxs.length === 0) { - die('Could not parse action address from broadcast file'); + die('Could not parse action address from broadcast file') } - const address = createTxs[createTxs.length - 1]?.contractAddress; + const address = createTxs[createTxs.length - 1]?.contractAddress if (!address) { - die('Could not parse action address from broadcast file'); + die('Could not parse action address from broadcast file') } - return address; + return address } export function findScript(dir: string, pattern: RegExp): string | null { if (!fs.existsSync(dir)) { - return null; + return null } - const files = fs.readdirSync(dir); + const files = fs.readdirSync(dir) for (const file of files) { if (pattern.test(file) && file.endsWith('.s.sol')) { - return path.join(dir, file); + return path.join(dir, file) } } - return null; + return null } - diff --git a/src/cli/utils/log.ts b/src/cli/utils/log.ts index 9df2a6c6..355c1699 100644 --- a/src/cli/utils/log.ts +++ b/src/cli/utils/log.ts @@ -1,10 +1,10 @@ -const PREFIX = '[orbit-actions]'; +const PREFIX = '[orbit-actions]' export function log(message: string): void { - console.log(`${PREFIX} ${message}`); + console.log(`${PREFIX} ${message}`) } export function die(message: string): never { - console.error(`Error: ${message}`); - process.exit(1); + console.error(`Error: ${message}`) + process.exit(1) } diff --git a/test/docker/test-docker.bash b/test/docker/test-docker.bash index 03167239..d24cd210 100755 --- a/test/docker/test-docker.bash +++ b/test/docker/test-docker.bash @@ -25,23 +25,23 @@ run_test() { fi } -# Test 1: Tools are installed (passthrough) -echo "--- Tool Passthrough ---" -run_test "forge" docker run --rm "$IMAGE_NAME" forge --version -run_test "cast" docker run --rm "$IMAGE_NAME" cast --version -run_test "yarn" docker run --rm "$IMAGE_NAME" yarn --version -run_test "node" docker run --rm "$IMAGE_NAME" node --version +# Test 1: Tools are installed (via --entrypoint) +echo "--- Tools Installed ---" +run_test "forge" docker run --rm --entrypoint forge "$IMAGE_NAME" --version +run_test "cast" docker run --rm --entrypoint cast "$IMAGE_NAME" --version +run_test "yarn" docker run --rm --entrypoint yarn "$IMAGE_NAME" --version +run_test "node" docker run --rm --entrypoint node "$IMAGE_NAME" --version # Test 2: Dependencies are installed echo "" echo "--- Dependencies ---" -run_test "node_modules exists" docker run --rm "$IMAGE_NAME" test -d node_modules -run_test "forge dependencies" docker run --rm "$IMAGE_NAME" test -d node_modules/@arbitrum +run_test "node_modules exists" docker run --rm --entrypoint test "$IMAGE_NAME" -d node_modules +run_test "forge dependencies" docker run --rm --entrypoint test "$IMAGE_NAME" -d node_modules/@arbitrum # Test 3: Contracts compile echo "" echo "--- Contract Compilation ---" -run_test "contracts built" docker run --rm "$IMAGE_NAME" test -d out +run_test "contracts built" docker run --rm --entrypoint test "$IMAGE_NAME" -d out # Test 4: Browsing - list directories echo "" @@ -119,13 +119,13 @@ run_test "help command" docker run --rm "$IMAGE_NAME" help # Test 7: Yarn scripts work echo "" echo "--- Yarn Scripts ---" -run_test "yarn orbit:contracts:version --help" docker run --rm "$IMAGE_NAME" yarn orbit:contracts:version --help +run_test "yarn orbit:contracts:version --help" docker run --rm --entrypoint yarn "$IMAGE_NAME" orbit:contracts:version --help # Test 8: Unit tests pass echo "" echo "--- Unit Tests ---" echo "Running unit tests inside container..." -if docker run --rm "$IMAGE_NAME" yarn test:unit; then +if docker run --rm --entrypoint yarn "$IMAGE_NAME" test:unit; then echo "Unit tests: OK" else echo "Unit tests: FAILED" From 92fbc3121dafbc0aebc37c3fc1dcde06d0aad9d6 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Feb 2026 15:39:06 +0000 Subject: [PATCH 13/21] test: add local smoke tests for CLI Non-Docker tests for the bin/router and related scripts. --- test/local/test-local.bash | 83 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100755 test/local/test-local.bash diff --git a/test/local/test-local.bash b/test/local/test-local.bash new file mode 100755 index 00000000..ef5358ec --- /dev/null +++ b/test/local/test-local.bash @@ -0,0 +1,83 @@ +#!/bin/bash +set -euo pipefail + +# Local (non-Docker) smoke tests for orbit-actions CLI +# Tests the bin/router and related scripts directly + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +ROUTER="$REPO_ROOT/bin/router" + +echo "=== Local Smoke Tests ===" +echo "Router: $ROUTER" +echo "" + +PASSED=0 +FAILED=0 + +check() { + local name="$1" + shift + printf "Testing %s... " "$name" + if "$@" >/dev/null 2>&1; then + echo "OK" + PASSED=$((PASSED + 1)) + else + echo "FAILED" + FAILED=$((FAILED + 1)) + fi +} + +check_output() { + local name="$1" + local expected="$2" + shift 2 + printf "Testing %s... " "$name" + # Disable pipefail for this check - it interferes with if/pipe/grep + if (set +o pipefail; "$@" 2>&1 | grep -q "$expected"); then + echo "OK" + PASSED=$((PASSED + 1)) + else + echo "FAILED (expected: $expected)" + FAILED=$((FAILED + 1)) + fi +} + +echo "--- Prerequisites ---" +check "forge installed" command -v forge +check "cast installed" command -v cast +check "jq installed" command -v jq + +echo "" +echo "--- Directory Browsing ---" +check "list top level" "$ROUTER" +check_output "list contract-upgrades" "1.2.1" "$ROUTER" contract-upgrades +check_output "list contract-upgrades/1.2.1" "deploy" "$ROUTER" contract-upgrades/1.2.1 +check_output "list arbos-upgrades" "at-timestamp" "$ROUTER" arbos-upgrades + +echo "" +echo "--- File Viewing ---" +check_output "view README" "Nitro contracts" "$ROUTER" contract-upgrades/1.2.1/README.md + +echo "" +echo "--- Help ---" +check_output "help command" "Usage:" "$ROUTER" help +check_output "contract-upgrade help" "deploy-execute-verify" "$REPO_ROOT/bin/contract-upgrade" --help +check_output "arbos-upgrade help" "deploy-execute-verify" "$REPO_ROOT/bin/arbos-upgrade" --help + +echo "" +echo "--- Passthrough ---" +check_output "forge passthrough" "forge" "$ROUTER" forge --version +check_output "cast passthrough" "cast" "$ROUTER" cast --version + +echo "" +echo "=== Summary ===" +echo "Passed: $PASSED" +echo "Failed: $FAILED" + +if [[ $FAILED -gt 0 ]]; then + echo "Some tests failed!" + exit 1 +else + echo "All tests passed!" +fi From e3addacaec53df068b777c62d5091a09594252dc Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Mon, 9 Feb 2026 16:10:22 +0000 Subject: [PATCH 14/21] docs: reorganize README with CLI and Docker sections Replace verbose Docker documentation with concise CLI section and streamlined Docker examples. Move tooling documentation to end of README to prioritize upgrade guides. --- README.md | 115 ++++++++++++++++++++++-------------------------------- 1 file changed, 47 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 90383e73..43d0a4fb 100644 --- a/README.md +++ b/README.md @@ -25,73 +25,6 @@ For token bridge related operations, these are the additional requirements: yarn install ``` -## Using Docker - -The Orbit actions are also available via Docker. - -### Build the image - -First, ensure Foundry dependencies are installed: - -```bash -forge install -``` - -Then build the image: - -```bash -docker build -t orbit-actions . -``` - -### Run commands - -Pass the command you want to run directly to Docker: - -```bash -# Check contract versions -docker run --rm \ - -e INBOX_ADDRESS=0xYourInboxAddress \ - -e INFURA_KEY=your_infura_key \ - orbit-actions \ - yarn orbit:contracts:version --network arb1 - -# Run forge script -docker run --rm \ - --env-file orbit.env \ - -v $(pwd)/broadcast:/app/broadcast \ - orbit-actions \ - forge script --sender 0xYourAddress --rpc-url $PARENT_CHAIN_RPC --broadcast \ - scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol -vvv - -# Run cast commands -docker run --rm \ - orbit-actions \ - cast call --rpc-url https://arb1.arbitrum.io/rpc 0xYourRollup "wasmModuleRoot()" -``` - -### Environment variables - -Create an `orbit.env` file with your configuration and pass it using `--env-file`: - -```bash -PARENT_CHAIN_RPC=https://arb1.arbitrum.io/rpc -INBOX_ADDRESS=0x... -PROXY_ADMIN_ADDRESS=0x... -PARENT_UPGRADE_EXECUTOR_ADDRESS=0x... -``` - -### Getting output artifacts - -Mount a volume to retrieve broadcast artifacts: - -```bash -docker run --rm \ - --env-file orbit.env \ - -v $(pwd)/broadcast:/app/broadcast \ - orbit-actions \ - forge script ... -``` - ## Check Version and Upgrade Path Run the follow command to check the version of Nitro contracts deployed on the parent chain of your Orbit chain. @@ -193,4 +126,50 @@ See [setCacheManager](scripts/foundry/stylus/setCacheManager). Currently limited to L2s; L3 support is expected in a future update. -See [Nitro contracts 3.1.0 upgrade](https://github.com/OffchainLabs/orbit-actions/tree/main/scripts/foundry/contract-upgrades/3.1.0). +See [Nitro contracts 3.1.0 upgrade](https://github.com/OffchainLabs/orbit-actions/tree/main/scripts/foundry/contract-upgrades/3.1.0). + +# CLI + +The `orbit-actions` CLI provides a guided interface for running upgrade scripts. It wraps Foundry commands and handles the deploy/execute/verify workflow. + +```bash +# Browse available scripts +yarn orbit-actions # List top-level directories +yarn orbit-actions contract-upgrades # List versions +yarn orbit-actions contract-upgrades/1.2.1 # List contents + commands + +# View files +yarn orbit-actions contract-upgrades/1.2.1/README.md + +# Run contract upgrades +yarn orbit-actions contract-upgrades/1.2.1/deploy-execute-verify --dry-run +yarn orbit-actions contract-upgrades/1.2.1/deploy --private-key $KEY + +# Run ArbOS upgrades +yarn orbit-actions arbos-upgrades/at-timestamp/deploy-execute-verify 32 --dry-run +``` + +Run `yarn orbit-actions help` for full usage details. The CLI reads configuration from a `.env` file in the working directory. + +## Docker + +The CLI is available as a Docker image at `offchainlabs/orbit-actions`: + +```bash +# Check contract versions +docker run --rm \ + -e INBOX_ADDRESS=0xaE21fDA3de92dE2FDAF606233b2863782Ba046F9 \ + -e INFURA_KEY=$INFURA_KEY \ + offchainlabs/orbit-actions:versioner \ + --network arb1 + +# Browse upgrade scripts +docker run --rm offchainlabs/orbit-actions contract-upgrades + +# Run upgrade with env file and capture broadcast output +docker run --rm \ + -v $(pwd)/.env:/app/.env \ + -v $(pwd)/broadcast:/app/broadcast \ + offchainlabs/orbit-actions \ + contract-upgrades/1.2.1/deploy-execute-verify --dry-run +``` From 596182b083fb1d857ac9756b593122e63f0a8d6c Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Feb 2026 12:06:04 +0000 Subject: [PATCH 15/21] refactor: simplify env loading to current directory only Remove fallback locations for .env files. Now only loads from process.cwd(), matching Forge's behavior for transparency. --- src/cli/utils/env.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/cli/utils/env.ts b/src/cli/utils/env.ts index 4bc8e22b..195305b5 100644 --- a/src/cli/utils/env.ts +++ b/src/cli/utils/env.ts @@ -17,18 +17,9 @@ function findRepoRoot(): string | null { } export function loadEnv(): void { - const repoRoot = findRepoRoot() - const candidates = [ - path.join(process.cwd(), '.env'), - repoRoot ? path.join(repoRoot, '.env') : null, - '/app/.env', - ].filter((p): p is string => p !== null) - - for (const envPath of candidates) { - if (fs.existsSync(envPath)) { - dotenv.config({ path: envPath }) - return - } + const envPath = path.join(process.cwd(), '.env') + if (fs.existsSync(envPath)) { + dotenv.config({ path: envPath }) } } From 9dc010880c3599911b335968cf0f69f78f9b8669 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Feb 2026 12:33:54 +0000 Subject: [PATCH 16/21] refactor: extract duplicate execute/verify logic in arbos-upgrade Extract executeUpgrade() and verifyUpgrade() helpers to eliminate code duplication between standalone commands and deploy-execute-verify. --- src/cli/commands/arbos-upgrade.ts | 149 +++++++++++------------------- 1 file changed, 56 insertions(+), 93 deletions(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 4fa9e229..bfb4e57c 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -30,50 +30,23 @@ const ARB_SYS = '0x0000000000000000000000000000000000000064' // Nitro ArbOS versions are offset by 55 to avoid collision with classic (pre-Nitro) versions const ARBOS_VERSION_OFFSET = 55 - -async function cmdDeploy(version: string, args: string[]): Promise { - const authArgs = parseAuthArgs(args) - - const rpcUrl = requireEnv('CHILD_CHAIN_RPC') - - if (!fs.existsSync(DEPLOY_SCRIPT)) { - die(`Deploy script not found: ${DEPLOY_SCRIPT}`) - } - - // Forge script reads this from env - process.env.ARBOS_VERSION = version - - log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) - - await runForgeScript({ - script: DEPLOY_SCRIPT, - rpcUrl, - authArgs, - broadcast: Boolean(authArgs), - slow: true, - }) -} - -async function cmdExecute(args: string[]): Promise { - const authArgs = parseAuthArgs(args) - - const rpcUrl = requireEnv('CHILD_CHAIN_RPC') - const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') - const actionAddress = requireEnv('UPGRADE_ACTION_ADDRESS') - - log(`Executing ArbOS upgrade action: ${actionAddress}`) - - const performCalldata = '0xb0a75d36' // perform() selector - - if (!authArgs) { - // No auth - output calldata for multisig +const PERFORM_SELECTOR = '0xb0a75d36' + +async function executeUpgrade( + actionAddress: string, + upgradeExecutor: string, + rpcUrl: string, + authArgs: string, + dryRun: boolean +): Promise { + if (dryRun || !authArgs) { const executeCalldata = await castCalldata( 'execute(address,bytes)', actionAddress, - performCalldata + PERFORM_SELECTOR ) - log('Calldata for UpgradeExecutor.execute():') + log(dryRun ? 'Dry run - calldata for UpgradeExecutor.execute():' : 'Calldata for UpgradeExecutor.execute():') console.log('') console.log(`To: ${upgradeExecutor}`) console.log(`Calldata: ${executeCalldata}`) @@ -83,7 +56,7 @@ async function cmdExecute(args: string[]): Promise { await runCastSend({ to: upgradeExecutor, sig: 'execute(address,bytes)', - args: [actionAddress, performCalldata], + args: [actionAddress, PERFORM_SELECTOR], rpcUrl, authArgs, }) @@ -92,9 +65,7 @@ async function cmdExecute(args: string[]): Promise { } } -async function cmdVerify(): Promise { - const rpcUrl = requireEnv('CHILD_CHAIN_RPC') - +async function verifyUpgrade(rpcUrl: string): Promise { log('Checking ArbOS upgrade status...') const scheduled = await runCastCall({ @@ -121,6 +92,46 @@ async function cmdVerify(): Promise { log(`Current ArbOS version: ${currentVersion}`) } +async function cmdDeploy(version: string, args: string[]): Promise { + const authArgs = parseAuthArgs(args) + + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') + + if (!fs.existsSync(DEPLOY_SCRIPT)) { + die(`Deploy script not found: ${DEPLOY_SCRIPT}`) + } + + // Forge script reads this from env + process.env.ARBOS_VERSION = version + + log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) + + await runForgeScript({ + script: DEPLOY_SCRIPT, + rpcUrl, + authArgs, + broadcast: Boolean(authArgs), + slow: true, + }) +} + +async function cmdExecute(args: string[]): Promise { + const authArgs = parseAuthArgs(args) + + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') + const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') + const actionAddress = requireEnv('UPGRADE_ACTION_ADDRESS') + + log(`Executing ArbOS upgrade action: ${actionAddress}`) + + await executeUpgrade(actionAddress, upgradeExecutor, rpcUrl, authArgs, false) +} + +async function cmdVerify(): Promise { + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') + await verifyUpgrade(rpcUrl) +} + async function cmdDeployExecuteVerify( version: string, options: { @@ -207,62 +218,14 @@ async function cmdDeployExecuteVerify( if (!auth.skipExecute) { log('Step 2: Executing ArbOS upgrade...') - - const performCalldata = '0xb0a75d36' // perform() selector - - if (auth.dryRun) { - const executeCalldata = await castCalldata( - 'execute(address,bytes)', - upgradeActionAddress, - performCalldata - ) - - log('Dry run - calldata for UpgradeExecutor.execute():') - console.log('') - console.log(`To: ${upgradeExecutor}`) - console.log(`Calldata: ${executeCalldata}`) - console.log('') - log('Submit this to your multisig/Safe to execute the upgrade') - } else { - await runCastSend({ - to: upgradeExecutor, - sig: 'execute(address,bytes)', - args: [upgradeActionAddress, performCalldata], - rpcUrl, - authArgs: executeAuth, - }) - - log('ArbOS upgrade scheduled successfully') - } + await executeUpgrade(upgradeActionAddress, upgradeExecutor, rpcUrl, executeAuth, auth.dryRun) } else { log('Step 2: Skipped execute') } if (!auth.dryRun && !auth.skipExecute) { log('Step 3: Verifying scheduled upgrade...') - - const scheduled = await runCastCall({ - to: ARB_OWNER_PUBLIC, - sig: 'getScheduledUpgrade()(uint64,uint64)', - rpcUrl, - }) - log(`Scheduled upgrade (version, timestamp): ${scheduled}`) - - const currentRaw = await runCastCall({ - to: ARB_SYS, - sig: 'arbOSVersion()(uint64)', - rpcUrl, - }) - - let currentVersion: number - if (currentRaw === 'N/A') { - currentVersion = 0 - } else { - const rawNum = parseInt(currentRaw, 10) - currentVersion = rawNum - ARBOS_VERSION_OFFSET - } - - log(`Current ArbOS version: ${currentVersion}`) + await verifyUpgrade(rpcUrl) } log('Done') From 6dd231fa2c18b8d63d28cc9eb4dd24a00abf051e Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Feb 2026 13:03:11 +0000 Subject: [PATCH 17/21] refactor: remove unused Commander subcommands Remove redundant subcommand interface - all functionality is accessed via the router's path-based syntax. Also extract deployAction helper to reduce duplication in arbos-upgrade. --- src/cli/commands/arbos-upgrade.ts | 113 +++++++-------------------- src/cli/commands/contract-upgrade.ts | 54 ------------- src/cli/index.ts | 5 -- 3 files changed, 28 insertions(+), 144 deletions(-) diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index bfb4e57c..02e84315 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -1,4 +1,3 @@ -import { Command } from 'commander' import * as path from 'path' import * as fs from 'fs' import { log, die } from '../utils/log' @@ -32,6 +31,31 @@ const ARB_SYS = '0x0000000000000000000000000000000000000064' const ARBOS_VERSION_OFFSET = 55 const PERFORM_SELECTOR = '0xb0a75d36' +function checkDeployScript(): void { + if (!fs.existsSync(DEPLOY_SCRIPT)) { + die(`Deploy script not found: ${DEPLOY_SCRIPT}`) + } +} + +async function deployAction( + version: string, + rpcUrl: string, + authArgs: string, + options: { broadcast: boolean; verify?: boolean } +): Promise { + checkDeployScript() + process.env.ARBOS_VERSION = version + + await runForgeScript({ + script: DEPLOY_SCRIPT, + rpcUrl, + authArgs, + broadcast: options.broadcast, + verify: options.verify, + slow: true, + }) +} + async function executeUpgrade( actionAddress: string, upgradeExecutor: string, @@ -94,25 +118,9 @@ async function verifyUpgrade(rpcUrl: string): Promise { async function cmdDeploy(version: string, args: string[]): Promise { const authArgs = parseAuthArgs(args) - const rpcUrl = requireEnv('CHILD_CHAIN_RPC') - - if (!fs.existsSync(DEPLOY_SCRIPT)) { - die(`Deploy script not found: ${DEPLOY_SCRIPT}`) - } - - // Forge script reads this from env - process.env.ARBOS_VERSION = version - log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) - - await runForgeScript({ - script: DEPLOY_SCRIPT, - rpcUrl, - authArgs, - broadcast: Boolean(authArgs), - slow: true, - }) + await deployAction(version, rpcUrl, authArgs, { broadcast: Boolean(authArgs) }) } async function cmdExecute(args: string[]): Promise { @@ -151,12 +159,8 @@ async function cmdDeployExecuteVerify( const auth = createDeployExecuteAuth(options) log(`ArbOS version: ${version}`) + checkDeployScript() - if (!fs.existsSync(DEPLOY_SCRIPT)) { - die(`Deploy script not found: ${DEPLOY_SCRIPT}`) - } - - // Auto-detect skip_deploy if UPGRADE_ACTION_ADDRESS is set const skipDeploy = Boolean(getEnv('UPGRADE_ACTION_ADDRESS')) let upgradeActionAddress = getEnv('UPGRADE_ACTION_ADDRESS') || '' @@ -168,9 +172,6 @@ async function cmdDeployExecuteVerify( const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') requireEnv('SCHEDULE_TIMESTAMP') - // Forge script reads this from env - process.env.ARBOS_VERSION = version - const deployAuth = getDeployAuth(auth) const executeAuth = getExecuteAuth(auth) @@ -193,13 +194,9 @@ async function cmdDeployExecuteVerify( log(`Target chain ID: ${chainId}`) log('Step 1: Deploying ArbOS upgrade action...') - await runForgeScript({ - script: DEPLOY_SCRIPT, - rpcUrl, - authArgs: deployAuth, + await deployAction(version, rpcUrl, deployAuth, { broadcast: !auth.dryRun, verify: auth.verifyContracts, - slow: true, }) if (!auth.dryRun) { @@ -231,58 +228,4 @@ async function cmdDeployExecuteVerify( log('Done') } -export function createArbosUpgradeCommand(): Command { - const cmd = new Command('arbos-upgrade') - .description('ArbOS upgrade operations') - .argument('', 'ArbOS version number') - .argument( - '[command]', - 'Command: deploy, execute, verify, deploy-execute-verify', - 'deploy-execute-verify' - ) - .option('--private-key ', 'Private key (for deploy/execute)') - .option('--account ', 'Keystore account (for deploy/execute)') - .option('--ledger', 'Use Ledger (for deploy/execute)') - .option('--interactive', 'Prompt for key (for deploy/execute)') - .option('--deploy-key ', 'Private key for deploy step') - .option('--deploy-account ', 'Keystore account for deploy') - .option('--deploy-ledger', 'Use Ledger for deploy') - .option('--deploy-interactive', 'Prompt for key for deploy') - .option('--execute-key ', 'Private key for execute step') - .option('--execute-account ', 'Keystore account for execute') - .option('--execute-ledger', 'Use Ledger for execute') - .option('--execute-interactive', 'Prompt for key for execute') - .option('-n, --dry-run', 'Simulate without broadcasting') - .option('--skip-execute', 'Deploy only') - .option('-v, --verify', 'Verify on block explorer') - .action(async (version: string, command: string, options) => { - const args: string[] = [] - if (options.privateKey) args.push('--private-key', options.privateKey) - if (options.account) args.push('--account', options.account) - if (options.ledger) args.push('--ledger') - if (options.interactive) args.push('--interactive') - - switch (command) { - case 'deploy': - await cmdDeploy(version, args) - break - case 'execute': - await cmdExecute(args) - break - case 'verify': - await cmdVerify() - break - case 'deploy-execute-verify': - await cmdDeployExecuteVerify(version, options) - break - default: - die( - `Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify` - ) - } - }) - - return cmd -} - export { cmdDeploy, cmdExecute, cmdVerify, cmdDeployExecuteVerify } diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts index 3d416573..48444311 100644 --- a/src/cli/commands/contract-upgrade.ts +++ b/src/cli/commands/contract-upgrade.ts @@ -1,4 +1,3 @@ -import { Command } from 'commander' import * as path from 'path' import * as fs from 'fs' import { log, die } from '../utils/log' @@ -226,59 +225,6 @@ async function cmdDeployExecuteVerify( log('Done') } -export function createContractUpgradeCommand(): Command { - const cmd = new Command('contract-upgrade') - .description('Contract upgrade operations') - .argument('', 'Contract version (e.g., 1.2.1)') - .argument( - '', - 'Command: deploy, execute, verify, deploy-execute-verify' - ) - .option('--private-key ', 'Private key (for deploy/execute)') - .option('--account ', 'Keystore account (for deploy/execute)') - .option('--ledger', 'Use Ledger (for deploy/execute)') - .option('--interactive', 'Prompt for key (for deploy/execute)') - .option('--deploy-key ', 'Private key for deploy step') - .option('--deploy-account ', 'Keystore account for deploy') - .option('--deploy-ledger', 'Use Ledger for deploy') - .option('--deploy-interactive', 'Prompt for key for deploy') - .option('--execute-key ', 'Private key for execute step') - .option('--execute-account ', 'Keystore account for execute') - .option('--execute-ledger', 'Use Ledger for execute') - .option('--execute-interactive', 'Prompt for key for execute') - .option('-n, --dry-run', 'Simulate without broadcasting') - .option('--skip-execute', 'Deploy only') - .option('-v, --verify', 'Verify on block explorer') - .action(async (version: string, command: string, options) => { - const args: string[] = [] - if (options.privateKey) args.push('--private-key', options.privateKey) - if (options.account) args.push('--account', options.account) - if (options.ledger) args.push('--ledger') - if (options.interactive) args.push('--interactive') - - switch (command) { - case 'deploy': - await cmdDeploy(version, args) - break - case 'execute': - await cmdExecute(version, args) - break - case 'verify': - await cmdVerify(version) - break - case 'deploy-execute-verify': - await cmdDeployExecuteVerify(version, options) - break - default: - die( - `Unknown command: ${command}\n\nCommands: deploy, execute, verify, deploy-execute-verify` - ) - } - }) - - return cmd -} - export { cmdDeploy, cmdExecute, diff --git a/src/cli/index.ts b/src/cli/index.ts index 0b009dc6..6d5ee588 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -3,8 +3,6 @@ import { program } from 'commander' import { loadEnv } from './utils/env' import { router } from './router' -import { createContractUpgradeCommand } from './commands/contract-upgrade' -import { createArbosUpgradeCommand } from './commands/arbos-upgrade' loadEnv() @@ -18,7 +16,4 @@ program await router(pathArg, args) }) -program.addCommand(createContractUpgradeCommand()) -program.addCommand(createArbosUpgradeCommand()) - program.parse() From aaa6eea34ca7e6762d295a9caf5613a7b560a45a Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Feb 2026 13:06:52 +0000 Subject: [PATCH 18/21] build: pin Foundry to specific nightly version Pin foundryup to nightly-2026-02-09 for reproducible Docker builds. --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b8fe17f2..e2003a34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,10 @@ RUN apt-get update && apt-get install -y \ jq \ && rm -rf /var/lib/apt/lists/* -# Install Foundry +# Install Foundry (pinned for reproducible builds) RUN curl -L https://foundry.paradigm.xyz | bash ENV PATH="/root/.foundry/bin:${PATH}" -RUN foundryup +RUN foundryup --version nightly-2026-02-09 # Install Yarn Classic (v1) - matches the repo's yarn.lock format RUN npm install -g --force yarn@1.22.22 From ead08146e36a1f6747a750e9ab1c5a50f6b5496c Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Feb 2026 13:08:38 +0000 Subject: [PATCH 19/21] chore: remove obvious Dockerfile comments --- Dockerfile | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index e2003a34..1e8fe26a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,23 +15,16 @@ RUN foundryup --version nightly-2026-02-09 # Install Yarn Classic (v1) - matches the repo's yarn.lock format RUN npm install -g --force yarn@1.22.22 -# Set working directory WORKDIR /app -# Copy package files first for better caching +# Copy package files first for better layer caching COPY package.json yarn.lock ./ -# Install dependencies (using --ignore-scripts like CI does, then forge install separately) +# --ignore-scripts: forge install runs separately after full copy RUN yarn install --frozen-lockfile --ignore-scripts -# Copy the rest of the repository COPY . . - -# Build contracts RUN forge build - -# Build CLI RUN yarn build:cli -# Direct node entrypoint (no shell wrapper) ENTRYPOINT ["node", "/app/dist/cli/index.js"] From 7977435038e717d6f95a4b88c99bb7ddf6076c9e Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Feb 2026 13:31:11 +0000 Subject: [PATCH 20/21] fix: correct Foundry version pin and lint issues - Pin to actual nightly release (2026-02-10) using commit hash - Add dist/ to eslintignore - Fix prettier formatting in arbos-upgrade.ts --- .eslintignore | 1 + Dockerfile | 4 ++-- src/cli/commands/arbos-upgrade.ts | 18 +++++++++++++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.eslintignore b/.eslintignore index c3af8579..a89c6a71 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ lib/ +dist/ diff --git a/Dockerfile b/Dockerfile index 1e8fe26a..9a7c5637 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,10 @@ RUN apt-get update && apt-get install -y \ jq \ && rm -rf /var/lib/apt/lists/* -# Install Foundry (pinned for reproducible builds) +# Install Foundry (pinned to 2026-02-10 nightly for reproducible builds) RUN curl -L https://foundry.paradigm.xyz | bash ENV PATH="/root/.foundry/bin:${PATH}" -RUN foundryup --version nightly-2026-02-09 +RUN foundryup --version nightly-e788798a511a32e896b127560e2269fb2c43eddd # Install Yarn Classic (v1) - matches the repo's yarn.lock format RUN npm install -g --force yarn@1.22.22 diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts index 02e84315..0a598485 100644 --- a/src/cli/commands/arbos-upgrade.ts +++ b/src/cli/commands/arbos-upgrade.ts @@ -70,7 +70,11 @@ async function executeUpgrade( PERFORM_SELECTOR ) - log(dryRun ? 'Dry run - calldata for UpgradeExecutor.execute():' : 'Calldata for UpgradeExecutor.execute():') + log( + dryRun + ? 'Dry run - calldata for UpgradeExecutor.execute():' + : 'Calldata for UpgradeExecutor.execute():' + ) console.log('') console.log(`To: ${upgradeExecutor}`) console.log(`Calldata: ${executeCalldata}`) @@ -120,7 +124,9 @@ async function cmdDeploy(version: string, args: string[]): Promise { const authArgs = parseAuthArgs(args) const rpcUrl = requireEnv('CHILD_CHAIN_RPC') log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) - await deployAction(version, rpcUrl, authArgs, { broadcast: Boolean(authArgs) }) + await deployAction(version, rpcUrl, authArgs, { + broadcast: Boolean(authArgs), + }) } async function cmdExecute(args: string[]): Promise { @@ -215,7 +221,13 @@ async function cmdDeployExecuteVerify( if (!auth.skipExecute) { log('Step 2: Executing ArbOS upgrade...') - await executeUpgrade(upgradeActionAddress, upgradeExecutor, rpcUrl, executeAuth, auth.dryRun) + await executeUpgrade( + upgradeActionAddress, + upgradeExecutor, + rpcUrl, + executeAuth, + auth.dryRun + ) } else { log('Step 2: Skipped execute') } From 2f30ddefd2ef236a8020ce02e45c7bf258799aaf Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Tue, 10 Feb 2026 14:23:52 +0000 Subject: [PATCH 21/21] revert: remove Foundry version pin that fails in Docker The foundryup --version flag doesn't work correctly in Docker when bootstrapping from the install script. Revert to unpinned foundryup which installs the latest stable release. --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9a7c5637..864e3ada 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,9 @@ RUN apt-get update && apt-get install -y \ jq \ && rm -rf /var/lib/apt/lists/* -# Install Foundry (pinned to 2026-02-10 nightly for reproducible builds) -RUN curl -L https://foundry.paradigm.xyz | bash +# Install Foundry ENV PATH="/root/.foundry/bin:${PATH}" -RUN foundryup --version nightly-e788798a511a32e896b127560e2269fb2c43eddd +RUN curl -L https://foundry.paradigm.xyz | bash && foundryup # Install Yarn Classic (v1) - matches the repo's yarn.lock format RUN npm install -g --force yarn@1.22.22