diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..10384db8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# 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 + +# Git +.git/ +.gitignore + +# IDE +.vscode/ +.idea/ + +# Test artifacts +broadcast/ +coverage/ + +# Docker +Dockerfile +.dockerignore diff --git a/.eslintignore b/.eslintignore index c3af8579..a89c6a71 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ lib/ +dist/ 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/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml new file mode 100644 index 00000000..422469bd --- /dev/null +++ b/.github/workflows/publish-docker.yml @@ -0,0 +1,48 @@ +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 + with: + submodules: recursive + + - 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: + # "orbit" is a deprecated term; the published image uses "chain-actions" + 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 diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml new file mode 100644 index 00000000..eea090e4 --- /dev/null +++ b/.github/workflows/test-docker.yml @@ -0,0 +1,32 @@ +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 + with: + submodules: recursive + + - 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/.gitignore b/.gitignore index 8e35eb36..e00e0e6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ node_modules .env +.DS_Store + +# TypeScript build output +/dist # Hardhat files /cache diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..14fcdadc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM node:22-slim + +# 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 +ENV PATH="/root/.foundry/bin:${PATH}" +RUN curl -L https://foundry.paradigm.xyz | bash && foundryup --install stable + +# Install Yarn Classic (v1) - matches the repo's yarn.lock format +RUN npm install -g --force yarn@1.22.22 + +WORKDIR /app + +# Copy package files first for better layer caching +COPY package.json yarn.lock ./ + +# --ignore-scripts: forge install runs separately after full copy +RUN yarn install --frozen-lockfile --ignore-scripts + +COPY . . +# forge install can't run here: it clones git submodules, but .dockerignore +# excludes .git/. CI runs forge install on the host so lib/ is copied in above. +RUN forge build +RUN yarn build:cli + +ENTRYPOINT ["node", "/app/dist/cli/index.js"] diff --git a/README.md b/README.md index bf5fe027..11a0183c 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ For ArbOS upgrades, a common pre-requisite is to deploy new Nitro contracts to t ### Nitro contracts 3.1.0 (for [BoLD](https://docs.arbitrum.io/how-arbitrum-works/bold/gentle-introduction)) -The [`nitro-contracts 3.1.0` upgrade guide](scripts/foundry/contract-upgrades/3.1.0) will use the [BOLDUpgradeAction](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/BOLDUpgradeAction.sol) from the [nitro-contract](https://github.com/OffchainLabs/nitro-contracts) repo. There is no associated ArbOS upgrade for BoLD. +The [`nitro-contracts 3.1.0` upgrade guide](scripts/foundry/contract-upgrades/3.1.0) will use the [BOLDUpgradeAction](https://github.com/OffchainLabs/nitro-contracts/blob/main/src/rollup/BOLDUpgradeAction.sol) from the [nitro-contract](https://github.com/OffchainLabs/nitro-contracts) repo. There is no associated ArbOS upgrade for BoLD. ### Nitro contracts 2.1.3 @@ -132,4 +132,94 @@ 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 cli # List top-level directories +yarn cli -- contract-upgrades # List versions +yarn cli -- contract-upgrades/1.2.1 # List contents + commands + +# View files +yarn cli -- contract-upgrades/1.2.1/README.md + +# Run contract upgrade steps individually +yarn cli -- contract-upgrades/1.2.1/deploy +yarn cli -- contract-upgrades/1.2.1/execute +yarn cli -- contract-upgrades/1.2.1/verify + +# Run ArbOS upgrade steps individually +yarn cli -- arbos-upgrades/at-timestamp/deploy 32 +yarn cli -- arbos-upgrades/at-timestamp/execute +yarn cli -- arbos-upgrades/at-timestamp/verify +``` + +Run `yarn cli -- help` for full usage details. + +### Configuration + +The CLI reads chain-specific configuration (RPC URLs, contract addresses) from a `.env` file in the project root. See env templates in each version directory for examples. + +Forge behavior -- broadcasting, authentication, verbosity, verification -- is controlled via standard `FOUNDRY_*` / `ETH_*` env vars in the same `.env` file. The CLI passes `process.env` through to forge, so any env var forge recognizes will work. + +Key forge env vars: + +| Variable | Effect | +| ------------------------ | ---------------------------------------------------------------- | +| `FOUNDRY_BROADCAST=true` | Broadcast transactions (without this, scripts run in simulation) | +| `ETH_PRIVATE_KEY=0x...` | Private key for signing transactions | + +All `FOUNDRY_*` env vars are supported -- see [Foundry configuration](https://book.getfoundry.sh/reference/config/) for the full list. + +### Full upgrade flow + +The deploy, execute, and verify steps are run separately. This allows multisig users to submit Safe approvals between steps, and EOA users can chain the commands: + +```bash +# 1. Deploy the upgrade action contract +yarn cli -- contract-upgrades/2.1.3/deploy +# Note the deployed action address printed at the end + +# 2. Set UPGRADE_ACTION_ADDRESS in .env, then execute +yarn cli -- contract-upgrades/2.1.3/execute + +# 3. Verify the upgrade +yarn cli -- contract-upgrades/2.1.3/verify +``` + +### Writing new deploy scripts + +The CLI identifies the action contract address by reading the last `CREATE` transaction from Forge's broadcast file. Deploy scripts must deploy all dependencies first and the action contract last. + +## 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 + +# Deploy with env file (simulation mode -- no FOUNDRY_BROADCAST) +docker run --rm \ + -v $(pwd)/.env:/app/.env \ + -v $(pwd)/broadcast:/app/broadcast \ + offchainlabs/orbit-actions \ + contract-upgrades/2.1.3/deploy + +# Execute with broadcasting enabled +docker run --rm \ + -v $(pwd)/.env:/app/.env \ + offchainlabs/orbit-actions \ + contract-upgrades/2.1.3/execute +``` diff --git a/package.json b/package.json index 74e3f3ba..e525ef41 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "repository": "https://github.com/OffchainLabs/blockchain-eng-template.git", "license": "Apache 2.0", "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", @@ -20,6 +22,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" @@ -61,5 +64,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/scripts/foundry/arbos-upgrades/at-timestamp/DeployUpgradeArbOSVersionAtTimestampAction.s.sol b/scripts/foundry/arbos-upgrades/at-timestamp/DeployUpgradeArbOSVersionAtTimestampAction.s.sol index 7a326e6f..e57c8e4e 100644 --- a/scripts/foundry/arbos-upgrades/at-timestamp/DeployUpgradeArbOSVersionAtTimestampAction.s.sol +++ b/scripts/foundry/arbos-upgrades/at-timestamp/DeployUpgradeArbOSVersionAtTimestampAction.s.sol @@ -25,7 +25,8 @@ contract DeployUpgradeArbOSVersionAtTimestampActionScript is Script { vm.startBroadcast(); - // finally deploy upgrade action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new UpgradeArbOSVersionAtTimestampAction({ _newArbOSVersion: uint64(arbosVersion), _upgradeTimestamp: uint64(scheduleTimestamp) }); diff --git a/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.local-upgrade.example b/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.local-upgrade.example index 2ceb824e..1f91f266 100644 --- a/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.local-upgrade.example +++ b/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.local-upgrade.example @@ -1,3 +1,10 @@ +## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ +# FOUNDRY_BROADCAST=true +# ETH_PRIVATE_KEY=0x... + +## Chain and contract addresses +CHILD_CHAIN_RPC= UPGRADE_ACTION_ADDRESS="0x217788c286797D56Cd59aF5e493f3699C39cbbe8" CHILD_UPGRADE_EXECUTOR_ADDRESS="0x6A17B0D4EA37c4F519caCf776450cb42199Df0A4" ARBOS_VERSION=20 diff --git a/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.sepolia-upgrade.example b/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.sepolia-upgrade.example index 821f2393..f39d61c2 100644 --- a/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.sepolia-upgrade.example +++ b/scripts/foundry/arbos-upgrades/at-timestamp/env-templates/.env.sepolia-upgrade.example @@ -1,3 +1,10 @@ +## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ +# FOUNDRY_BROADCAST=true +# ETH_PRIVATE_KEY=0x... + +## Chain and contract addresses +CHILD_CHAIN_RPC= UPGRADE_ACTION_ADDRESS="0x7A132A8130a0C2dCeABd1FDb42ed01FCf6B9494a" CHILD_UPGRADE_EXECUTOR_ADDRESS="0x059EF6e2CaA4d779e087273646EfF49ef45dBD81" ARBOS_VERSION=20 diff --git a/scripts/foundry/contract-upgrades/1.2.1/DeployNitroContracts1Point2Point1UpgradeAction.s.sol b/scripts/foundry/contract-upgrades/1.2.1/DeployNitroContracts1Point2Point1UpgradeAction.s.sol index 5bd18a0b..53abf4fc 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/DeployNitroContracts1Point2Point1UpgradeAction.s.sol +++ b/scripts/foundry/contract-upgrades/1.2.1/DeployNitroContracts1Point2Point1UpgradeAction.s.sol @@ -82,7 +82,8 @@ contract DeployNitroContracts1Point2Point1UpgradeActionScript is Script { abi.encode(vm.envUint("MAX_DATA_SIZE"), reader4844Address, vm.envBool("IS_FEE_TOKEN_CHAIN")) ); - // finally deploy upgrade action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new NitroContracts1Point2Point1UpgradeAction({ _newWasmModuleRoot: vm.envBytes32("WASM_MODULE_ROOT"), _newSequencerInboxImpl: seqInbox, 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/1.2.1/env-templates/.env.arbsepolia-upgrade.example b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example index e315f603..1f536f21 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example +++ b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.arbsepolia-upgrade.example @@ -1,3 +1,10 @@ +## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ +# FOUNDRY_BROADCAST=true +# ETH_PRIVATE_KEY=0x... + +## Chain and contract addresses +PARENT_CHAIN_RPC= WASM_MODULE_ROOT="0x8b104a2e80ac6165dc58b9048de12f301d70b02a0ab51396c22b4b4b802a16a4" PARENT_CHAIN_IS_ARBITRUM=true IS_FEE_TOKEN_CHAIN=false @@ -5,4 +12,5 @@ MAX_DATA_SIZE=104857 UPGRADE_ACTION_ADDRESS="" INBOX_ADDRESS="" PROXY_ADMIN_ADDRESS="" -PARENT_UPGRADE_EXECUTOR_ADDRESS="" \ No newline at end of file +PARENT_UPGRADE_EXECUTOR_ADDRESS="" +ROLLUP= \ No newline at end of file diff --git a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example index 2556ed03..b7b88cd8 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example +++ b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.local-upgrade.example @@ -1,3 +1,10 @@ +## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ +# FOUNDRY_BROADCAST=true +# ETH_PRIVATE_KEY=0x... + +## Chain and contract addresses +PARENT_CHAIN_RPC= WASM_MODULE_ROOT="0x8b104a2e80ac6165dc58b9048de12f301d70b02a0ab51396c22b4b4b802a16a4" PARENT_CHAIN_IS_ARBITRUM=true IS_FEE_TOKEN_CHAIN=false @@ -6,3 +13,4 @@ UPGRADE_ACTION_ADDRESS="0x7B5F0B437EE68A22992DdD629AA22525Fc5dfa9A" INBOX_ADDRESS="0xc76802b32fc760324d3061a80890cd47d0e135e8" PROXY_ADMIN_ADDRESS="0xef83c42810b3816882d83cfdc922333c05448b55" PARENT_UPGRADE_EXECUTOR_ADDRESS="0x9115d7feb9698fd4349bc9c4f1fbe96b583dc044" +ROLLUP= diff --git a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example index c5999a05..f00d9e5a 100644 --- a/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example +++ b/scripts/foundry/contract-upgrades/1.2.1/env-templates/.env.sepolia-upgrade.example @@ -1,3 +1,10 @@ +## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ +# FOUNDRY_BROADCAST=true +# ETH_PRIVATE_KEY=0x... + +## Chain and contract addresses +PARENT_CHAIN_RPC= WASM_MODULE_ROOT="0x8b104a2e80ac6165dc58b9048de12f301d70b02a0ab51396c22b4b4b802a16a4" PARENT_CHAIN_IS_ARBITRUM=false IS_FEE_TOKEN_CHAIN=false @@ -5,4 +12,5 @@ MAX_DATA_SIZE=117964 UPGRADE_ACTION_ADDRESS="0xBC1e0ca800781F58F3a2f73dA4D895FdD61B0Cb5" INBOX_ADDRESS="0xaAe29B0366299461418F5324a79Afc425BE5ae21" PROXY_ADMIN_ADDRESS="0xdd63bCAA89d7c3199Ef220c1Dd59C49F821078B8" -PARENT_UPGRADE_EXECUTOR_ADDRESS="0x5FEe78FE9AD96c1d8557C6D6BB22Eb5A61eeD315" \ No newline at end of file +PARENT_UPGRADE_EXECUTOR_ADDRESS="0x5FEe78FE9AD96c1d8557C6D6BB22Eb5A61eeD315" +ROLLUP= \ No newline at end of file diff --git a/scripts/foundry/contract-upgrades/2.1.0/.env.sample b/scripts/foundry/contract-upgrades/2.1.0/.env.sample index 9521eef3..bad2a3dd 100644 --- a/scripts/foundry/contract-upgrades/2.1.0/.env.sample +++ b/scripts/foundry/contract-upgrades/2.1.0/.env.sample @@ -1,8 +1,14 @@ -## These env vars are used for ExecuteNitroContracts2Point1Point0UpgradeScript +## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ +# FOUNDRY_BROADCAST=true +# ETH_PRIVATE_KEY=0x... +## Chain and contract addresses +PARENT_CHAIN_RPC= UPGRADE_ACTION_ADDRESS= INBOX_ADDRESS= PROXY_ADMIN_ADDRESS= PARENT_UPGRADE_EXECUTOR_ADDRESS= TARGET_WASM_MODULE_ROOT=0x184884e1eb9fefdc158f6c8ac912bb183bf3cf83f0090317e0bc4ac5860baa39 -PARENT_CHAIN_IS_ARBITRUM=true \ No newline at end of file +PARENT_CHAIN_IS_ARBITRUM=true +ROLLUP= \ No newline at end of file diff --git a/scripts/foundry/contract-upgrades/2.1.0/DeployNitroContracts2Point1Point0UpgradeAction.s.sol b/scripts/foundry/contract-upgrades/2.1.0/DeployNitroContracts2Point1Point0UpgradeAction.s.sol index 8816c21b..b0828174 100644 --- a/scripts/foundry/contract-upgrades/2.1.0/DeployNitroContracts2Point1Point0UpgradeAction.s.sol +++ b/scripts/foundry/contract-upgrades/2.1.0/DeployNitroContracts2Point1Point0UpgradeAction.s.sol @@ -86,7 +86,8 @@ contract DeployNitroContracts2Point1Point0UpgradeActionScript is DeploymentHelpe "/node_modules/@arbitrum/nitro-contracts-2.1.0/build/contracts/src/rollup/RollupUserLogic.sol/RollupUserLogic.json" ); - // finally deploy upgrade action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new NitroContracts2Point1Point0UpgradeAction({ _newWasmModuleRoot: WASM_MODULE_ROOT, _newChallengeManagerImpl: challengeManager, 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/.env.sample b/scripts/foundry/contract-upgrades/2.1.2/.env.sample index eba03a66..5d029650 100644 --- a/scripts/foundry/contract-upgrades/2.1.2/.env.sample +++ b/scripts/foundry/contract-upgrades/2.1.2/.env.sample @@ -1,5 +1,10 @@ -## These env vars are used for ExecuteNitroContracts2Point1Point2UpgradeScript +## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ +# FOUNDRY_BROADCAST=true +# ETH_PRIVATE_KEY=0x... +## Chain and contract addresses +PARENT_CHAIN_RPC= UPGRADE_ACTION_ADDRESS= INBOX_ADDRESS= PROXY_ADMIN_ADDRESS= diff --git a/scripts/foundry/contract-upgrades/2.1.2/DeployNitroContracts2Point1Point2UpgradeAction.s.sol b/scripts/foundry/contract-upgrades/2.1.2/DeployNitroContracts2Point1Point2UpgradeAction.s.sol index b0e68052..f48e4498 100644 --- a/scripts/foundry/contract-upgrades/2.1.2/DeployNitroContracts2Point1Point2UpgradeAction.s.sol +++ b/scripts/foundry/contract-upgrades/2.1.2/DeployNitroContracts2Point1Point2UpgradeAction.s.sol @@ -19,7 +19,8 @@ contract DeployNitroContracts2Point1Point2UpgradeActionScript is DeploymentHelpe "/node_modules/@arbitrum/nitro-contracts-2.1.2/build/contracts/src/bridge/ERC20Bridge.sol/ERC20Bridge.json" ); - // deploy upgrade action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new NitroContracts2Point1Point2UpgradeAction(newBridgeImpl); vm.stopBroadcast(); 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/.env.sample b/scripts/foundry/contract-upgrades/2.1.3/.env.sample index c189ee2f..565f8a6f 100644 --- a/scripts/foundry/contract-upgrades/2.1.3/.env.sample +++ b/scripts/foundry/contract-upgrades/2.1.3/.env.sample @@ -1,4 +1,10 @@ -## These env vars are used for ExecuteNitroContracts2Point1Point2UpgradeScript +## Forge configuration (uncomment/adjust as needed) +## All FOUNDRY_* env vars are supported: https://book.getfoundry.sh/reference/config/ +# FOUNDRY_BROADCAST=true +# ETH_PRIVATE_KEY=0x... + +## Chain and contract addresses +PARENT_CHAIN_RPC= PARENT_CHAIN_IS_ARBITRUM=true MAX_DATA_SIZE=104857 UPGRADE_ACTION_ADDRESS= diff --git a/scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol b/scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol index 819251b5..8d2ed77a 100644 --- a/scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol +++ b/scripts/foundry/contract-upgrades/2.1.3/DeployNitroContracts2Point1Point3UpgradeAction.s.sol @@ -53,7 +53,8 @@ contract DeployNitroContracts2Point1Point3UpgradeActionScript is DeploymentHelpe abi.encode(vm.envUint("MAX_DATA_SIZE"), reader4844Address, true) ); - // deploy upgrade action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new NitroContracts2Point1Point3UpgradeAction( newEthInboxImpl, newERC20InboxImpl, newEthSeqInboxImpl, newErc20SeqInboxImpl ); diff --git a/scripts/foundry/contract-upgrades/2.1.3/README.md b/scripts/foundry/contract-upgrades/2.1.3/README.md index af8e4087..0d036527 100644 --- a/scripts/foundry/contract-upgrades/2.1.3/README.md +++ b/scripts/foundry/contract-upgrades/2.1.3/README.md @@ -72,12 +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. - -```bash -# should return 18 -cast call --rpc-url $PARENT_CHAIN_RPC $BRIDGE "nativeTokenDecimals()(uint8)" -``` +4. That's it, upgrade has been performed. There is no automated verification script for + 2.1.3 at this time. (The previous script incorrectly checked `nativeTokenDecimals`, + which is a 2.1.2 concern -- 2.1.3 upgrades Inbox and SequencerInbox, not the bridge, + and `nativeTokenDecimals()` reverts on ETH-native chains.) ## FAQ diff --git a/scripts/foundry/fast-confirm/DeployEnableFastConfirmAction.s.sol b/scripts/foundry/fast-confirm/DeployEnableFastConfirmAction.s.sol index 26d2f7b8..b819a8c2 100644 --- a/scripts/foundry/fast-confirm/DeployEnableFastConfirmAction.s.sol +++ b/scripts/foundry/fast-confirm/DeployEnableFastConfirmAction.s.sol @@ -16,7 +16,8 @@ contract DeployEnableFastConfirmAction is DeploymentHelpersScript { function run() public { vm.startBroadcast(); - // deploy action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new EnableFastConfirmAction({ gnosisSafeProxyFactory: GNOSIS_SAFE_PROXY_FACTORY, gnosisSafe1_3_0: GNOSIS_SAFE_1_3_0, diff --git a/scripts/foundry/sequencer/max-time-variation/DeploySetSequencerInboxMaxTimeVariationAction.s.sol b/scripts/foundry/sequencer/max-time-variation/DeploySetSequencerInboxMaxTimeVariationAction.s.sol index 69357172..556ae36c 100644 --- a/scripts/foundry/sequencer/max-time-variation/DeploySetSequencerInboxMaxTimeVariationAction.s.sol +++ b/scripts/foundry/sequencer/max-time-variation/DeploySetSequencerInboxMaxTimeVariationAction.s.sol @@ -14,7 +14,8 @@ contract DeploySetSequencerInboxMaxTimeVariationActionScript is Script { function run() public { vm.startBroadcast(); - // finally deploy upgrade action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new SetSequencerInboxMaxTimeVariationAction({ _delayBlocks: vm.envUint("DELAY_BLOCKS"), _futureBlocks: vm.envUint("FUTURE_BLOCKS"), diff --git a/scripts/foundry/stylus/setCacheManager/DeployAddWasmCacheManagerAction.s.sol b/scripts/foundry/stylus/setCacheManager/DeployAddWasmCacheManagerAction.s.sol index 2d3296ef..d029c955 100644 --- a/scripts/foundry/stylus/setCacheManager/DeployAddWasmCacheManagerAction.s.sol +++ b/scripts/foundry/stylus/setCacheManager/DeployAddWasmCacheManagerAction.s.sol @@ -31,7 +31,8 @@ contract DeployAddWasmCacheManagerActionScript is DeploymentHelpersScript { ICacheManager cacheManager = ICacheManager(cacheManagerProxy); cacheManager.initialize(uint64(vm.envUint("INIT_CACHE_SIZE")), uint64(vm.envUint("INIT_DECAY"))); - // deploy action + // Deploy the action contract last. The CLI identifies the deployed action + // by taking the last CREATE from the broadcast file. new AddWasmCacheManagerAction({ _wasmCachemanager: address(cacheManager), _targetArbOSVersion: TARGET_ARBOS_VERSION }); diff --git a/src/cli/commands/arbos-upgrade.ts b/src/cli/commands/arbos-upgrade.ts new file mode 100644 index 00000000..e2606f13 --- /dev/null +++ b/src/cli/commands/arbos-upgrade.ts @@ -0,0 +1,152 @@ +import * as path from 'path' +import * as fs from 'fs' +import { Interface } from 'ethers' +import { die } from '../utils/log' +import { requireEnv, getScriptsDir } from '../utils/env' +import { + runForgeScript, + runCastSend, + runCastCall, + resolveActionAddress, +} from '../utils/forge' + +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 +const PERFORM_SELECTOR = new Interface(['function perform()']).getFunction( + 'perform' +)!.selector + +function checkDeployScript(): void { + if (!fs.existsSync(DEPLOY_SCRIPT)) { + die(`Deploy script not found: ${DEPLOY_SCRIPT}`) + } +} + +async function deployAction(version: string, rpcUrl: string): Promise { + checkDeployScript() + + await runForgeScript({ + script: DEPLOY_SCRIPT, + rpcUrl, + env: { ARBOS_VERSION: version }, + }) +} + +async function executeUpgrade( + actionAddress: string, + upgradeExecutor: string, + rpcUrl: string +): Promise { + const iface = new Interface(['function execute(address,bytes)']) + const calldata = iface.encodeFunctionData('execute', [ + actionAddress, + PERFORM_SELECTOR, + ]) + + console.log('Calldata for UpgradeExecutor.execute():') + console.log('') + console.log(`To: ${upgradeExecutor}`) + console.log(`Calldata: ${calldata}`) + + if (process.env.FOUNDRY_BROADCAST) { + console.log('Broadcasting transaction...') + await runCastSend({ to: upgradeExecutor, data: calldata, rpcUrl }) + + console.log('ArbOS upgrade scheduled successfully') + } else { + console.log( + 'Set FOUNDRY_BROADCAST=true in .env to broadcast this transaction.' + ) + } +} + +async function verifyUpgrade(rpcUrl: string): Promise { + console.log('Checking ArbOS upgrade status...') + + const scheduledIface = new Interface([ + 'function getScheduledUpgrade() view returns (uint64, uint64)', + ]) + const scheduledRaw = await runCastCall({ + to: ARB_OWNER_PUBLIC, + data: scheduledIface.encodeFunctionData('getScheduledUpgrade'), + rpcUrl, + }) + const [version, timestamp] = scheduledIface.decodeFunctionResult( + 'getScheduledUpgrade', + scheduledRaw + ) + console.log( + `Scheduled upgrade (version, timestamp): (${version}, ${timestamp})` + ) + + const arbSysIface = new Interface([ + 'function arbOSVersion() view returns (uint64)', + ]) + const versionRaw = await runCastCall({ + to: ARB_SYS, + data: arbSysIface.encodeFunctionData('arbOSVersion'), + rpcUrl, + }) + const [currentVersionRaw] = arbSysIface.decodeFunctionResult( + 'arbOSVersion', + versionRaw + ) + const currentVersion = Number(currentVersionRaw) - ARBOS_VERSION_OFFSET + + console.log(`Current ArbOS version: ${currentVersion}`) +} + +async function cmdDeploy(version: string): Promise { + if (process.env.UPGRADE_ACTION_ADDRESS) { + console.log( + `Action already deployed at: ${process.env.UPGRADE_ACTION_ADDRESS}` + ) + console.log( + 'Run execute next, or remove UPGRADE_ACTION_ADDRESS from .env to redeploy.' + ) + return + } + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') + console.log(`Running: ${path.basename(DEPLOY_SCRIPT)} for ArbOS ${version}`) + await deployAction(version, rpcUrl) + + if (!process.env.FOUNDRY_BROADCAST) { + console.log( + 'Rerun with FOUNDRY_BROADCAST=true to deploy, then run execute.' + ) + return + } + + const address = await resolveActionAddress(DEPLOY_SCRIPT, rpcUrl) + console.log(`Deployed action address: ${address}`) + console.log( + 'Run "execute" next, or set UPGRADE_ACTION_ADDRESS in .env to override' + ) +} + +async function cmdExecute(): Promise { + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') + const upgradeExecutor = requireEnv('CHILD_UPGRADE_EXECUTOR_ADDRESS') + const actionAddress = await resolveActionAddress(DEPLOY_SCRIPT, rpcUrl) + + console.log(`Executing ArbOS upgrade action: ${actionAddress}`) + + await executeUpgrade(actionAddress, upgradeExecutor, rpcUrl) +} + +async function cmdVerify(): Promise { + const rpcUrl = requireEnv('CHILD_CHAIN_RPC') + await verifyUpgrade(rpcUrl) +} + +export { cmdDeploy, cmdExecute, cmdVerify } diff --git a/src/cli/commands/contract-upgrade.ts b/src/cli/commands/contract-upgrade.ts new file mode 100644 index 00000000..578fde99 --- /dev/null +++ b/src/cli/commands/contract-upgrade.ts @@ -0,0 +1,115 @@ +import * as path from 'path' +import * as fs from 'fs' +import { die } from '../utils/log' +import { requireEnv, getScriptsDir } from '../utils/env' +import { + runForgeScript, + resolveActionAddress, + 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): Promise { + if (process.env.UPGRADE_ACTION_ADDRESS) { + console.log( + `Action already deployed at: ${process.env.UPGRADE_ACTION_ADDRESS}` + ) + console.log( + 'Run execute next, or remove UPGRADE_ACTION_ADDRESS from .env to redeploy.' + ) + return + } + + const versionDir = getVersionDir(version) + + const rpcUrl = requireEnv('PARENT_CHAIN_RPC') + + const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/) + if (!deployScript) { + die(`No deploy script found in ${versionDir}`) + } + + console.log(`Running: ${path.basename(deployScript)}`) + + await runForgeScript({ + script: deployScript, + rpcUrl, + }) + + if (!process.env.FOUNDRY_BROADCAST) { + console.log( + 'Rerun with FOUNDRY_BROADCAST=true to deploy, then run execute.' + ) + return + } + + const address = await resolveActionAddress(deployScript, rpcUrl) + console.log(`Deployed action address: ${address}`) + console.log( + 'Run execute next, or set UPGRADE_ACTION_ADDRESS in .env to override' + ) +} + +async function cmdExecute(version: string): Promise { + const versionDir = getVersionDir(version) + const rpcUrl = requireEnv('PARENT_CHAIN_RPC') + const deployScript = findScript(versionDir, /^Deploy.*\.s\.sol$/) + if (!deployScript && !process.env.UPGRADE_ACTION_ADDRESS) { + die( + `No deploy script found in ${versionDir}.\n` + + 'Set UPGRADE_ACTION_ADDRESS in .env to provide the action address manually.' + ) + } + const actionAddress = await resolveActionAddress(deployScript, rpcUrl) + + const executeScript = findScript(versionDir, /^Execute.*\.s\.sol$/) + if (!executeScript) { + die(`No execute script found in ${versionDir}`) + } + + console.log(`Using action address: ${actionAddress}`) + console.log(`Running: ${path.basename(executeScript)}`) + + await runForgeScript({ + script: executeScript, + rpcUrl, + env: { UPGRADE_ACTION_ADDRESS: actionAddress }, + }) +} + +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` + ) + } + + console.log(`Running: ${path.basename(verifyScript)}`) + + await runForgeScript({ + script: verifyScript, + rpcUrl, + }) +} + +export { cmdDeploy, cmdExecute, cmdVerify, getVersionDir } diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 00000000..93339294 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +import { program } from 'commander' +import { loadEnv } from './utils/env' +import { router } from './router' + +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') + .action(async (pathArg?: string, args?: string[]) => { + await router(pathArg, args) + }) + +program.parse() diff --git a/src/cli/router.ts b/src/cli/router.ts new file mode 100644 index 00000000..23d7c077 --- /dev/null +++ b/src/cli/router.ts @@ -0,0 +1,190 @@ +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, +} from './commands/contract-upgrade' +import { + cmdDeploy as arbosDeploy, + cmdExecute as arbosExecute, + cmdVerify as arbosVerify, +} from './commands/arbos-upgrade' + +const HELP_TEXT = `Usage: [path] [args...] + +Browse and execute scripts from the foundry scripts directory. + +Browsing: + 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 + contract-upgrades//execute + contract-upgrades//verify + + arbos-upgrades/at-timestamp/deploy + arbos-upgrades/at-timestamp/execute + arbos-upgrades/at-timestamp/verify + +Commands can be chained (e.g. deploy && execute). The execute step +automatically reads the deployed address from the broadcast output. +Set UPGRADE_ACTION_ADDRESS in .env to override (e.g. for multisig flows). + +Forge behavior (broadcast, auth, verbosity, etc.) is configured via +FOUNDRY_* / ETH_* env vars in your .env file. See env templates for examples.` + +function listDirectory(dir: string): void { + const scriptsDir = getScriptsDir() + let relPath = path.relative(scriptsDir, dir) + if (relPath === '.') relPath = '' + + const isVersionDir = /^contract-upgrades\/[0-9][^/]*$/.test(relPath) + const isArbosDir = relPath === 'arbos-upgrades/at-timestamp' + if (isVersionDir || isArbosDir) { + const hasEnvTemplates = fs.existsSync(path.join(dir, 'env-templates')) + if (hasEnvTemplates) { + console.log( + 'Configure .env before running. See env-templates/ for examples.' + ) + } else { + console.log('Configure .env before running. See the README for details.') + } + } + console.log('') + + const contents = fs.readdirSync(dir) + for (const item of contents) { + if (!item.startsWith('.')) { + console.log(` ${item}`) + } + } + + if (isVersionDir) { + console.log('') + console.log('Commands:') + console.log(` ${relPath}/deploy`) + console.log(` ${relPath}/execute`) + console.log(` ${relPath}/verify`) + } else if (isArbosDir) { + console.log('') + console.log('Commands:') + console.log(` ${relPath}/deploy `) + console.log(` ${relPath}/execute`) + console.log(` ${relPath}/verify`) + } +} + +export async function router( + pathArg?: string, + args: string[] = [] +): Promise { + const scriptsDir = getScriptsDir() + + if (!pathArg) { + console.log('orbit-actions - CLI for Orbit chain upgrade actions') + console.log('') + console.log( + 'Browse and run upgrade scripts for Orbit chains. Configuration' + ) + console.log( + 'is read from .env in the project root. Forge behavior (broadcast,' + ) + console.log( + 'auth, verbosity) is controlled via FOUNDRY_* / ETH_* env vars.' + ) + console.log('') + console.log('Available:') + const contents = fs.readdirSync(scriptsDir) + for (const item of contents) { + if (!item.startsWith('.')) { + console.log(` ${item}/`) + } + } + console.log('') + console.log('Usage:') + console.log(' Browse scripts') + console.log(' /deploy Run a script') + console.log(' help Full usage details') + return + } + + if (pathArg === 'help' || pathArg === '--help' || pathArg === '-h') { + console.log(HELP_TEXT) + return + } + + // Block path traversal outside the scripts directory + if (pathArg.includes('..')) { + die('Invalid path: ".." is not allowed') + } + + 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) + + if (/^contract-upgrades\/[0-9][^/]*$/.test(relParent)) { + const version = path.basename(relParent) + + switch (basename) { + case 'deploy': + await contractDeploy(version) + return + case 'execute': + await contractExecute(version) + return + case 'verify': + await contractVerify(version) + return + } + } + + if (relParent === 'arbos-upgrades/at-timestamp') { + switch (basename) { + case 'deploy': { + const version = args[0] + if (!version) { + die( + 'ArbOS version required\nUsage: arbos-upgrades/at-timestamp/deploy ' + ) + } + await arbosDeploy(version) + return + } + case 'execute': + await arbosExecute() + return + case 'verify': + await arbosVerify() + return + } + } + } + + if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) { + listDirectory(fullPath) + return + } + + if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) { + const content = fs.readFileSync(fullPath, 'utf-8') + console.log(content) + return + } + + die(`Not found: ${pathArg} + +Use 'help' to see available commands.`) +} diff --git a/src/cli/utils/env.ts b/src/cli/utils/env.ts new file mode 100644 index 00000000..bc906555 --- /dev/null +++ b/src/cli/utils/env.ts @@ -0,0 +1,48 @@ +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 + 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 +} + +export function loadEnv(): void { + const repoRoot = findRepoRoot() + const envPath = path.join(repoRoot ?? process.cwd(), '.env') + if (fs.existsSync(envPath)) { + dotenv.config({ path: envPath }) + } +} + +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 +} + +export function getScriptsDir(): string { + const repoRoot = findRepoRoot() + if (repoRoot) { + return path.join(repoRoot, 'scripts', 'foundry') + } + return '/app/scripts/foundry' +} + +export function getRepoRoot(): string { + const root = findRepoRoot() + if (root) return root + console.warn('Could not find repo root, assuming /app') + return '/app' +} diff --git a/src/cli/utils/forge.ts b/src/cli/utils/forge.ts new file mode 100644 index 00000000..711b93ec --- /dev/null +++ b/src/cli/utils/forge.ts @@ -0,0 +1,142 @@ +import execa from 'execa' +import * as fs from 'fs' +import * as path from 'path' +import { die } from './log' +import { getRepoRoot } from './env' + +export interface ForgeScriptOptions { + script: string + rpcUrl: string + env?: Record +} + +export async function runForgeScript( + options: ForgeScriptOptions +): Promise { + const args = ['script', options.script, '--rpc-url', options.rpcUrl] + + console.log(`Running: forge ${args.join(' ')}`) + + try { + await execa('forge', args, { + stdio: 'inherit', + env: { ...process.env, ...options.env }, + }) + } catch { + die('Forge script failed') + } +} + +export interface CastSendOptions { + to: string + data: string + rpcUrl: string +} + +export async function runCastSend(options: CastSendOptions): Promise { + try { + await execa( + 'cast', + ['send', options.to, '--data', options.data, '--rpc-url', options.rpcUrl], + { stdio: 'inherit' } + ) + } catch { + die('Cast send failed') + } +} + +export interface CastCallOptions { + to: string + data: string + rpcUrl: string +} + +export async function runCastCall(options: CastCallOptions): Promise { + try { + const result = await execa( + 'cast', + ['call', options.to, '--data', options.data, '--rpc-url', options.rpcUrl], + { stderr: 'inherit' } + ) + return result.stdout + } catch { + die(`cast call failed on ${options.to}`) + } +} + +export async function getChainId(rpcUrl: string): Promise { + try { + const result = await execa('cast', ['chain-id', '--rpc-url', rpcUrl], { + stderr: 'inherit', + }) + return result.stdout.trim() + } catch { + die(`Failed to get chain ID from ${rpcUrl}`) + } +} + +// Assumes the action contract is the last CREATE in the broadcast file. +// This holds for all current deploy scripts, which deploy dependencies first +// and the action contract last. +function parseActionAddress( + scriptPath: string, + chainId: string +): string | null { + const scriptName = path.basename(scriptPath) + const repoRoot = getRepoRoot() + const broadcastFile = path.join( + repoRoot, + 'broadcast', + scriptName, + chainId, + 'run-latest.json' + ) + + if (!fs.existsSync(broadcastFile)) { + return null + } + + 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) { + return null + } + + return createTxs[createTxs.length - 1]?.contractAddress ?? null +} + +export async function resolveActionAddress( + deployScript: string | null, + rpcUrl: string +): Promise { + const fromEnv = process.env.UPGRADE_ACTION_ADDRESS + if (fromEnv) return fromEnv + + if (deployScript) { + const chainId = await getChainId(rpcUrl) + const fromBroadcast = parseActionAddress(deployScript, chainId) + if (fromBroadcast) return fromBroadcast + } + + die( + 'Could not resolve action address.\n' + + 'Either set UPGRADE_ACTION_ADDRESS in .env, or run deploy first.' + ) +} + +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)) { + 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..d1029944 --- /dev/null +++ b/src/cli/utils/log.ts @@ -0,0 +1,4 @@ +export function die(message: string): never { + console.error(`Error: ${message}`) + process.exit(1) +} diff --git a/test/docker/test-docker.bash b/test/docker/test-docker.bash new file mode 100755 index 00000000..e863e216 --- /dev/null +++ b/test/docker/test-docker.bash @@ -0,0 +1,167 @@ +#!/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 (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 --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 --entrypoint test "$IMAGE_NAME" -d out + +# Test 4: Browsing - list directories +echo "" +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" > /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 --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 --entrypoint yarn "$IMAGE_NAME" test:unit; then + echo "Unit tests: OK" +else + echo "Unit tests: FAILED" + 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 ===" +if [ $FAILURES -eq 0 ]; then + echo "All tests passed!" + exit 0 +else + echo "$FAILURES test(s) failed" + exit 1 +fi diff --git a/test/local/test-local.bash b/test/local/test-local.bash new file mode 100755 index 00000000..54cbea4e --- /dev/null +++ b/test/local/test-local.bash @@ -0,0 +1,78 @@ +#!/bin/bash +set -euo pipefail + +# Local (non-Docker) smoke tests for orbit-actions CLI +# Tests the CLI via yarn cli + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +echo "=== Local Smoke Tests ===" +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 +} + +cli() { + yarn --silent --cwd "$REPO_ROOT" cli -- "$@" +} + +echo "--- Prerequisites ---" +check "forge installed" command -v forge +check "cast installed" command -v cast +check "yarn installed" command -v yarn + +echo "" +echo "--- Directory Browsing ---" +check "list top level" cli +check_output "list contract-upgrades" "1.2.1" cli contract-upgrades +check_output "list contract-upgrades/1.2.1" "deploy" cli contract-upgrades/1.2.1 +check_output "list arbos-upgrades" "at-timestamp" cli arbos-upgrades + +echo "" +echo "--- File Viewing ---" +check_output "view README" "Nitro contracts" cli contract-upgrades/1.2.1/README.md + +echo "" +echo "--- Help ---" +check_output "help command" "Usage:" cli help + +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 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"