├── .changeset ├── README.md └── config.json ├── .circleci └── config.yml ├── .editorconfig ├── .gitattributes ├── .github └── CODEOWNERS ├── .gitignore ├── .nvmrc ├── CONTRIBUTING.md ├── README.md ├── mise.toml ├── nx.json ├── package.json ├── packages ├── cli │ ├── .prettierignore │ ├── CHANGELOG.md │ ├── drizzle.config.ts │ ├── drizzle │ │ ├── 0000_volatile_gertrude_yorkes.sql │ │ └── meta │ │ │ ├── 0000_snapshot.json │ │ │ └── _journal.json │ ├── package.json │ ├── src │ │ ├── actions │ │ │ ├── bridge │ │ │ │ └── wizard │ │ │ │ │ ├── BridgeWizard.tsx │ │ │ │ │ ├── bridgeWizardStore.ts │ │ │ │ │ └── steps │ │ │ │ │ ├── EnterAmount.tsx │ │ │ │ │ ├── EnterRecipient.tsx │ │ │ │ │ ├── SelectChains.tsx │ │ │ │ │ └── SelectNetwork.tsx │ │ │ ├── deploy-create2 │ │ │ │ ├── DeployCreate2Command.tsx │ │ │ │ ├── components │ │ │ │ │ ├── DeployStatus.tsx │ │ │ │ │ ├── ExternalSignerExecution.tsx │ │ │ │ │ ├── GasEstimation.tsx │ │ │ │ │ └── PrivateKeyExecution.tsx │ │ │ │ ├── computeDeploymentParams.ts │ │ │ │ ├── deployCreate2.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── useChecksForChains.ts │ │ │ │ │ ├── useChecksForChainsForContracts.ts │ │ │ │ │ ├── useCodeForChains.ts │ │ │ │ │ └── useRefetchCodeOnReceipt.ts │ │ │ │ ├── queries │ │ │ │ │ ├── preVerificationCheckQuery.ts │ │ │ │ │ └── simulationCheckQuery.ts │ │ │ │ ├── sendAllTransactionTasks.ts │ │ │ │ ├── types.ts │ │ │ │ └── wizard │ │ │ │ │ ├── DeployCreate2Wizard.tsx │ │ │ │ │ ├── deployCreate2WizardStore.ts │ │ │ │ │ └── steps │ │ │ │ │ ├── ConfigureConstructorArguments.tsx │ │ │ │ │ ├── ConfigureSalt.tsx │ │ │ │ │ ├── EnterFoundryProjectPath.tsx │ │ │ │ │ ├── SelectChains.tsx │ │ │ │ │ ├── SelectContract.tsx │ │ │ │ │ ├── SelectNetwork.tsx │ │ │ │ │ └── ShouldVerifyContract.tsx │ │ │ ├── deployCreateXCreate2.ts │ │ │ ├── verify │ │ │ │ ├── blockscout.ts │ │ │ │ ├── createStandardJsonInput.ts │ │ │ │ ├── getContractOnBlockscoutQuery.ts │ │ │ │ ├── getStandardJsonInputQuery.ts │ │ │ │ ├── identifyExplorerType.ts │ │ │ │ └── verifyOnBlockscoutMutation.ts │ │ │ └── verifyContract.ts │ │ ├── cli.tsx │ │ ├── commands │ │ │ ├── _app.tsx │ │ │ ├── bridge.tsx │ │ │ ├── deploy │ │ │ │ ├── create2-many.tsx │ │ │ │ └── create2.tsx │ │ │ ├── index.tsx │ │ │ └── verify.tsx │ │ ├── components │ │ │ ├── AbiItemForm.tsx │ │ │ ├── ChooseExecutionOption.tsx │ │ │ ├── navigation │ │ │ │ └── BackNavigation.tsx │ │ │ └── path-input │ │ │ │ ├── PathInput.tsx │ │ │ │ ├── useTextInput.tsx │ │ │ │ └── useTextInputState.tsx │ │ ├── constants │ │ │ ├── l1StandardBridgeAbi.ts │ │ │ └── multicall3Abi.ts │ │ ├── createWagmiConfig.ts │ │ ├── db │ │ │ ├── database.ts │ │ │ └── dbContext.tsx │ │ ├── hooks │ │ │ ├── useGasEstimation.ts │ │ │ └── useSaveWizardProgress.ts │ │ ├── models │ │ │ └── userContext.ts │ │ ├── queries │ │ │ ├── chainById.ts │ │ │ ├── chainByIdentifier.ts │ │ │ ├── chains.ts │ │ │ ├── forgeArtifact.ts │ │ │ ├── listFoundryProjectSolidityFiles.ts │ │ │ ├── superchainRegistryAddresses.ts │ │ │ ├── superchainRegistryChainList.ts │ │ │ └── userContext.ts │ │ ├── server │ │ │ ├── api.ts │ │ │ └── startServer.ts │ │ ├── stores │ │ │ ├── operationStore.ts │ │ │ └── transactionTaskStore.ts │ │ └── util │ │ │ ├── AsyncQueue.ts │ │ │ ├── TxSender.ts │ │ │ ├── abi.ts │ │ │ ├── blockExplorer.ts │ │ │ ├── broadcasts.ts │ │ │ ├── chains │ │ │ ├── chainIdentifier.ts │ │ │ ├── chains.ts │ │ │ ├── networks.ts │ │ │ └── superchainRegistryChainList.ts │ │ │ ├── config.ts │ │ │ ├── createx │ │ │ ├── computeCreate2Address.ts │ │ │ ├── constants.ts │ │ │ ├── deployCreate2Contract.ts │ │ │ └── salt.ts │ │ │ ├── fetchSuperchainRegistryAddresses.ts │ │ │ ├── fetchSuperchainRegistryChainList.ts │ │ │ ├── forge │ │ │ ├── findSolidityFiles.ts │ │ │ ├── foundryProject.ts │ │ │ └── readForgeArtifact.ts │ │ │ ├── resolveContractDeploymentParams.ts │ │ │ ├── schemas.ts │ │ │ ├── serialization.ts │ │ │ ├── sponsoredSender.ts │ │ │ ├── toCliFlags.ts │ │ │ ├── transactionTask.ts │ │ │ └── wizard-builder │ │ │ ├── createWizardStore.ts │ │ │ ├── defineWizard.ts │ │ │ ├── example.ts │ │ │ └── utils.ts │ └── tsconfig.json └── signer-frontend │ ├── .gitignore │ ├── README.md │ ├── components.json │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── favicon.ico │ ├── src │ ├── App.tsx │ ├── Providers.tsx │ ├── api.ts │ ├── assets │ │ └── react.svg │ ├── components │ │ ├── TransactionTasks.tsx │ │ └── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── sonner.tsx │ │ │ ├── table.tsx │ │ │ └── tabs.tsx │ ├── index.css │ ├── lib │ │ └── utils.ts │ ├── main.tsx │ ├── vite-env.d.ts │ └── wagmiConfig.ts │ ├── tailwind.config.js │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { 6 | "repo": "ethereum-optimism/super-cli" 7 | } 8 | ], 9 | "commit": false, 10 | "access": "public", 11 | "baseBranch": "main", 12 | "updateInternalDependencies": "patch", 13 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 14 | "onlyUpdatePeerDependentsWhenOutOfRange": true 15 | }, 16 | "ignore": [] 17 | } 18 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | utils: ethereum-optimism/circleci-utils@1.0.17 5 | nx: nrwl/nx@1.6.2 6 | 7 | 8 | 9 | commands: 10 | prepare-snapshot: 11 | description: "Prepare the snapshot name" 12 | steps: 13 | - run: 14 | name: Setup snapshot name 15 | command: | 16 | SNAPSHOT_NAME=${CIRCLE_BRANCH} 17 | echo "Will create snapshot using name $SNAPSHOT_NAME" 18 | echo "export SNAPSHOT_NAME=${SNAPSHOT_NAME}" >> "$BASH_ENV" 19 | - run: 20 | name: Prepare changeset version environment 21 | command: | 22 | echo "export GITHUB_TOKEN=${GITHUB_TOKEN_GOVERNANCE}" >> "$BASH_ENV" 23 | - run: 24 | name: Create snapshot versions 25 | command: pnpm release:version:snapshot 26 | 27 | setup: 28 | description: "Setup Node.js environment with pnpm and nx" 29 | steps: 30 | - utils/checkout-with-mise # Install dependencies 31 | - run: 32 | name: Install dependencies 33 | environment: 34 | NPM_TOKEN: nada 35 | command: | 36 | pnpm i --frozen-lockfile 37 | 38 | jobs: 39 | publish-to-npm: 40 | machine: 41 | image: ubuntu-2204:2024.08.1 42 | parameters: 43 | prepare-snapshot: 44 | type: boolean 45 | default: false 46 | steps: 47 | - setup 48 | - run: 49 | name: Check NPM Token 50 | command: | 51 | if [ -z "${NPM_TOKEN}" ]; then 52 | echo "NPM_TOKEN is not set. Please set it in CircleCI project settings." 53 | exit 1 54 | fi 55 | 56 | - run: 57 | name: Configure NPM Token and Registry 58 | command: | 59 | npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}" 60 | 61 | - run: 62 | name: Verify NPM Token 63 | command: npm whoami 64 | 65 | - run: 66 | name: Build 67 | command: pnpm nx run-many --target=build 68 | environment: 69 | NPM_TOKEN: nada 70 | 71 | - when: 72 | condition: 73 | equal: [<< parameters.prepare-snapshot >>, true] 74 | steps: 75 | - prepare-snapshot 76 | 77 | - utils/changesets: 78 | createGithubReleases: false 79 | publish: "pnpm release:publish" 80 | version: "pnpm release:version" 81 | 82 | 83 | check: 84 | machine: 85 | image: ubuntu-2204:2024.08.1 86 | steps: 87 | - setup 88 | - nx/set-shas 89 | - run: 90 | name: Build 91 | command: pnpm nx affected --base=$NX_BASE --head=$NX_HEAD --target=build 92 | - run: 93 | name: Lint 94 | command: pnpm nx affected --base=$NX_BASE --head=$NX_HEAD --target=lint 95 | - run: 96 | name: Unit Tests 97 | command: pnpm nx affected --base=$NX_BASE --head=$NX_HEAD --target=test 98 | - run: 99 | name: Typecheck 100 | command: pnpm nx affected --base=$NX_BASE --head=$NX_HEAD --target=typecheck 101 | 102 | workflows: 103 | check-workflow: 104 | jobs: 105 | - check: 106 | filters: 107 | branches: 108 | ignore: main # ignore main branch as it is included in the release workflow 109 | release-workflow: 110 | jobs: 111 | - check: 112 | filters: 113 | branches: 114 | only: main 115 | - publish-to-npm: 116 | name: Publish new versions 117 | context: 118 | - circleci-repo-super-cli # for GITHUB_TOKEN_GOVERNANCE && NPM_TOKEN 119 | requires: 120 | - check 121 | filters: 122 | branches: 123 | only: main 124 | 125 | - publish-to-npm: 126 | name: Publish snapshot versions 127 | prepare-snapshot: true 128 | context: 129 | - circleci-repo-super-cli # for GITHUB_TOKEN_GOVERNANCE && NPM_TOKEN 130 | requires: 131 | - check 132 | - "Publish new versions" # Changed from 'publish-to-npm' to the actual job name 133 | filters: 134 | branches: 135 | only: main 136 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default owners 2 | * @ethereum-optimism/devxpod 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | **/node_modules 3 | /.pnp 4 | packages/*/.env 5 | .npmrc 6 | 7 | # testing 8 | /coverage 9 | 10 | # build artifacts 11 | **/target/ 12 | **/build/ 13 | **/dist/ 14 | **/tsconfig.tsbuildinfo 15 | .nx 16 | 17 | # misc 18 | .DS_Store 19 | .env 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | 27 | local.db -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.10 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # super-cli 2 | 3 | ## Local Development Setup 4 | 5 | **Install Node** 6 | 7 | ```bash 8 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash 9 | nvm install 10 | ``` 11 | 12 | **Install pnpm** 13 | 14 | ```bash 15 | corepack enable pnpm 16 | corepack use pnpm@9.0.2 17 | ``` 18 | 19 | **Install Dependencies & Build** 20 | 21 | ```bash 22 | pnpm i 23 | pnpm nx run-many -t build 24 | ``` 25 | 26 | ## Package Tasks 27 | 28 | Each package contains the following tasks: 29 | 30 | - `build` 31 | - `lint` 32 | - `lint:fix` 33 | - `typecheck` 34 | - `test` 35 | 36 | You can run these tasks individually in a single package or across all packages. 37 | 38 | **Linting** 39 | 40 | ```bash 41 | # Lint all packages 42 | pnpm nx run-many -t lint 43 | 44 | # Lint a single package 45 | pnpm nx run :lint 46 | ``` 47 | 48 | **Typechecking** 49 | 50 | ```bash 51 | # Typecheck all packages 52 | pnpm nx run-many -t typecheck 53 | 54 | # Typecheck a single package 55 | pnpm nx run :typecheck 56 | ``` 57 | 58 | **Unit Testing** 59 | 60 | ```bash 61 | # Run unit tests for all packages 62 | pnpm nx run-many -t test 63 | 64 | # Run unit tests for a single package 65 | pnpm nx run :test 66 | ``` 67 | 68 | ## Getting Started 69 | 70 | **Run the CLI** 71 | 72 | ```bash 73 | pnpm nx run cli:dev 74 | ``` 75 | 76 | ## RPC URL Override 77 | 78 | You can override the RPC URL by setting the `{name}_RPC_URL` environment variable. 79 | 80 | For example, lets say we wanted to override OP & Base Mainnet we could do. 81 | 82 | ``` 83 | OP_RPC_URL=... 84 | BASE_RPC_URL=... 85 | ``` 86 | 87 | It uses the keys that exist in the superchain registry. 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡️ `sup` - Superchain CLI 2 | 3 | `sup` is a CLI tool to help deploy and manage contracts on the Superchain. 4 | 5 | ## ✨ Features 6 | 7 | - 🤝 works with existing `foundry` projects (`sup` is a companion, not a replacement to `foundry`) 8 | - 🕹️ interactive mode (no more juggling cli flags) 9 | - 🚀 deploy and verify **multiple** contracts to **multiple** chains at once 10 | - 💸 bridge funds to multiple chains at once (no more "how do I get gas on all of these chains?") 11 | - 🔑 use connected wallets (Metamask / WalletConnect) to deploy contracts (no more `.env` files with private keys) 12 | 13 | ## 🚀 Getting started 14 | 15 | ### 1. Install prerequisites: `node.js` 16 | 17 | Follow [this guide](https://nodejs.org/en/download) to install Node.js. 18 | 19 | ### 2. Install `sup` 20 | 21 | ```sh 22 | npm i -g @eth-optimism/super-cli 23 | ``` 24 | 25 | ### 3. Run the CLI 26 | 27 | ```bash 28 | sup 29 | ``` 30 | 31 | ## 🔀 First steps 32 | 33 | ### Deploy a contract from a `foundry` project 34 | 35 | WIP 36 | 37 | ## RPC URL Override 38 | 39 | You can override the RPC URL by setting the `{name}_RPC_URL` environment variable. 40 | 41 | For example, lets say we wanted to override OP & Base Mainnet 42 | 43 | ``` 44 | OP_RPC_URL=... 45 | BASE_RPC_URL=... 46 | ``` 47 | 48 | ### ❓ Is `sup` a replacement for `foundry`? 49 | 50 | Nope, `foundry` is great! `sup` is meant to be a lightweight add-on tool to help with the annoyances of multichain development. It works with your existing `foundry` project, and expects you to use `foundry` to build contracts. It even emits the same broadcast artifacts when you deploy contracts. 51 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | node = "18.18.0" 3 | 4 | [hooks] 5 | # Enabling corepack will install the `pnpm` package manager specified in package.json 6 | postinstall = "npx corepack enable" 7 | 8 | [settings] 9 | # Needs to be enabled for hooks to work 10 | experimental = true -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "neverConnectToCloud": true, 4 | "targetDefaults": { 5 | "build": { 6 | "cache": true, 7 | "dependsOn": ["^build"] 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "super", 3 | "private": true, 4 | "packageManager": "pnpm@9.0.2", 5 | "nx": {}, 6 | "scripts": { 7 | "release:publish": "changeset publish", 8 | "release:version": "changeset version && pnpm install --lockfile-only", 9 | "release:version:snapshot": "changeset version --snapshot ${SNAPSHOT_NAME:-snapshot} && pnpm install --lockfile-only" 10 | }, 11 | "dependencies": { 12 | "@changesets/cli": "^2.27.10", 13 | "nx": "^20.1.2" 14 | }, 15 | "devDependencies": { 16 | "@changesets/changelog-github": "^0.5.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/cli/.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @eth-optimism/super-cli 2 | 3 | ## 0.0.13 4 | 5 | ### Patch Changes 6 | 7 | - [#85](https://github.com/ethereum-optimism/super-cli/pull/85) [`4c8757a`](https://github.com/ethereum-optimism/super-cli/commit/4c8757af2acd38d56177ee4d6ea75d762d344e0e) Thanks [@jakim929](https://github.com/jakim929)! - fixed blockscout get smart contract function 8 | 9 | ## 0.0.12 10 | 11 | ### Patch Changes 12 | 13 | - [#83](https://github.com/ethereum-optimism/super-cli/pull/83) [`47b3346`](https://github.com/ethereum-optimism/super-cli/commit/47b33462c8da2faf53862b9997023e113caba7f4) Thanks [@jakim929](https://github.com/jakim929)! - added sponsored deployer and checks before verifying 14 | 15 | ## 0.0.11 16 | 17 | ### Patch Changes 18 | 19 | - [#81](https://github.com/ethereum-optimism/super-cli/pull/81) [`15d5403`](https://github.com/ethereum-optimism/super-cli/commit/15d5403e30d8a37df1a4d6acaa9ab7ed1d4704ce) Thanks [@jakim929](https://github.com/jakim929)! - error out when cycle is detected, suggest foundry project path, --prepare mode handles string quotations 20 | 21 | ## 0.0.10 22 | 23 | ### Patch Changes 24 | 25 | - [#79](https://github.com/ethereum-optimism/super-cli/pull/79) [`fe860ce`](https://github.com/ethereum-optimism/super-cli/commit/fe860cec5aa946ae19bf3c121a623099d75f17c6) Thanks [@jakim929](https://github.com/jakim929)! - fixed private key deployments causing replacement tx 26 | 27 | ## 0.0.9 28 | 29 | ### Patch Changes 30 | 31 | - [#77](https://github.com/ethereum-optimism/super-cli/pull/77) [`2148322`](https://github.com/ethereum-optimism/super-cli/commit/21483224bf2a0ceabbe8d16bd19cb39a98f47ff1) Thanks [@jakim929](https://github.com/jakim929)! - added create2-many command 32 | 33 | ## 0.0.8 34 | 35 | ### Patch Changes 36 | 37 | - [#75](https://github.com/ethereum-optimism/super-cli/pull/75) [`25cafbf`](https://github.com/ethereum-optimism/super-cli/commit/25cafbf3ed41393591044830503a8d76cb014d6b) Thanks [@tremarkley](https://github.com/tremarkley)! - bump nvmrc to 20.10 38 | 39 | ## 0.0.7 40 | 41 | ### Patch Changes 42 | 43 | - [#72](https://github.com/ethereum-optimism/super-cli/pull/72) [`0079643`](https://github.com/ethereum-optimism/super-cli/commit/00796432e682e327bf3a3a962a482a4b28d635ee) Thanks [@jakim929](https://github.com/jakim929)! - added exit when finished deploying 44 | 45 | ## 0.0.6 46 | 47 | ### Patch Changes 48 | 49 | - [#70](https://github.com/ethereum-optimism/super-cli/pull/70) [`87862c0`](https://github.com/ethereum-optimism/super-cli/commit/87862c0f5da41f2d7a8f8a2b7c26abac8499eb3a) Thanks [@tremarkley](https://github.com/tremarkley)! - bump node down to 18.20 50 | 51 | ## 0.0.5 52 | 53 | ### Patch Changes 54 | 55 | - [#65](https://github.com/ethereum-optimism/super-cli/pull/65) [`5a52a70`](https://github.com/ethereum-optimism/super-cli/commit/5a52a7064f47e3932084ee1ef8f153156bb08a4b) Thanks [@jakim929](https://github.com/jakim929)! - added interop-alpha support and sponsored sender flow 56 | 57 | ## 0.0.4 58 | 59 | ### Patch Changes 60 | 61 | - [#63](https://github.com/ethereum-optimism/super-cli/pull/63) [`53e9ba8`](https://github.com/ethereum-optimism/super-cli/commit/53e9ba88c269e02fb5c4932dd9b937c952c7c37b) Thanks [@jakim929](https://github.com/jakim929)! - fixed bug with chainIdentifierToChain and added back navigation 62 | 63 | ## 0.0.3 64 | 65 | ### Patch Changes 66 | 67 | - [#60](https://github.com/ethereum-optimism/super-cli/pull/60) [`a639010`](https://github.com/ethereum-optimism/super-cli/commit/a6390104897e30d77cb73bbfc18fa82076604f5c) Thanks [@jakim929](https://github.com/jakim929)! - improved path input, updated frontend, updated @eth-optimism/viem package 68 | 69 | ## 0.0.2 70 | 71 | ### Patch Changes 72 | 73 | - [#20](https://github.com/ethereum-optimism/super-cli/pull/20) [`229ec3e`](https://github.com/ethereum-optimism/super-cli/commit/229ec3e4849707ddd370ae1a9a12eb96a489bff5) Thanks [@nitaliano](https://github.com/nitaliano)! - Init 74 | -------------------------------------------------------------------------------- /packages/cli/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'drizzle-kit'; 2 | 3 | export default defineConfig({ 4 | out: './drizzle', 5 | schema: './src/models/**/*.ts', 6 | dialect: 'sqlite', 7 | }); 8 | -------------------------------------------------------------------------------- /packages/cli/drizzle/0000_volatile_gertrude_yorkes.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `user_context` ( 2 | `id` text PRIMARY KEY NOT NULL, 3 | `updated_at` integer NOT NULL, 4 | `created_at` integer NOT NULL, 5 | `forge_project_path` text, 6 | `last_wizard_id` text, 7 | `last_wizard_state` text 8 | ); 9 | -------------------------------------------------------------------------------- /packages/cli/drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "id": "a5ac4492-685e-4358-9df3-c684c4d79dbd", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "user_context": { 8 | "name": "user_context", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "text", 13 | "primaryKey": true, 14 | "notNull": true, 15 | "autoincrement": false 16 | }, 17 | "updated_at": { 18 | "name": "updated_at", 19 | "type": "integer", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false 23 | }, 24 | "created_at": { 25 | "name": "created_at", 26 | "type": "integer", 27 | "primaryKey": false, 28 | "notNull": true, 29 | "autoincrement": false 30 | }, 31 | "forge_project_path": { 32 | "name": "forge_project_path", 33 | "type": "text", 34 | "primaryKey": false, 35 | "notNull": false, 36 | "autoincrement": false 37 | }, 38 | "last_wizard_id": { 39 | "name": "last_wizard_id", 40 | "type": "text", 41 | "primaryKey": false, 42 | "notNull": false, 43 | "autoincrement": false 44 | }, 45 | "last_wizard_state": { 46 | "name": "last_wizard_state", 47 | "type": "text", 48 | "primaryKey": false, 49 | "notNull": false, 50 | "autoincrement": false 51 | } 52 | }, 53 | "indexes": {}, 54 | "foreignKeys": {}, 55 | "compositePrimaryKeys": {}, 56 | "uniqueConstraints": {}, 57 | "checkConstraints": {} 58 | } 59 | }, 60 | "views": {}, 61 | "enums": {}, 62 | "_meta": { 63 | "schemas": {}, 64 | "tables": {}, 65 | "columns": {} 66 | }, 67 | "internal": { 68 | "indexes": {} 69 | } 70 | } -------------------------------------------------------------------------------- /packages/cli/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1734384509921, 9 | "tag": "0000_volatile_gertrude_yorkes", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eth-optimism/super-cli", 3 | "version": "0.0.13", 4 | "license": "MIT", 5 | "bin": { 6 | "sup": "dist/cli.js" 7 | }, 8 | "type": "module", 9 | "engines": { 10 | "node": ">=18.20" 11 | }, 12 | "scripts": { 13 | "build": "tsc && resolve-tspaths && pnpm copy:signer-frontend", 14 | "build:watch": "tsc --watch", 15 | "dev": "tsx src/cli.tsx", 16 | "lint": "oxlint && pnpm prettier --check \"**/*.{ts,tsx}\"", 17 | "lint:fix": "oxlint --fix && pnpm prettier \"**/*.{ts,tsx}\" --write --loglevel=warn", 18 | "start": "node dist/cli.js", 19 | "typecheck": "tsc --noEmit", 20 | "copy:signer-frontend": "cp -r ../signer-frontend/dist dist/signer-frontend", 21 | "migrations:generate": "drizzle-kit generate" 22 | }, 23 | "files": [ 24 | "dist", 25 | "drizzle" 26 | ], 27 | "dependencies": { 28 | "@eth-optimism/viem": "^0.3.2", 29 | "@hono/node-server": "^1.13.7", 30 | "@inkjs/ui": "^2.0.0", 31 | "@libsql/client": "^0.14.0", 32 | "@tanstack/react-query": "^5.59.20", 33 | "@vitejs/plugin-react": "^4.3.3", 34 | "@wagmi/core": "^2.16.0", 35 | "abitype": "^1.0.6", 36 | "chalk": "^5.3.0", 37 | "dependency-graph": "^1.0.0", 38 | "dotenv": "^16.4.7", 39 | "drizzle-orm": "^0.38.1", 40 | "fast-json-stable-stringify": "^2.1.0", 41 | "figures": "^6.1.0", 42 | "form-data": "^4.0.1", 43 | "hono": "^4.6.14", 44 | "immer": "^10.1.1", 45 | "ink": "^5.0.1", 46 | "ink-big-text": "^2.0.0", 47 | "ink-gradient": "^3.0.0", 48 | "ink-link": "^4.1.0", 49 | "pastel": "^3.0.0", 50 | "react": "^18.2.0", 51 | "smol-toml": "^1.3.1", 52 | "viem": "^2.21.41", 53 | "wagmi": "^2.14.2", 54 | "zod": "^3.21.4", 55 | "zod-validation-error": "^3.4.0", 56 | "zustand": "^5.0.1" 57 | }, 58 | "devDependencies": { 59 | "@eth-optimism/super-cli-signer-frontend": "workspace:*", 60 | "@types/node": "^22.9.0", 61 | "@types/react": "^18.0.32", 62 | "@vdemedes/prettier-config": "^2.0.1", 63 | "drizzle-kit": "^0.30.0", 64 | "eslint-config-xo-react": "^0.27.0", 65 | "eslint-plugin-react": "^7.32.2", 66 | "eslint-plugin-react-hooks": "^4.6.0", 67 | "ink-testing-library": "^4.0.0", 68 | "node-gyp": "^10.2.0", 69 | "oxlint": "^0.12.0", 70 | "prettier": "^2.8.7", 71 | "resolve-tspaths": "^0.8.22", 72 | "ts-node": "^10.9.1", 73 | "tsx": "^4.19.2", 74 | "typescript": "^5.0.3", 75 | "vite": "^5.4.11" 76 | }, 77 | "prettier": "@vdemedes/prettier-config" 78 | } -------------------------------------------------------------------------------- /packages/cli/src/actions/bridge/wizard/bridgeWizardStore.ts: -------------------------------------------------------------------------------- 1 | import {defineWizard, InferStepId} from '@/util/wizard-builder/defineWizard'; 2 | import {z} from 'zod'; 3 | import {formatEther} from 'viem'; 4 | import {createWizardStore} from '@/util/wizard-builder/createWizardStore'; 5 | import {zodAddress} from '@/util/schemas'; 6 | 7 | const bridgeWizard = defineWizard() 8 | .addStep({ 9 | id: 'select-network', 10 | schema: z.object({ 11 | network: z.string(), 12 | }), 13 | title: 'Select Network', 14 | getSummary: state => `${state.network}`, 15 | }) 16 | .addStep({ 17 | id: 'select-chains', 18 | schema: z.object({ 19 | chains: z.array(z.string()), 20 | }), 21 | title: 'Select Chains', 22 | getSummary: state => `${state.chains.join(', ')}`, 23 | }) 24 | .addStep({ 25 | id: 'enter-recipient', 26 | schema: z.object({ 27 | recipient: zodAddress, 28 | }), 29 | title: 'Enter Recipient', 30 | getSummary: () => '', 31 | }) 32 | .addStep({ 33 | id: 'enter-amount', 34 | schema: z.object({ 35 | amount: z.bigint(), 36 | }), 37 | title: 'Enter Amount', 38 | getSummary: state => { 39 | const perChainAmount = Number(formatEther(state.amount)).toFixed(2); 40 | const totalAmount = Number( 41 | formatEther(state.amount * BigInt(state.chains.length)), 42 | ).toFixed(2); 43 | return `${perChainAmount} ETH × ${state.chains.length} chains = ${totalAmount} ETH total`; 44 | }, 45 | }) 46 | .build(); 47 | 48 | export const bridgeWizardIndexByStepId = bridgeWizard.reduce( 49 | (acc, step, index) => { 50 | acc[step.id] = index; 51 | return acc; 52 | }, 53 | {} as Record, 54 | ); 55 | 56 | export type BridgeWizardStepId = InferStepId; 57 | 58 | export const useBridgeWizardStore = createWizardStore(bridgeWizard); 59 | -------------------------------------------------------------------------------- /packages/cli/src/actions/bridge/wizard/steps/EnterAmount.tsx: -------------------------------------------------------------------------------- 1 | import {useBridgeWizardStore} from '@/actions/bridge/wizard/bridgeWizardStore'; 2 | import {Select, Spinner} from '@inkjs/ui'; 3 | import {useQuery} from '@tanstack/react-query'; 4 | import {Box, Text} from 'ink'; 5 | import {formatEther, parseEther} from 'viem'; 6 | import {createPublicClient, http} from 'viem'; 7 | import {Address} from 'viem'; 8 | import {mainnet, sepolia} from 'viem/chains'; 9 | 10 | const getBalanceForNetworkL1 = (network: string, address: Address) => { 11 | // TODO support supersim 12 | const chain = network === 'mainnet' ? mainnet : sepolia; 13 | const client = createPublicClient({ 14 | transport: http(), 15 | chain, 16 | }); 17 | 18 | return client.getBalance({ 19 | address, 20 | }); 21 | }; 22 | 23 | const useBalance = (network: string, address: Address) => { 24 | return useQuery({ 25 | queryKey: ['balance', 'l1', network, address], 26 | queryFn: () => getBalanceForNetworkL1(network, address), 27 | staleTime: Infinity, 28 | }); 29 | }; 30 | 31 | const supportedAmounts: bigint[] = [ 32 | parseEther('0.01'), 33 | parseEther('0.05'), 34 | parseEther('0.1'), 35 | parseEther('0.25'), 36 | parseEther('0.5'), 37 | ]; 38 | 39 | export const EnterAmount = () => { 40 | const {wizardState, submitEnterAmount} = useBridgeWizardStore(); 41 | 42 | if (wizardState.stepId !== 'enter-amount') { 43 | throw new Error('Invalid state'); 44 | } 45 | 46 | const {data: balance, isLoading: isLoadingBalance} = useBalance( 47 | wizardState.network, 48 | wizardState.recipient, 49 | ); 50 | 51 | const numChains = wizardState.chains.length; 52 | 53 | return ( 54 | 55 | 56 | How much would you like to bridge to {numChains} chain 57 | {numChains === 1 ? '' : 's'}?{' '} 58 | 59 | 60 | 61 | Balance on {wizardState.network}: 62 | {isLoadingBalance ? ( 63 | 64 | ) : balance ? ( 65 | {formatEther(balance)} ETH 66 | ) : ( 67 | Unable to fetch balance 68 | )} 69 | 70 | 71 | 72 | ({ 20 | label: `${network} (${ 21 | chainById[chainIdByParentChainName[network]]?.name 22 | })`, 23 | value: network, 24 | }))} 25 | onChange={value => submitSelectNetwork({network: value})} 26 | /> 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/components/ExternalSignerExecution.tsx: -------------------------------------------------------------------------------- 1 | import {executeTransactionOperation} from '@/actions/deploy-create2/deployCreate2'; 2 | import {useOperation} from '@/stores/operationStore'; 3 | import {Address, Chain, Hex} from 'viem'; 4 | import {useWaitForTransactionReceipt} from 'wagmi'; 5 | import {Spinner, Badge} from '@inkjs/ui'; 6 | import {Text, Box} from 'ink'; 7 | import {getBlockExplorerAddressLink} from '@/util/blockExplorer'; 8 | 9 | export const ExternalSignerExecution = ({ 10 | chain, 11 | initCode, 12 | baseSalt, 13 | deterministicAddress, 14 | }: { 15 | chain: Chain; 16 | initCode: Hex; 17 | baseSalt: Hex; 18 | deterministicAddress: Address; 19 | }) => { 20 | const {data: hash} = useOperation( 21 | executeTransactionOperation({ 22 | chainId: chain.id, 23 | deterministicAddress, 24 | initCode, 25 | baseSalt, 26 | }), 27 | ); 28 | 29 | const {data: receipt, isLoading: isReceiptLoading} = 30 | useWaitForTransactionReceipt({ 31 | hash, 32 | chainId: chain.id, 33 | }); 34 | 35 | if (!hash) { 36 | return ( 37 | 38 | 39 | 40 | Send the transaction at 41 | 42 | http://localhost:3000 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | if (isReceiptLoading || !receipt) { 50 | return ; 51 | } 52 | 53 | return ( 54 | 55 | Deployed 56 | Contract successfully deployed 57 | {getBlockExplorerAddressLink(chain, deterministicAddress)} 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/components/GasEstimation.tsx: -------------------------------------------------------------------------------- 1 | import {formatEther, formatUnits} from 'viem'; 2 | import {Text} from 'ink'; 3 | 4 | export const GasEstimation = ({ 5 | gasEstimation, 6 | }: { 7 | gasEstimation: { 8 | totalFee: bigint; 9 | estimatedL1Fee: bigint; 10 | estimatedL2Gas: bigint; 11 | l2GasPrice: bigint; 12 | }; 13 | }) => { 14 | return ( 15 | 16 | (L1 Fee: 17 | {formatEther(gasEstimation.estimatedL1Fee)} ETH 18 | ) + (L2 Gas: 19 | {gasEstimation.estimatedL2Gas.toString()} 20 | gas × L2 Gas Price: 21 | {formatUnits(gasEstimation.l2GasPrice, 9)} gwei 22 | ) = 23 | 24 | {formatEther(gasEstimation.totalFee)} ETH 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/components/PrivateKeyExecution.tsx: -------------------------------------------------------------------------------- 1 | import {executeTransactionOperation} from '@/actions/deploy-create2/deployCreate2'; 2 | import {useOperation} from '@/stores/operationStore'; 3 | import {Spinner, Badge} from '@inkjs/ui'; 4 | import {Address, Chain, Hex} from 'viem'; 5 | import {useWaitForTransactionReceipt} from 'wagmi'; 6 | import {getBlockExplorerAddressLink} from '@/util/blockExplorer'; 7 | import {Text, Box} from 'ink'; 8 | 9 | export const PrivateKeyExecution = ({ 10 | chain, 11 | initCode, 12 | baseSalt, 13 | deterministicAddress, 14 | }: { 15 | chain: Chain; 16 | initCode: Hex; 17 | baseSalt: Hex; 18 | deterministicAddress: Address; 19 | }) => { 20 | const { 21 | status, 22 | data: transactionHash, 23 | error, 24 | } = useOperation( 25 | executeTransactionOperation({ 26 | chainId: chain.id, 27 | deterministicAddress, 28 | initCode, 29 | baseSalt, 30 | }), 31 | ); 32 | 33 | const {isLoading: isReceiptLoading} = useWaitForTransactionReceipt({ 34 | hash: transactionHash, 35 | chainId: chain.id, 36 | }); 37 | 38 | if (status === 'pending') { 39 | return ; 40 | } 41 | 42 | if (error) { 43 | return ( 44 | 45 | {/* @ts-expect-error */} 46 | Error deploying contract: {error.shortMessage || error.message} 47 | 48 | ); 49 | } 50 | 51 | if (isReceiptLoading) { 52 | return ; 53 | } 54 | 55 | return ( 56 | 57 | Deployed 58 | Contract successfully deployed 59 | {getBlockExplorerAddressLink(chain, deterministicAddress)} 60 | 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/computeDeploymentParams.ts: -------------------------------------------------------------------------------- 1 | import {computeCreate2Address} from '@/util/createx/computeCreate2Address'; 2 | import {createBaseSalt, createGuardedSalt} from '@/util/createx/salt'; 3 | import {getEncodedConstructorArgs} from '@/util/abi'; 4 | import {concatHex, keccak256, toHex} from 'viem'; 5 | import { 6 | ComputedDeploymentParams, 7 | DeploymentIntent, 8 | } from '@/actions/deploy-create2/types'; 9 | 10 | // Prepares params for deployCreate2 11 | export const computeDeploymentParams = ({ 12 | forgeArtifact, 13 | constructorArgs, 14 | salt, 15 | }: Pick< 16 | DeploymentIntent, 17 | 'forgeArtifact' | 'constructorArgs' | 'salt' 18 | >): ComputedDeploymentParams => { 19 | const baseSalt = createBaseSalt({ 20 | shouldAddRedeployProtection: false, 21 | additionalEntropy: toHex(salt, {size: 32}), 22 | }); 23 | 24 | const guardedSalt = createGuardedSalt({ 25 | baseSalt: toHex(salt, {size: 32}), 26 | }); 27 | 28 | const encodedConstructorArgs = getEncodedConstructorArgs( 29 | forgeArtifact.abi, 30 | constructorArgs, 31 | ); 32 | 33 | const initCode = encodedConstructorArgs 34 | ? concatHex([forgeArtifact.bytecode.object, encodedConstructorArgs]) 35 | : forgeArtifact.bytecode.object; 36 | 37 | const deterministicAddress = computeCreate2Address({ 38 | guardedSalt, 39 | initCodeHash: keccak256(initCode), 40 | }); 41 | 42 | return { 43 | initCode, 44 | baseSalt, 45 | deterministicAddress, 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/deployCreate2.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Broadcast, 3 | MultichainBroadcast, 4 | writeMultichainBroadcast, 5 | } from '@/util/broadcasts'; 6 | import {CREATEX_ADDRESS, createXABI} from '@/util/createx/constants'; 7 | import {fromFoundryArtifactPath} from '@/util/forge/foundryProject'; 8 | 9 | import {runOperation, runOperationsMany} from '@/stores/operationStore'; 10 | import {requestTransactionTask} from '@/stores/transactionTaskStore'; 11 | import {Config, getTransaction, waitForTransactionReceipt} from '@wagmi/core'; 12 | import {Address, Chain, encodeFunctionData, Hex} from 'viem'; 13 | import {TxSender} from '@/util/TxSender'; 14 | 15 | export const executeTransactionOperation = ({ 16 | chainId, 17 | deterministicAddress, 18 | initCode, 19 | baseSalt, 20 | txSender, 21 | }: { 22 | chainId: number; 23 | deterministicAddress: Address; 24 | initCode: Hex; 25 | baseSalt: Hex; 26 | txSender?: TxSender; 27 | }) => { 28 | return { 29 | key: ['executeTransaction', chainId, deterministicAddress], 30 | fn: async () => { 31 | if (txSender) { 32 | return await txSender.sendTx({ 33 | chainId, 34 | to: CREATEX_ADDRESS, 35 | data: encodeFunctionData({ 36 | abi: createXABI, 37 | functionName: 'deployCreate2', 38 | args: [baseSalt, initCode], 39 | }), 40 | }); 41 | } 42 | return await requestTransactionTask({ 43 | chainId, 44 | to: CREATEX_ADDRESS, 45 | data: encodeFunctionData({ 46 | abi: createXABI, 47 | functionName: 'deployCreate2', 48 | args: [baseSalt, initCode], 49 | }), 50 | }); 51 | }, 52 | }; 53 | }; 54 | 55 | export const writeBroadcastArtifactOperation = ({ 56 | foundryArtifactPath, 57 | contractArguments, 58 | deterministicAddress, 59 | broadcasts, 60 | }: { 61 | foundryArtifactPath: string; 62 | contractArguments: string[]; 63 | deterministicAddress: Address; 64 | broadcasts: Broadcast[]; 65 | }) => { 66 | return { 67 | key: ['writeBroadcastArtifact', foundryArtifactPath, deterministicAddress], 68 | fn: async () => { 69 | const {foundryProject, contractFileName} = await fromFoundryArtifactPath( 70 | foundryArtifactPath, 71 | ); 72 | 73 | const multichainBroadcast: MultichainBroadcast = { 74 | name: contractFileName, 75 | address: deterministicAddress, 76 | timestamp: Math.floor(Date.now() / 1000), 77 | type: 'CREATE2', 78 | contractArguments, 79 | foundryProjectRoot: foundryProject.baseDir, 80 | transactions: broadcasts.reduce((acc, broadcast) => { 81 | acc[broadcast.chainId] = broadcast; 82 | return acc; 83 | }, {} as Record), 84 | }; 85 | 86 | await writeMultichainBroadcast(multichainBroadcast); 87 | }, 88 | }; 89 | }; 90 | 91 | export const deployCreate2 = async ({ 92 | wagmiConfig, 93 | deterministicAddress, 94 | initCode, 95 | baseSalt, 96 | chains, 97 | foundryArtifactPath, 98 | contractArguments, 99 | txSender, 100 | }: { 101 | wagmiConfig: Config; 102 | deterministicAddress: Address; 103 | initCode: Hex; 104 | baseSalt: Hex; 105 | chains: Chain[]; 106 | foundryArtifactPath: string; 107 | contractArguments: string[]; 108 | txSender?: TxSender; 109 | }) => { 110 | const transactionHashes = await runOperationsMany( 111 | chains.map(chain => 112 | executeTransactionOperation({ 113 | chainId: chain.id, 114 | deterministicAddress, 115 | initCode, 116 | baseSalt, 117 | txSender, 118 | }), 119 | ), 120 | ); 121 | 122 | const receipts = await Promise.all( 123 | chains.map(async (chain, i) => { 124 | const receipt = await waitForTransactionReceipt(wagmiConfig, { 125 | chainId: chain.id, 126 | hash: transactionHashes[i]!, 127 | }); 128 | 129 | return receipt; 130 | }), 131 | ); 132 | 133 | const transactions = await Promise.all( 134 | chains.map(async (chain, i) => { 135 | const transaction = await getTransaction(wagmiConfig, { 136 | chainId: chain.id, 137 | hash: transactionHashes[i]!, 138 | }); 139 | 140 | return transaction; 141 | }), 142 | ); 143 | 144 | const broadcasts = transactions.map((transaction, i) => ({ 145 | chainId: chains[i]!.id, 146 | hash: transactionHashes[i]!, 147 | transaction, 148 | receipt: receipts[i]!, 149 | })); 150 | 151 | await runOperation( 152 | writeBroadcastArtifactOperation({ 153 | foundryArtifactPath, 154 | contractArguments, 155 | deterministicAddress, 156 | broadcasts, 157 | }), 158 | ); 159 | 160 | return broadcasts; 161 | }; 162 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/hooks/useChecksForChains.ts: -------------------------------------------------------------------------------- 1 | import {useConfig} from 'wagmi'; 2 | 3 | import {useQueries} from '@tanstack/react-query'; 4 | import {preVerificationCheckQueryOptions} from '@/actions/deploy-create2/queries/preVerificationCheckQuery'; 5 | import {simulationCheckQueryOptions} from '@/actions/deploy-create2/queries/simulationCheckQuery'; 6 | import {DeploymentParams} from '@/actions/deploy-create2/types'; 7 | 8 | // Gives a handle for the overall check status so the top level component can 9 | // display the appropriate UI 10 | export const useChecksForChains = ({ 11 | intent, 12 | computedParams, 13 | }: DeploymentParams) => { 14 | const wagmiConfig = useConfig(); 15 | 16 | const preVerificationCheckQueries = useQueries({ 17 | queries: intent.chains.map(chain => { 18 | return { 19 | ...preVerificationCheckQueryOptions(wagmiConfig, { 20 | deterministicAddress: computedParams.deterministicAddress, 21 | initCode: computedParams.initCode, 22 | baseSalt: computedParams.baseSalt, 23 | chainId: chain.id, 24 | }), 25 | }; 26 | }), 27 | }); 28 | 29 | const simulationQueries = useQueries({ 30 | queries: intent.chains.map(chain => { 31 | return { 32 | ...simulationCheckQueryOptions(wagmiConfig, { 33 | deterministicAddress: computedParams.deterministicAddress, 34 | initCode: computedParams.initCode, 35 | baseSalt: computedParams.baseSalt, 36 | chainId: chain.id, 37 | }), 38 | }; 39 | }), 40 | }); 41 | 42 | // Simulation reverts if the address is already deployed 43 | const isSimulationCompleted = simulationQueries.every( 44 | query => query.isSuccess, 45 | ); 46 | 47 | const isPreVerificationCheckCompleted = preVerificationCheckQueries.every( 48 | query => query.isSuccess, 49 | ); 50 | 51 | if (isSimulationCompleted && isPreVerificationCheckCompleted) { 52 | const chainIdsToDeployTo = intent.chains 53 | .filter( 54 | (_, i) => 55 | !preVerificationCheckQueries[i]!.data!.isAlreadyDeployed && 56 | simulationQueries[i]!.data!.isAddressSameAsExpected, 57 | ) 58 | .map(chain => chain.id); 59 | 60 | return { 61 | isSimulationCompleted, 62 | isPreVerificationCheckCompleted, 63 | chainIdsToDeployTo, 64 | }; 65 | } 66 | 67 | return { 68 | isSimulationCompleted, 69 | isPreVerificationCheckCompleted, 70 | chainIdsToDeployTo: undefined, 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/hooks/useChecksForChainsForContracts.ts: -------------------------------------------------------------------------------- 1 | import {Address} from 'viem'; 2 | import {useConfig} from 'wagmi'; 3 | 4 | import {useQueries} from '@tanstack/react-query'; 5 | import {preVerificationCheckQueryOptions} from '@/actions/deploy-create2/queries/preVerificationCheckQuery'; 6 | import {simulationCheckQueryOptions} from '@/actions/deploy-create2/queries/simulationCheckQuery'; 7 | import {DeploymentParams} from '@/actions/deploy-create2/types'; 8 | 9 | export const useChecksForChainsForContracts = ( 10 | deployments: DeploymentParams[], 11 | ) => { 12 | const wagmiConfig = useConfig(); 13 | 14 | const flattenedDeployments = deployments.flatMap(deployment => { 15 | return deployment.intent.chains.map(chain => ({ 16 | ...deployment, 17 | chain, 18 | })); 19 | }); 20 | 21 | const preVerificationCheckQueries = useQueries({ 22 | queries: flattenedDeployments.map(({chain, computedParams}) => ({ 23 | ...preVerificationCheckQueryOptions(wagmiConfig, { 24 | deterministicAddress: computedParams.deterministicAddress, 25 | initCode: computedParams.initCode, 26 | baseSalt: computedParams.baseSalt, 27 | chainId: chain.id, 28 | }), 29 | })), 30 | }); 31 | 32 | const simulationQueries = useQueries({ 33 | queries: flattenedDeployments.map(({chain, computedParams}) => ({ 34 | ...simulationCheckQueryOptions(wagmiConfig, { 35 | deterministicAddress: computedParams.deterministicAddress, 36 | initCode: computedParams.initCode, 37 | baseSalt: computedParams.baseSalt, 38 | chainId: chain.id, 39 | }), 40 | })), 41 | }); 42 | 43 | const isSimulationCompleted = simulationQueries.every( 44 | query => query.isSuccess, 45 | ); 46 | 47 | const isPreVerificationCheckCompleted = preVerificationCheckQueries.every( 48 | query => query.isSuccess, 49 | ); 50 | 51 | if (isSimulationCompleted && isPreVerificationCheckCompleted) { 52 | const shouldDeployArr = flattenedDeployments.map((_, i) => { 53 | return ( 54 | !preVerificationCheckQueries[i]!.data!.isAlreadyDeployed && 55 | simulationQueries[i]!.data!.isAddressSameAsExpected 56 | ); 57 | }); 58 | 59 | const chainIdSetByAddress = deployments.reduce< 60 | Record> 61 | >((acc, deployment, deploymentIndex) => { 62 | acc[deployment.computedParams.deterministicAddress] = new Set( 63 | deployment.intent.chains 64 | .filter((_, chainIndex) => { 65 | const queryIndex = 66 | deploymentIndex * deployment.intent.chains.length + chainIndex; 67 | return shouldDeployArr[queryIndex]!; 68 | }) 69 | .map(chain => chain.id), 70 | ); 71 | return acc; 72 | }, {}); 73 | 74 | return { 75 | isSimulationCompleted, 76 | isPreVerificationCheckCompleted, 77 | chainIdSetByAddress, 78 | hasDeployableChains: shouldDeployArr.some(shouldDeploy => shouldDeploy), 79 | }; 80 | } 81 | 82 | return { 83 | isSimulationCompleted, 84 | isPreVerificationCheckCompleted, 85 | chainIdSetByAddress: undefined, 86 | hasDeployableChains: undefined, 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/hooks/useCodeForChains.ts: -------------------------------------------------------------------------------- 1 | import {useQueries} from '@tanstack/react-query'; 2 | import {Address} from 'viem'; 3 | import {useConfig} from 'wagmi'; 4 | import {getBytecodeQueryOptions} from 'wagmi/query'; 5 | 6 | export const useCodeForChains = (address: Address, chainIds: number[]) => { 7 | const wagmiConfig = useConfig(); 8 | 9 | const queries = useQueries({ 10 | queries: chainIds.map(chainId => { 11 | return { 12 | ...getBytecodeQueryOptions(wagmiConfig, { 13 | address, 14 | chainId, 15 | }), 16 | }; 17 | }), 18 | }); 19 | 20 | const isDeployedToAllChains = queries.every( 21 | query => query.data !== null && query.data !== undefined, 22 | ); 23 | 24 | return { 25 | isDeployedToAllChains, 26 | queries, 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/hooks/useRefetchCodeOnReceipt.ts: -------------------------------------------------------------------------------- 1 | import {useQueryClient} from '@tanstack/react-query'; 2 | import {getBytecodeQueryKey} from '@wagmi/core/query'; 3 | import {useEffect} from 'react'; 4 | import {Address, TransactionReceipt} from 'viem'; 5 | 6 | export const useRefetchCodeOnReceipt = ( 7 | address: Address, 8 | chainId: number, 9 | receipt?: TransactionReceipt, 10 | ) => { 11 | const queryClient = useQueryClient(); 12 | 13 | useEffect(() => { 14 | if (receipt) { 15 | queryClient.invalidateQueries({ 16 | queryKey: getBytecodeQueryKey({address, chainId}), 17 | }); 18 | } 19 | }, [receipt]); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/queries/preVerificationCheckQuery.ts: -------------------------------------------------------------------------------- 1 | import {Address, Hex} from 'viem'; 2 | import {Config} from 'wagmi'; 3 | 4 | import {getBytecode} from '@wagmi/core'; 5 | import {ComputedDeploymentParams} from '@/actions/deploy-create2/types'; 6 | 7 | export const preVerificationCheckQueryKey = ( 8 | deterministicAddress: Address, 9 | initCode: Hex, 10 | baseSalt: Hex, 11 | chainId: number, 12 | ) => { 13 | return [ 14 | 'preVerificationCheck', 15 | deterministicAddress, 16 | initCode, 17 | baseSalt, 18 | chainId, 19 | ]; 20 | }; 21 | 22 | export const preVerificationCheckQueryOptions = ( 23 | wagmiConfig: Config, 24 | { 25 | deterministicAddress, 26 | initCode, 27 | baseSalt, 28 | chainId, 29 | }: ComputedDeploymentParams & { 30 | chainId: number; 31 | }, 32 | ) => { 33 | return { 34 | queryKey: preVerificationCheckQueryKey( 35 | deterministicAddress, 36 | initCode, 37 | baseSalt, 38 | chainId, 39 | ), 40 | queryFn: async () => { 41 | const bytecode = await getBytecode(wagmiConfig, { 42 | address: deterministicAddress, 43 | chainId, 44 | }); 45 | 46 | return { 47 | isAlreadyDeployed: !!bytecode, 48 | }; 49 | }, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/queries/simulationCheckQuery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | BaseError, 4 | ContractFunctionRevertedError, 5 | getAddress, 6 | Hex, 7 | zeroAddress, 8 | } from 'viem'; 9 | import {Config} from 'wagmi'; 10 | 11 | import {simulateContract} from '@wagmi/core'; 12 | import {CREATEX_ADDRESS, createXABI} from '@/util/createx/constants'; 13 | import {supersimNetwork} from '@/util/chains/networks'; 14 | import {ComputedDeploymentParams} from '@/actions/deploy-create2/types'; 15 | 16 | // Heuristics for funded accounts on chains 17 | const getFundedAccountForChain = (chainId: number) => { 18 | // @ts-expect-error 19 | if (supersimNetwork.chains.map(c => c.id).includes(chainId)) { 20 | return '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'; 21 | } 22 | 23 | return zeroAddress; 24 | }; 25 | 26 | export const simulationCheckQueryKey = ( 27 | deterministicAddress: Address, 28 | initCode: Hex, 29 | baseSalt: Hex, 30 | chainId: number, 31 | ) => { 32 | return ['simulationCheck', deterministicAddress, initCode, baseSalt, chainId]; 33 | }; 34 | 35 | export const simulationCheckQueryOptions = ( 36 | wagmiConfig: Config, 37 | { 38 | deterministicAddress, 39 | initCode, 40 | baseSalt, 41 | chainId, 42 | }: ComputedDeploymentParams & { 43 | chainId: number; 44 | }, 45 | ) => { 46 | return { 47 | queryKey: simulationCheckQueryKey( 48 | deterministicAddress, 49 | initCode, 50 | baseSalt, 51 | chainId, 52 | ), 53 | queryFn: async () => { 54 | try { 55 | const simulationResult = await simulateContract(wagmiConfig, { 56 | abi: createXABI, 57 | account: getFundedAccountForChain(chainId), 58 | address: CREATEX_ADDRESS, 59 | chainId, 60 | functionName: 'deployCreate2', 61 | args: [baseSalt, initCode], 62 | }); 63 | 64 | return { 65 | isAddressSameAsExpected: 66 | getAddress(simulationResult.result) === 67 | getAddress(deterministicAddress), 68 | }; 69 | } catch (err) { 70 | if (err instanceof BaseError) { 71 | const revertError = err.walk( 72 | err => err instanceof ContractFunctionRevertedError, 73 | ); 74 | 75 | if (revertError) { 76 | return { 77 | isAddressSameAsExpected: true, 78 | }; 79 | } 80 | } 81 | throw err; 82 | } 83 | }, 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/sendAllTransactionTasks.ts: -------------------------------------------------------------------------------- 1 | import {wagmiConfig} from '@/commands/_app'; 2 | import { 3 | onTaskSuccess, 4 | useTransactionTaskStore, 5 | } from '@/stores/transactionTaskStore'; 6 | import {chainById} from '@/util/chains/chains'; 7 | import {TxSender} from '@/util/TxSender'; 8 | import {http, sendTransaction} from '@wagmi/core'; 9 | import {createWalletClient, zeroAddress} from 'viem'; 10 | import {PrivateKeyAccount} from 'viem/accounts'; 11 | 12 | export const sendAllTransactionTasks = async (txSender: TxSender) => { 13 | const taskEntryById = useTransactionTaskStore.getState().taskEntryById; 14 | 15 | await Promise.all( 16 | Object.values(taskEntryById).map(async task => { 17 | const hash = await txSender.sendTx(task.request); 18 | onTaskSuccess(task.id, hash); 19 | }), 20 | ); 21 | }; 22 | 23 | export const sendAllTransactionTasksWithPrivateKeyAccount = async ( 24 | account: PrivateKeyAccount, 25 | ) => { 26 | const taskEntryById = useTransactionTaskStore.getState().taskEntryById; 27 | 28 | // Group transactions by chainId 29 | const tasksByChain = Object.values(taskEntryById).reduce((acc, task) => { 30 | const chainId = task.request.chainId; 31 | if (!acc[chainId]) { 32 | acc[chainId] = []; 33 | } 34 | acc[chainId].push(task); 35 | return acc; 36 | }, {} as Record); 37 | 38 | // Process each chain's transactions sequentially, but different chains can run in parallel 39 | await Promise.all( 40 | Object.entries(tasksByChain).map(async ([_, chainTasks]) => { 41 | for (const task of chainTasks) { 42 | const hash = await sendTransaction(wagmiConfig, { 43 | to: task.request.to, 44 | data: task.request.data, 45 | account, 46 | chainId: task.request.chainId, 47 | }); 48 | 49 | onTaskSuccess(task.id, hash); 50 | } 51 | }), 52 | ); 53 | }; 54 | 55 | export const sendAllTransactionTasksWithCustomWalletRpc = async ( 56 | getRpcUrl: (chainId: number) => string, 57 | ) => { 58 | const taskEntryById = useTransactionTaskStore.getState().taskEntryById; 59 | 60 | await Promise.all( 61 | Object.values(taskEntryById).map(async task => { 62 | const chain = chainById[task.request.chainId]; 63 | if (!chain) { 64 | throw new Error(`Chain ${task.request.chainId} not found`); 65 | } 66 | 67 | const walletClient = createWalletClient({ 68 | transport: http(getRpcUrl(chain.id)), 69 | }); 70 | 71 | const hash = await walletClient.sendTransaction({ 72 | to: task.request.to, 73 | data: task.request.data, 74 | account: zeroAddress, // will be ignored 75 | chain, 76 | }); 77 | 78 | onTaskSuccess(task.id, hash); 79 | }), 80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/types.ts: -------------------------------------------------------------------------------- 1 | import {ForgeArtifact} from '@/util/forge/readForgeArtifact'; 2 | import {Address, Chain, Hex} from 'viem'; 3 | 4 | export type DeploymentIntent = { 5 | chains: Chain[]; 6 | forgeArtifactPath: string; 7 | forgeArtifact: ForgeArtifact; 8 | constructorArgs?: any[]; 9 | salt: string; 10 | }; 11 | 12 | export type ComputedDeploymentParams = { 13 | deterministicAddress: Address; 14 | initCode: Hex; 15 | baseSalt: Hex; 16 | }; 17 | 18 | export type DeploymentParams = { 19 | intent: DeploymentIntent; 20 | computedParams: ComputedDeploymentParams; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/wizard/deployCreate2WizardStore.ts: -------------------------------------------------------------------------------- 1 | import {zodSupportedNetwork} from '@/util/fetchSuperchainRegistryChainList'; 2 | import {createWizardStore} from '@/util/wizard-builder/createWizardStore'; 3 | import {defineWizard, InferStepId} from '@/util/wizard-builder/defineWizard'; 4 | import {z} from 'zod'; 5 | 6 | const deployCreate2WizardStore = defineWizard() 7 | .addStep({ 8 | id: 'enter-foundry-project-path', 9 | schema: z.object({ 10 | foundryProjectPath: z.string(), 11 | }), 12 | title: 'Enter Foundry Project Path', 13 | getSummary: state => state.foundryProjectPath, 14 | }) 15 | .addStep({ 16 | id: 'select-contract', 17 | schema: z.object({ 18 | selectedContract: z.string(), 19 | }), 20 | title: 'Select Contract', 21 | getSummary: state => state.selectedContract, 22 | }) 23 | .addStep({ 24 | id: 'configure-constructor-arguments', 25 | schema: z.object({ 26 | constructorArgs: z.array(z.string()), 27 | }), 28 | title: 'Configure Constructor Arguments', 29 | getSummary: state => state.constructorArgs.join(', '), 30 | }) 31 | .addStep({ 32 | id: 'configure-salt', 33 | schema: z.object({ 34 | salt: z.string(), 35 | }), 36 | title: 'Configure Salt', 37 | getSummary: state => state.salt, 38 | }) 39 | .addStep({ 40 | id: 'select-network', 41 | schema: z.object({ 42 | network: zodSupportedNetwork, 43 | }), 44 | title: 'Select Network', 45 | getSummary: state => state.network, 46 | }) 47 | .addStep({ 48 | id: 'select-chains', 49 | schema: z.object({ 50 | chainNames: z.array(z.string()), 51 | }), 52 | title: 'Select Chains', 53 | getSummary: state => state.chainNames.join(', '), 54 | }) 55 | .addStep({ 56 | id: 'should-verify-contract', 57 | schema: z.object({ 58 | shouldVerifyContract: z.boolean(), 59 | }), 60 | title: 'Verify Contract', 61 | getSummary: state => (state.shouldVerifyContract ? 'Yes' : 'No'), 62 | }) 63 | .build(); 64 | 65 | export type DeployCreate2WizardStore = typeof deployCreate2WizardStore; 66 | 67 | export type DeployCreate2WizardStepId = InferStepId< 68 | typeof deployCreate2WizardStore 69 | >; 70 | 71 | export const useDeployCreate2WizardStore = createWizardStore( 72 | deployCreate2WizardStore, 73 | ); 74 | 75 | export const deployCreate2WizardIndexByStepId = deployCreate2WizardStore.reduce( 76 | (acc, step, index) => { 77 | acc[step.id] = index; 78 | return acc; 79 | }, 80 | {} as Record, 81 | ); 82 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/wizard/steps/ConfigureConstructorArguments.tsx: -------------------------------------------------------------------------------- 1 | import {useDeployCreate2WizardStore} from '@/actions/deploy-create2/wizard/deployCreate2WizardStore'; 2 | import {getConstructorAbi} from '@/util/abi'; 3 | import {Box} from 'ink'; 4 | import {AbiItemForm} from '@/components/AbiItemForm'; 5 | import {getArtifactPathForContract} from '@/util/forge/foundryProject'; 6 | import {useForgeArtifact} from '@/queries/forgeArtifact'; 7 | import {Spinner} from '@inkjs/ui'; 8 | import {useEffect} from 'react'; 9 | 10 | export const ConfigureConstructorArguments = () => { 11 | const {wizardState, submitConfigureConstructorArguments} = 12 | useDeployCreate2WizardStore(); 13 | 14 | if (wizardState.stepId !== 'configure-constructor-arguments') { 15 | throw new Error('Invalid state'); 16 | } 17 | 18 | const path = getArtifactPathForContract( 19 | wizardState.foundryProjectPath, 20 | wizardState.selectedContract, 21 | ); 22 | 23 | const {data: artifact, isLoading} = useForgeArtifact(path); 24 | 25 | const constructorAbi = 26 | artifact === undefined ? undefined : getConstructorAbi(artifact.abi); 27 | 28 | useEffect(() => { 29 | if (artifact && !constructorAbi) { 30 | // Some contracts don't have a constructor 31 | submitConfigureConstructorArguments({constructorArgs: []}); 32 | } 33 | }, [artifact]); 34 | 35 | if (isLoading || !artifact) { 36 | return ( 37 | 38 | 39 | 40 | ); 41 | } 42 | 43 | if (!constructorAbi) { 44 | return null; 45 | } 46 | 47 | return ( 48 | 49 | { 52 | // @ts-ignore TODO fix type 53 | submitConfigureConstructorArguments({constructorArgs: result}); 54 | }} 55 | /> 56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/wizard/steps/ConfigureSalt.tsx: -------------------------------------------------------------------------------- 1 | import {useDeployCreate2WizardStore} from '@/actions/deploy-create2/wizard/deployCreate2WizardStore'; 2 | import {Box, Text} from 'ink'; 3 | import {TextInput} from '@inkjs/ui'; 4 | export const ConfigureSalt = () => { 5 | const {wizardState, submitConfigureSalt} = useDeployCreate2WizardStore(); 6 | 7 | if (wizardState.stepId !== 'configure-salt') { 8 | throw new Error('Invalid state'); 9 | } 10 | 11 | return ( 12 | 13 | 14 | Enter a salt value (press Enter to confirm): 15 | 16 | submitConfigureSalt({salt})} 18 | defaultValue={'ethers phoenix'} 19 | /> 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/wizard/steps/EnterFoundryProjectPath.tsx: -------------------------------------------------------------------------------- 1 | import {useDeployCreate2WizardStore} from '@/actions/deploy-create2/wizard/deployCreate2WizardStore'; 2 | import {Box, Text} from 'ink'; 3 | import {Spinner} from '@inkjs/ui'; 4 | import {useUpdateUserContext, useUserContext} from '@/queries/userContext'; 5 | import {PathInput} from '@/components/path-input/PathInput'; 6 | import {findFoundryRootDown} from '@/util/forge/foundryProject'; 7 | import {useState} from 'react'; 8 | 9 | export const EnterFoundryProjectPath = () => { 10 | const {submitEnterFoundryProjectPath} = useDeployCreate2WizardStore(); 11 | const [errorMessage, setErrorMessage] = useState(null); 12 | const [foundRoot, setFoundRoot] = useState(null); 13 | 14 | const {data: userContext, isLoading: isUserContextLoading} = useUserContext(); 15 | 16 | const {mutate: updateUserContext} = useUpdateUserContext(); 17 | 18 | // if (wizardState.stepId !== 'enter-foundry-project-path') { 19 | // console.log('Invalid state', wizardState.stepId); 20 | // throw new Error('Invalid state'); 21 | // } 22 | 23 | if (isUserContextLoading || !userContext) { 24 | return ; 25 | } 26 | 27 | return ( 28 | 29 | 30 | Enter the path to your 31 | 32 | Foundry 33 | 34 | project 35 | (press 36 | Tab 37 | for autocomplete, 38 | Ctrl+U 39 | to clear, default: 40 | 41 | . 42 | 43 | ) 44 | : 45 | 46 | {errorMessage && ( 47 | 48 | {errorMessage} 49 | {foundRoot && ( 50 | 51 | {foundRoot} 52 | 53 | )} 54 | 55 | )} 56 | { 59 | const projectPath = foundryProjectPath.trim(); 60 | // setErrorMessage(null); 61 | // setFoundRoot(null); 62 | 63 | let foundryRoot: string | undefined; 64 | try { 65 | foundryRoot = await findFoundryRootDown(projectPath, 4); 66 | 67 | if (foundryRoot && foundryRoot !== projectPath) { 68 | setErrorMessage( 69 | 'No foundry.toml found here, but one was found at:', 70 | ); 71 | setFoundRoot(foundryRoot); 72 | return; 73 | } 74 | 75 | if (projectPath !== '') { 76 | updateUserContext({ 77 | forgeProjectPath: projectPath, 78 | }); 79 | } 80 | 81 | submitEnterFoundryProjectPath({ 82 | foundryProjectPath: projectPath === '' ? '.' : projectPath, 83 | }); 84 | } catch (e) { 85 | setErrorMessage('Could not find a Foundry project (foundry.toml)'); 86 | } 87 | }} 88 | /> 89 | 90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/wizard/steps/SelectChains.tsx: -------------------------------------------------------------------------------- 1 | import {useDeployCreate2WizardStore} from '@/actions/deploy-create2/wizard/deployCreate2WizardStore'; 2 | import {rollupChainToIdentifier} from '@/util/chains/chainIdentifier'; 3 | import {networkByName} from '@/util/chains/networks'; 4 | import {MultiSelect} from '@inkjs/ui'; 5 | import {Box, Text} from 'ink'; 6 | import {useState} from 'react'; 7 | 8 | export const SelectChains = () => { 9 | const {wizardState, submitSelectChains} = useDeployCreate2WizardStore(); 10 | const [errorMessage, setErrorMessage] = useState(null); 11 | 12 | if (wizardState.stepId !== 'select-chains') { 13 | throw new Error('Invalid state'); 14 | } 15 | 16 | const network = networkByName[wizardState.network]!; 17 | 18 | return ( 19 | 20 | 21 | 22 | Select chains to deploy to{' '} 23 | 24 | ( 25 | ↑↓ 26 | navigate - more below, 27 | space 28 | select, 29 | enter 30 | to confirm) 31 | 32 | ({ 34 | label: `${chain.name} (${chain.id})`, 35 | value: rollupChainToIdentifier(chain).split('/')[1]!, 36 | }))} 37 | onSubmit={chainNames => { 38 | if (chainNames.length === 0) { 39 | setErrorMessage('You must select at least one chain'); 40 | return; 41 | } 42 | 43 | submitSelectChains({chainNames}); 44 | }} 45 | /> 46 | {errorMessage && {errorMessage}} 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/wizard/steps/SelectContract.tsx: -------------------------------------------------------------------------------- 1 | import {useFoundryProjectSolidityFiles} from '@/queries/listFoundryProjectSolidityFiles'; 2 | import {useDeployCreate2WizardStore} from '@/actions/deploy-create2/wizard/deployCreate2WizardStore'; 3 | import {Select, Spinner} from '@inkjs/ui'; 4 | import {Box, Text} from 'ink'; 5 | 6 | export const SelectContract = () => { 7 | const {wizardState, submitSelectContract} = useDeployCreate2WizardStore(); 8 | 9 | if (wizardState.stepId !== 'select-contract') { 10 | throw new Error('Invalid state'); 11 | } 12 | 13 | const { 14 | data: contractFileNames, 15 | isLoading, 16 | error, 17 | } = useFoundryProjectSolidityFiles(wizardState.foundryProjectPath); 18 | 19 | if (isLoading) { 20 | return ( 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | if (error || !contractFileNames) { 28 | return ( 29 | 30 | ❌ Failed to load contracts 31 | {error && {error.toString()}} 32 | 33 | ); 34 | } 35 | 36 | if (contractFileNames.length === 0) { 37 | return ( 38 | 39 | ⚠️ No contracts found in project 40 | 41 | ); 42 | } 43 | 44 | return ( 45 | 46 | Select which contract you want to deploy 47 | ({ 23 | label: `${network} (${ 24 | chainById[chainIdByParentChainName[network]]?.name 25 | })`, 26 | value: network, 27 | }))} 28 | onChange={(network: string) => 29 | submitSelectNetwork?.({network: network as SupportedNetwork}) 30 | } 31 | /> 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deploy-create2/wizard/steps/ShouldVerifyContract.tsx: -------------------------------------------------------------------------------- 1 | import {useDeployCreate2WizardStore} from '@/actions/deploy-create2/wizard/deployCreate2WizardStore'; 2 | import {ConfirmInput} from '@inkjs/ui'; 3 | import {Box, Text} from 'ink'; 4 | 5 | export const ShouldVerifyContract = () => { 6 | const {wizardState, submitShouldVerifyContract} = 7 | useDeployCreate2WizardStore(); 8 | 9 | if (wizardState.stepId !== 'should-verify-contract') { 10 | throw new Error('Invalid state'); 11 | } 12 | 13 | return ( 14 | 15 | Do you want to verify the contract on the block explorer? 16 | { 18 | submitShouldVerifyContract({shouldVerifyContract: true}); 19 | }} 20 | onCancel={() => { 21 | submitShouldVerifyContract({shouldVerifyContract: false}); 22 | }} 23 | /> 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/cli/src/actions/deployCreateXCreate2.ts: -------------------------------------------------------------------------------- 1 | import {zodSupportedNetwork} from '@/util/fetchSuperchainRegistryChainList'; 2 | import {zodPrivateKey} from '@/util/schemas'; 3 | import {option} from 'pastel'; 4 | 5 | import z from 'zod'; 6 | 7 | export const zodDeployCreateXCreate2Params = z.object({ 8 | forgeArtifactPath: z 9 | .string() 10 | .describe( 11 | option({ 12 | description: 'Path to the Forge artifact', 13 | alias: 'f', 14 | }), 15 | ) 16 | .min(1), 17 | constructorArgs: z 18 | .string() 19 | .describe( 20 | option({ 21 | description: 'Arguments to the constructor', 22 | alias: 'cargs', 23 | }), 24 | ) 25 | .min(1) 26 | .optional(), 27 | salt: z.string().describe( 28 | option({ 29 | description: 'Salt', 30 | alias: 's', 31 | }), 32 | ), 33 | privateKey: zodPrivateKey.optional().describe( 34 | option({ 35 | description: 'Signer private key', 36 | alias: 'pk', 37 | }), 38 | ), 39 | chains: z.array(z.string()).describe( 40 | option({ 41 | description: 'Chains to deploy to', 42 | alias: 'c', 43 | }), 44 | ), 45 | network: zodSupportedNetwork.describe( 46 | option({ 47 | description: 'Network to deploy to', 48 | alias: 'n', 49 | }), 50 | ), 51 | verify: z 52 | .boolean() 53 | .default(false) 54 | .optional() 55 | .describe( 56 | option({ 57 | description: 'Verify contract on deployed chains', 58 | alias: 'v', 59 | }), 60 | ), 61 | }); 62 | 63 | export type DeployCreateXCreate2Params = z.infer< 64 | typeof zodDeployCreateXCreate2Params 65 | >; 66 | -------------------------------------------------------------------------------- /packages/cli/src/actions/verify/blockscout.ts: -------------------------------------------------------------------------------- 1 | import {zodHex} from '@/util/schemas'; 2 | import {Address} from 'viem'; 3 | import {z} from 'zod'; 4 | 5 | // TODO switch to a real type 6 | export const verifyContractOnBlockscout = async ( 7 | blockscoutApiBaseUrl: string, 8 | contractAddress: Address, 9 | contractName: string, 10 | standardJsonInput: any, 11 | ) => { 12 | // ie. https://eth.blockscout.com/api/v2/smart-contracts/0x9c1c619176b4f8521a0ab166945d785b92aef453/verification/via/standard-input 13 | const url = `${blockscoutApiBaseUrl}/api/v2/smart-contracts/${contractAddress}/verification/via/standard-input`; 14 | 15 | const formData = new FormData(); 16 | 17 | const jsonBlob = new Blob([JSON.stringify(standardJsonInput)], { 18 | type: 'application/json', 19 | }); 20 | 21 | formData.append('compiler_version', standardJsonInput.version); 22 | formData.append('license_type', 'mit'); 23 | formData.append('contract_name', contractName); 24 | formData.append('autodetect_constructor_args', 'false'); 25 | formData.append('constructor_args', ''); 26 | formData.append('files[0]', jsonBlob, 'temp-input.json'); 27 | 28 | const response = await fetch(url, { 29 | method: 'POST', 30 | body: formData, 31 | }); 32 | 33 | const result = await response.json(); 34 | 35 | if ( 36 | result.message === 'Smart-contract verification started' || 37 | result.message === 'Already verified' 38 | ) { 39 | return; 40 | } 41 | 42 | throw new Error(result.message); 43 | // TODO poll for result - although blockscout doesn't have that endpoint 44 | }; 45 | 46 | const zBlockscoutSmartContract = z 47 | .object({ 48 | creation_bytecode: zodHex, 49 | is_verified: z.boolean().optional(), 50 | // TOOD: there's more but this is all we need for now 51 | // https://optimism-sepolia.blockscout.com/api-docs 52 | }) 53 | .transform(data => ({ 54 | creationBytecode: data.creation_bytecode, 55 | isVerified: !!data.is_verified, 56 | })); 57 | 58 | export const getSmartContractOnBlockscout = async ( 59 | blockscoutApiBaseUrl: string, 60 | contractAddress: Address, 61 | ) => { 62 | const url = `${blockscoutApiBaseUrl}/api/v2/smart-contracts/${contractAddress}`; 63 | 64 | const response = await fetch(url); 65 | 66 | if (response.status === 404) { 67 | throw new Error('Contract not found on Blockscout'); 68 | } 69 | 70 | const result = await response.json(); 71 | 72 | return zBlockscoutSmartContract.parse(result); 73 | }; 74 | -------------------------------------------------------------------------------- /packages/cli/src/actions/verify/createStandardJsonInput.ts: -------------------------------------------------------------------------------- 1 | import {getArtifactPathForContract} from '@/util/forge/foundryProject'; 2 | import {readForgeArtifact} from '@/util/forge/readForgeArtifact'; 3 | import fs from 'fs/promises'; 4 | 5 | // https://github.com/ethereum-optimism/optimism/issues/10202 6 | // https://github.com/foundry-rs/foundry/issues/7791 7 | // OP uses project root relative paths (ie. src/L2/...), and this breaks import remapping inside the artifact output 8 | // When writing a contract that imports OP contracts, this is a problem 9 | // The hacky solution is to assume src/ contracts are from OP monorepo 10 | // if (filename starts with 'src/' && can't find it in the local artifacts) 11 | // look for the file in the optimism contracts bedrock repo (check foundry.toml) 12 | // and use that as the source 13 | 14 | // TODO handle different install cases & different lib folders 15 | // ie. npm installs 16 | const opMonorepoPath = 'lib/optimism/packages/contracts-bedrock'; 17 | 18 | const getOpMonorepoFileName = (foundryPath: string, path: string) => { 19 | return `${foundryPath}/${opMonorepoPath}/${path}`; 20 | }; 21 | 22 | const getFile = async (path: string) => { 23 | return await fs.readFile(path, 'utf8'); 24 | }; 25 | 26 | const getSource = async (foundryProjectPath: string, path: string) => { 27 | let content: string; 28 | 29 | try { 30 | content = await getFile(`${foundryProjectPath}/${path}`); 31 | } catch (e) { 32 | if (path.startsWith('src/')) { 33 | content = await getFile(getOpMonorepoFileName(foundryProjectPath, path)); 34 | } else { 35 | throw e; 36 | } 37 | } 38 | return { 39 | content, 40 | }; 41 | }; 42 | 43 | const getSources = async (foundryProjectPath: string, paths: string[]) => { 44 | const sources = await Promise.all( 45 | paths.map(path => getSource(foundryProjectPath, path)), 46 | ); 47 | 48 | return Object.fromEntries( 49 | sources.map((source, index) => [paths[index], source]), 50 | ) as Record; 51 | }; 52 | 53 | export const createStandardJsonInput = async ( 54 | foundryProjectPath: string, 55 | contractFileName: string, 56 | ) => { 57 | const artifact = await readForgeArtifact( 58 | getArtifactPathForContract(foundryProjectPath, contractFileName), 59 | ); 60 | 61 | const sources = await getSources( 62 | foundryProjectPath, 63 | Object.keys(artifact.metadata.sources), 64 | ); 65 | 66 | return { 67 | version: artifact.metadata.compiler.version, 68 | language: artifact.metadata.language, 69 | sources: sources, 70 | settings: { 71 | remappings: artifact.metadata.settings.remappings, 72 | optimizer: artifact.metadata.settings.optimizer, 73 | metadata: artifact.metadata.settings.metadata, 74 | evmVersion: artifact.metadata.settings.evmVersion, 75 | libraries: artifact.metadata.settings.libraries, 76 | }, 77 | }; 78 | }; 79 | -------------------------------------------------------------------------------- /packages/cli/src/actions/verify/getContractOnBlockscoutQuery.ts: -------------------------------------------------------------------------------- 1 | import {getSmartContractOnBlockscout} from '@/actions/verify/blockscout'; 2 | import {Address, Chain} from 'viem'; 3 | 4 | export const getSmartContractOnBlockscoutQueryKey = ( 5 | contractAddress: Address, 6 | chain: Chain, 7 | ) => ['contractOnBlockscout', contractAddress, chain.id]; 8 | 9 | export const getSmartContractOnBlockscoutQuery = async ( 10 | contractAddress: Address, 11 | chain: Chain, 12 | ) => { 13 | const smartContract = await getSmartContractOnBlockscout( 14 | chain.blockExplorers!.default.url, 15 | contractAddress, 16 | ); 17 | 18 | return smartContract; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/cli/src/actions/verify/getStandardJsonInputQuery.ts: -------------------------------------------------------------------------------- 1 | import {fromFoundryArtifactPath} from '@/util/forge/foundryProject'; 2 | import {createStandardJsonInput} from '@/actions/verify/createStandardJsonInput'; 3 | import {useQuery} from '@tanstack/react-query'; 4 | 5 | export const getStandardJsonInputQueryKey = (forgeArtifactPath: string) => [ 6 | 'standardJsonInput', 7 | forgeArtifactPath, 8 | ]; 9 | 10 | export const getStandardJsonInputQuery = async (forgeArtifactPath: string) => { 11 | const {foundryProject, contractFileName} = await fromFoundryArtifactPath( 12 | forgeArtifactPath, 13 | ); 14 | 15 | const standardJsonInput = await createStandardJsonInput( 16 | foundryProject.baseDir, 17 | contractFileName, 18 | ); 19 | 20 | return standardJsonInput; 21 | }; 22 | 23 | export const useStandardJsonInputQuery = (forgeArtifactPath: string) => { 24 | return useQuery({ 25 | queryKey: getStandardJsonInputQueryKey(forgeArtifactPath), 26 | queryFn: () => getStandardJsonInputQuery(forgeArtifactPath), 27 | staleTime: Infinity, // For the duration of the CLI session, this is cached 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/cli/src/actions/verify/identifyExplorerType.ts: -------------------------------------------------------------------------------- 1 | // Heuristics to identify the type of explorer 2 | export const identifyExplorer = async (baseUrl: string) => { 3 | const apiUrl = `${baseUrl}/api`; 4 | const testAddress = '0x0000000000000000000000000000000000000000'; 5 | 6 | try { 7 | const response = await fetch( 8 | `${apiUrl}?module=account&action=balance&address=${testAddress}`, 9 | ); 10 | const data = await response.json(); 11 | 12 | // Etherscan returns a specific error for missing API key 13 | if (data.message === 'NOTOK' && data.result === 'Missing/Invalid API Key') { 14 | return 'etherscan' as const; 15 | } 16 | 17 | // If we get a successful response, it's likely Blockscout 18 | if (data.status === '1' && data.result) { 19 | return 'blockscout' as const; 20 | } 21 | } catch (error) { 22 | throw new Error('Error identifying explorer'); 23 | } 24 | 25 | throw new Error('Unknown explorer type'); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/cli/src/actions/verify/verifyOnBlockscoutMutation.ts: -------------------------------------------------------------------------------- 1 | import {verifyContractOnBlockscout} from '@/actions/verify/blockscout'; 2 | import {Address, Chain} from 'viem'; 3 | 4 | export const verifyOnBlockscoutMutationKey = ( 5 | address: Address, 6 | chain: Chain, 7 | // @ts-ignore 8 | standardJsonInput: any, // TODO: use this as part of cache key 9 | ) => { 10 | return ['verifyOnBlockscout', address, chain.id]; 11 | }; 12 | 13 | export const verifyOnBlockscoutMutation = async ( 14 | address: Address, 15 | chain: Chain, 16 | standardJsonInput: any, 17 | contractName: string, 18 | ) => { 19 | await verifyContractOnBlockscout( 20 | chain.blockExplorers!.default.url, 21 | address, 22 | contractName, 23 | standardJsonInput, 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/cli/src/actions/verifyContract.ts: -------------------------------------------------------------------------------- 1 | import {zodSupportedNetwork} from '@/util/fetchSuperchainRegistryChainList'; 2 | import {zodAddress} from '@/util/schemas'; 3 | 4 | import {option} from 'pastel'; 5 | import {z} from 'zod'; 6 | 7 | export const zodVerifyContractParams = z.object({ 8 | forgeArtifactPath: z 9 | .string() 10 | .describe( 11 | option({ 12 | description: 'Path to the Forge artifact', 13 | alias: 'f', 14 | }), 15 | ) 16 | .min(1), 17 | contractAddress: zodAddress.describe( 18 | option({description: 'Contract address', alias: 'a'}), 19 | ), 20 | network: zodSupportedNetwork.describe( 21 | option({ 22 | description: 'Network to verify on', 23 | alias: 'n', 24 | }), 25 | ), 26 | chains: z.array(z.string()).describe( 27 | option({ 28 | description: 'Chains to verify on', 29 | alias: 'c', 30 | }), 31 | ), 32 | }); 33 | -------------------------------------------------------------------------------- /packages/cli/src/cli.tsx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --no-warnings 2 | import Pastel from 'pastel'; 3 | 4 | const app = new Pastel({ 5 | importMeta: import.meta, 6 | }); 7 | 8 | await app.run(); 9 | -------------------------------------------------------------------------------- /packages/cli/src/commands/_app.tsx: -------------------------------------------------------------------------------- 1 | import type {AppProps} from 'pastel'; 2 | import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; 3 | import {useInput} from 'ink'; 4 | import {DbProvider} from '@/db/dbContext'; 5 | import {useEffect, useRef} from 'react'; 6 | import {createWagmiConfig} from '@/createWagmiConfig'; 7 | import {WagmiProvider} from 'wagmi'; 8 | import {startServer} from '@/server/startServer'; 9 | import {chainById} from '@/util/chains/chains'; 10 | 11 | export const queryClient = new QueryClient(); 12 | 13 | export const wagmiConfig = createWagmiConfig(chainById); 14 | 15 | const AppInner = ({children}: {children: React.ReactNode}) => { 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | }; 22 | 23 | export default function App({Component, commandProps}: AppProps) { 24 | const serverRef = useRef>>(); 25 | 26 | useInput((input, key) => { 27 | if (input === 'c' && key.ctrl) { 28 | process.exit(0); 29 | } 30 | }); 31 | 32 | useEffect(() => { 33 | startServer() 34 | .then(s => { 35 | serverRef.current = s; 36 | }) 37 | .catch(err => { 38 | console.error('Failed to start server:', err); 39 | process.exit(1); 40 | }); 41 | 42 | return () => { 43 | if (serverRef.current) { 44 | serverRef.current.close(err => { 45 | if (err) { 46 | console.error('Error while closing server:', err); 47 | } 48 | }); 49 | } 50 | }; 51 | }, []); 52 | 53 | return ( 54 | 55 | 56 | 57 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /packages/cli/src/commands/deploy/create2.tsx: -------------------------------------------------------------------------------- 1 | import {StatusMessage} from '@inkjs/ui'; 2 | import {zodDeployCreateXCreate2Params} from '@/actions/deployCreateXCreate2'; 3 | import {z} from 'zod'; 4 | import {option} from 'pastel'; 5 | import {DeployCreate2Wizard} from '@/actions/deploy-create2/wizard/DeployCreate2Wizard'; 6 | import {fromZodError} from 'zod-validation-error'; 7 | import {parseSuperConfigFromTOML} from '@/util/config'; 8 | 9 | import {DeployCreate2Command} from '@/actions/deploy-create2/DeployCreate2Command'; 10 | import {SupportedNetwork} from '@/util/fetchSuperchainRegistryChainList'; 11 | 12 | const zodDeployCreate2CommandEntrypointOptions = zodDeployCreateXCreate2Params 13 | .partial() 14 | .merge( 15 | z.object({ 16 | interactive: z 17 | .boolean() 18 | .default(false) 19 | .optional() 20 | .describe( 21 | option({ 22 | description: 'Interactive mode', 23 | alias: 'i', 24 | }), 25 | ), 26 | toml: z 27 | .string() 28 | .optional() 29 | .describe( 30 | option({ 31 | description: 'Path to a TOML file to use as a configuration', 32 | alias: 't', 33 | }), 34 | ), 35 | }), 36 | ); 37 | 38 | type EntrypointOptions = z.infer< 39 | typeof zodDeployCreate2CommandEntrypointOptions 40 | >; 41 | 42 | const DeployCreate2CommandEntrypoint = ({ 43 | options, 44 | }: { 45 | options: EntrypointOptions; 46 | }) => { 47 | if (options.interactive) { 48 | return ; 49 | } 50 | 51 | const commandOptions = options.toml 52 | ? getOptionsFromTOML(options.toml) 53 | : options; 54 | 55 | const parseResult = zodDeployCreateXCreate2Params.safeParse(commandOptions); 56 | 57 | return parseResult.success ? ( 58 | 62 | chain.split(',').map(chain => chain.trim()), 63 | ), 64 | }} 65 | /> 66 | ) : ( 67 | 68 | { 69 | fromZodError(parseResult.error, { 70 | maxIssuesInMessage: 1, 71 | prefix: '', 72 | prefixSeparator: '', 73 | }).message 74 | } 75 | 76 | ); 77 | }; 78 | 79 | const getOptionsFromTOML = (tomlPath: string) => { 80 | const superConfig = parseSuperConfigFromTOML(tomlPath); 81 | const params = superConfig.creation_params?.[0]; 82 | 83 | if (!params) { 84 | throw new Error('No creation params found in config file.'); 85 | } 86 | 87 | return { 88 | salt: params.salt, 89 | chains: params.chains, 90 | network: params.network as SupportedNetwork, 91 | verify: params.verify, 92 | constructorArgs: params.constructor_args?.join(','), 93 | }; 94 | }; 95 | 96 | export default DeployCreate2CommandEntrypoint; 97 | export const options = zodDeployCreate2CommandEntrypointOptions; 98 | -------------------------------------------------------------------------------- /packages/cli/src/commands/index.tsx: -------------------------------------------------------------------------------- 1 | import {BridgeWizard} from '@/actions/bridge/wizard/BridgeWizard'; 2 | import {DeployCreate2Wizard} from '@/actions/deploy-create2/wizard/DeployCreate2Wizard'; 3 | import {Select} from '@inkjs/ui'; 4 | import {Box, Text} from 'ink'; 5 | import {useState} from 'react'; 6 | import Gradient from 'ink-gradient'; 7 | import BigText from 'ink-big-text'; 8 | import {useUserContext} from '@/queries/userContext'; 9 | import {actionDescriptionByWizardId} from '@/models/userContext'; 10 | import {useBridgeWizardStore} from '@/actions/bridge/wizard/bridgeWizardStore'; 11 | import {useDeployCreate2WizardStore} from '@/actions/deploy-create2/wizard/deployCreate2WizardStore'; 12 | import {z} from 'zod'; 13 | import {option} from 'pastel'; 14 | 15 | const actions = [ 16 | {label: '🚀 Deploy a contract', value: 'deploy'}, 17 | {label: '🌉 Bridge assets', value: 'bridge'}, 18 | // {label: '✅ Verify a contract', value: 'verify'}, 19 | ]; 20 | 21 | const zodOptions = z.object({ 22 | prepare: z 23 | .boolean() 24 | .optional() 25 | .describe( 26 | option({ 27 | description: 'Print the command without executing it', 28 | alias: 'p', 29 | }), 30 | ), 31 | }); 32 | 33 | export const options = zodOptions; 34 | 35 | export default function DefaultEntrypoint({ 36 | options, 37 | }: { 38 | options: z.infer; 39 | }) { 40 | const [selectedOption, setSelectedOption] = useState< 41 | 'bridge' | 'deploy' | 'verify' | 'continue' | null 42 | >(null); 43 | 44 | const {data: userContext, isLoading: isUserContextLoading} = useUserContext(); 45 | 46 | if (isUserContextLoading || !userContext) { 47 | return null; 48 | } 49 | 50 | const {lastWizardId, lastWizardState} = userContext; 51 | 52 | const optionsWithLastAction = lastWizardId 53 | ? [ 54 | { 55 | label: `🔄 Pick up where you left off: ${actionDescriptionByWizardId[lastWizardId]}`, 56 | value: 'continue', 57 | }, 58 | ...actions, 59 | ] 60 | : actions; 61 | 62 | if (!selectedOption) { 63 | return ( 64 | 65 | 66 | 67 | 68 | 69 | {options.prepare && ( 70 | 71 | 72 | Prepare mode: Command will be displayed but not 73 | executed 74 | 75 | 76 | )} 77 | 78 | Sup, what would you like to do on the Superchain today? ✨ 79 | 80 | 81 | { 53 | if (option === 'privateKey') { 54 | setChosePrivateKey(true); 55 | } else if (option === 'externalSigner') { 56 | onSubmit({type: 'externalSigner'}); 57 | } else if (option === 'sponsoredSender') { 58 | setChoseSponsoredSender(true); 59 | } 60 | }} 61 | /> 62 | {chosePrivateKey && ( 63 | { 65 | onSubmit({type: 'privateKey', privateKey}); 66 | }} 67 | /> 68 | )} 69 | {choseSponsoredSender && ( 70 | { 72 | onSubmit({type: 'sponsoredSender', apiKey}); 73 | }} 74 | /> 75 | )} 76 | 77 | ); 78 | }; 79 | 80 | const PrivateKeyInput = ({onSubmit}: {onSubmit: (privateKey: Hex) => void}) => { 81 | const [errorMessage, setErrorMessage] = useState(''); 82 | const [resetKey, setResetKey] = useState(0); 83 | 84 | return ( 85 | 86 | 87 | Enter your private key for your account: 88 | 89 | { 92 | if (!isHex(privateKey)) { 93 | setErrorMessage('Invalid private key: must start with 0x'); 94 | setResetKey(prev => prev + 1); 95 | return; 96 | } 97 | 98 | try { 99 | privateKeyToAccount(privateKey); 100 | } catch (err) { 101 | const error = err as PrivateKeyToAccountErrorType; 102 | setErrorMessage( 103 | // @ts-expect-error 104 | `Invalid private key: ${error.shortMessage ?? error.message}`, 105 | ); 106 | setResetKey(prev => prev + 1); 107 | return; 108 | } 109 | 110 | onSubmit(privateKey); 111 | }} 112 | /> 113 | {errorMessage && ( 114 | 115 | {errorMessage ? `❌ ${errorMessage}` : ' '} 116 | 117 | )} 118 | 119 | ); 120 | }; 121 | 122 | export const DEFAULT_API_KEY_ETH_DENVER_2025 = 'eth-denver-2025-shared-7947'; 123 | 124 | const ApiKeyInput = ({onSubmit}: {onSubmit: (apiKey: string) => void}) => { 125 | const [errorMessage, setErrorMessage] = useState(''); 126 | const [resetKey, setResetKey] = useState(0); 127 | 128 | return ( 129 | 130 | 131 | Enter your API key for the sponsored sender: 132 | 133 | { 137 | if (!apiKey || apiKey.trim() === '') { 138 | setErrorMessage('API key cannot be empty'); 139 | setResetKey(prev => prev + 1); 140 | return; 141 | } 142 | 143 | onSubmit(apiKey.trim()); 144 | }} 145 | /> 146 | {errorMessage && ( 147 | 148 | {errorMessage ? `❌ ${errorMessage}` : ' '} 149 | 150 | )} 151 | 152 | ); 153 | }; 154 | -------------------------------------------------------------------------------- /packages/cli/src/components/navigation/BackNavigation.tsx: -------------------------------------------------------------------------------- 1 | import {Text, useInput} from 'ink'; 2 | 3 | // TODO: there's a bug here when a step automatically goes to the next step 4 | // ie. ConstructorArguments 5 | // it comes back to the current step. Will need to fix this 6 | 7 | interface BackNavigationProps { 8 | onBack: () => void; 9 | } 10 | 11 | export const BackNavigation = ({onBack}: BackNavigationProps) => { 12 | useInput((_, key) => { 13 | if (key.leftArrow) { 14 | onBack(); 15 | } 16 | }); 17 | 18 | return ( 19 | 20 | Press to go back 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/cli/src/components/path-input/useTextInput.tsx: -------------------------------------------------------------------------------- 1 | import {useMemo} from 'react'; 2 | import {useInput} from 'ink'; 3 | import chalk from 'chalk'; 4 | import {type TextInputState} from '@/components/path-input/useTextInputState'; 5 | 6 | export type UseTextInputProps = { 7 | /** 8 | * When disabled, user input is ignored. 9 | * 10 | * @default false 11 | */ 12 | isDisabled?: boolean; 13 | 14 | /** 15 | * Text input state. 16 | */ 17 | state: TextInputState; 18 | 19 | /** 20 | * Text to display when input is empty. 21 | */ 22 | placeholder?: string; 23 | }; 24 | 25 | export type UseTextInputResult = { 26 | /** 27 | * Input value. 28 | */ 29 | inputValue: string; 30 | }; 31 | 32 | const cursor = chalk.inverse(' '); 33 | 34 | export const useTextInput = ({ 35 | isDisabled = false, 36 | state, 37 | placeholder = '', 38 | }: UseTextInputProps): UseTextInputResult => { 39 | const renderedPlaceholder = useMemo(() => { 40 | if (isDisabled) { 41 | return placeholder ? chalk.dim(placeholder) : ''; 42 | } 43 | 44 | return placeholder && placeholder.length > 0 45 | ? chalk.inverse(placeholder[0]) + chalk.dim(placeholder.slice(1)) 46 | : cursor; 47 | }, [isDisabled, placeholder]); 48 | 49 | const renderedValue = useMemo(() => { 50 | if (isDisabled) { 51 | return state.value; 52 | } 53 | 54 | let index = 0; 55 | let result = state.value.length > 0 ? '' : cursor; 56 | 57 | for (const char of state.value) { 58 | result += index === state.cursorOffset ? chalk.inverse(char) : char; 59 | 60 | index++; 61 | } 62 | 63 | if (state.suggestion) { 64 | if (state.cursorOffset === state.value.length) { 65 | result += 66 | chalk.inverse(state.suggestion[0]) + 67 | chalk.dim(state.suggestion.slice(1)); 68 | } else { 69 | result += chalk.dim(state.suggestion); 70 | } 71 | 72 | return result; 73 | } 74 | 75 | if (state.value.length > 0 && state.cursorOffset === state.value.length) { 76 | result += cursor; 77 | } 78 | 79 | return result; 80 | }, [isDisabled, state.value, state.cursorOffset, state.suggestion]); 81 | 82 | useInput( 83 | (input, key) => { 84 | if ( 85 | key.upArrow || 86 | key.downArrow || 87 | (key.ctrl && input === 'c') || 88 | key.tab || 89 | (key.shift && key.tab) 90 | ) { 91 | return; 92 | } 93 | 94 | if (key.return) { 95 | state.submit(); 96 | return; 97 | } 98 | 99 | if (key.leftArrow) { 100 | state.moveCursorLeft(); 101 | } else if (key.rightArrow) { 102 | state.moveCursorRight(); 103 | } else if (key.backspace || key.delete) { 104 | state.delete(); 105 | } else { 106 | state.insert(input); 107 | } 108 | }, 109 | {isActive: !isDisabled}, 110 | ); 111 | 112 | return { 113 | inputValue: state.value.length > 0 ? renderedValue : renderedPlaceholder, 114 | }; 115 | }; 116 | -------------------------------------------------------------------------------- /packages/cli/src/createWagmiConfig.ts: -------------------------------------------------------------------------------- 1 | import {Chain, http} from 'viem'; 2 | import {createConfig} from 'wagmi'; 3 | 4 | export const createWagmiConfig = (chainById: Record) => { 5 | return createConfig({ 6 | chains: Object.values(chainById) as [Chain, ...Chain[]], 7 | transports: Object.fromEntries( 8 | Object.entries(chainById).map(([id]) => [Number(id), http()]), 9 | ), 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/cli/src/db/database.ts: -------------------------------------------------------------------------------- 1 | import {createClient} from '@libsql/client'; 2 | import {drizzle} from 'drizzle-orm/libsql'; 3 | import {migrate} from 'drizzle-orm/libsql/migrator'; 4 | import path from 'path'; 5 | import {fileURLToPath} from 'url'; 6 | import fs from 'fs/promises'; 7 | import os from 'os'; 8 | 9 | import 'dotenv/config'; 10 | 11 | const isDevMode = process.env['SUP_DEV_MODE'] === 'true'; 12 | 13 | const dbFileName = isDevMode ? 'sup.dev.db' : 'sup.db'; 14 | 15 | const dbDir = path.join(os.homedir(), '.sup'); 16 | const dbFile = path.join(dbDir, dbFileName); 17 | 18 | const createDir = async () => { 19 | // no-op if dir already exists 20 | if ( 21 | await fs 22 | .stat(dbDir) 23 | .then(stat => stat.isDirectory()) 24 | .catch(() => false) 25 | ) { 26 | return; 27 | } 28 | await fs.mkdir(dbDir, {recursive: true}); 29 | }; 30 | 31 | export async function initializeDatabase() { 32 | try { 33 | await createDir(); 34 | 35 | const client = createClient({ 36 | url: `file:${dbFile}`, 37 | }); 38 | 39 | const db = drizzle(client); 40 | 41 | return db; 42 | } catch (error) { 43 | throw new Error(`Failed to initialize database: ${error}`); 44 | } 45 | } 46 | 47 | export type DB = Awaited>; 48 | 49 | export async function runMigrations(db: DB) { 50 | try { 51 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 52 | 53 | // Resolve migrations path relative to your package 54 | const migrationsPath = path.join(__dirname, '../../drizzle'); 55 | 56 | await migrate(db, { 57 | migrationsFolder: migrationsPath, 58 | }); 59 | } catch (error) { 60 | throw new Error(`Failed to run migrations: ${error}`); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/cli/src/db/dbContext.tsx: -------------------------------------------------------------------------------- 1 | import {createContext, useContext, type ReactNode} from 'react'; 2 | import {type DB} from '@/db/database'; 3 | import {initializeDatabase, runMigrations} from '@/db/database'; 4 | import {useQuery} from '@tanstack/react-query'; 5 | 6 | export const useInitializedDb = () => { 7 | return useQuery({ 8 | queryKey: ['db'], 9 | queryFn: async () => { 10 | const db = await initializeDatabase(); 11 | await runMigrations(db); 12 | return db; 13 | }, 14 | staleTime: Infinity, 15 | retry: false, 16 | }); 17 | }; 18 | 19 | interface DbContextValue { 20 | db: DB; 21 | } 22 | 23 | const DbContext = createContext(undefined); 24 | 25 | export function DbProvider({children}: {children: ReactNode}) { 26 | const {data: db, isLoading, error} = useInitializedDb(); 27 | 28 | if (isLoading || error || !db) { 29 | return null; 30 | } 31 | 32 | return {children}; 33 | } 34 | 35 | export function useDb() { 36 | const context = useContext(DbContext); 37 | if (context === undefined) { 38 | throw new Error('useDb must be used within a DbProvider'); 39 | } 40 | return context.db; 41 | } 42 | -------------------------------------------------------------------------------- /packages/cli/src/hooks/useGasEstimation.ts: -------------------------------------------------------------------------------- 1 | import {useQuery} from '@tanstack/react-query'; 2 | import {Account, Address, Hex} from 'viem'; 3 | import {estimateL1Fee} from 'viem/op-stack'; 4 | import {useEstimateGas, usePublicClient, useGasPrice} from 'wagmi'; 5 | 6 | export const useGasEstimation = ({ 7 | to, 8 | data, 9 | account, 10 | chainId, 11 | }: { 12 | to: Address; 13 | data: Hex; 14 | account: Account | Address; 15 | chainId: number; 16 | }) => { 17 | const publicClient = usePublicClient({ 18 | chainId, 19 | }); 20 | 21 | const { 22 | data: estimatedL1Fee, 23 | isLoading: isL1FeeEstimationLoading, 24 | error: l1FeeEstimationError, 25 | } = useQuery({ 26 | queryKey: ['estimateL1Fee', chainId, to, account, data], 27 | queryFn: () => { 28 | // @ts-expect-error 29 | return estimateL1Fee(publicClient, { 30 | chainId, 31 | to, 32 | account, 33 | data, 34 | }); 35 | }, 36 | }); 37 | 38 | const { 39 | data: estimatedL2Gas, 40 | isLoading: isL2GasEstimationLoading, 41 | error: l2GasEstimationError, 42 | } = useEstimateGas({ 43 | chainId, 44 | to, 45 | account, 46 | data, 47 | }); 48 | 49 | const { 50 | data: l2GasPrice, 51 | isLoading: isL2GasPriceLoading, 52 | error: l2GasPriceError, 53 | } = useGasPrice({ 54 | chainId, 55 | }); 56 | 57 | const isLoading = 58 | isL1FeeEstimationLoading || isL2GasEstimationLoading || isL2GasPriceLoading; 59 | 60 | const error = 61 | l1FeeEstimationError || 62 | l2GasEstimationError || 63 | l2GasPriceError || 64 | undefined; 65 | 66 | const areValuesAvailable = 67 | estimatedL1Fee !== undefined && 68 | estimatedL2Gas !== undefined && 69 | l2GasPrice !== undefined; 70 | 71 | if (isLoading || !areValuesAvailable) { 72 | return { 73 | data: undefined, 74 | error: null, 75 | isLoading: true, 76 | }; 77 | } 78 | 79 | if (error) { 80 | return { 81 | data: undefined, 82 | error: new Error('Gas estimation failed'), 83 | isLoading: false, 84 | }; 85 | } 86 | 87 | const totalFee = estimatedL1Fee + estimatedL2Gas * l2GasPrice; 88 | 89 | return { 90 | data: { 91 | totalFee: totalFee, 92 | estimatedL1Fee: estimatedL1Fee, 93 | estimatedL2Gas: estimatedL2Gas, 94 | l2GasPrice: l2GasPrice, 95 | }, 96 | error: null, 97 | isLoading: false, 98 | }; 99 | }; 100 | -------------------------------------------------------------------------------- /packages/cli/src/hooks/useSaveWizardProgress.ts: -------------------------------------------------------------------------------- 1 | import {WizardId} from '@/models/userContext'; 2 | import {useUpdateUserContext} from '@/queries/userContext'; 3 | import {useEffect} from 'react'; 4 | 5 | export const useSaveWizardProgress = ( 6 | wizardId: WizardId, 7 | wizardState: any, 8 | stepIdsToSkip: string[], 9 | ) => { 10 | const {mutate: updateUserContext} = useUpdateUserContext(); 11 | 12 | useEffect(() => { 13 | if (stepIdsToSkip.includes(wizardState.stepId)) { 14 | return; 15 | } 16 | updateUserContext({lastWizardId: wizardId, lastWizardState: wizardState}); 17 | }, [wizardId, wizardState]); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/cli/src/models/userContext.ts: -------------------------------------------------------------------------------- 1 | import {DB} from '@/db/database'; 2 | import {InferInsertModel, InferSelectModel} from 'drizzle-orm'; 3 | import {eq} from 'drizzle-orm'; 4 | import {int, sqliteTable, text} from 'drizzle-orm/sqlite-core'; 5 | import { 6 | transformValueToBigInt, 7 | transformValueToSerializable, 8 | } from '@/util/serialization'; 9 | 10 | const singletonId = 'singleton'; 11 | 12 | export const actionDescriptionByWizardId = { 13 | deployCreate2: 'Deploy a contract', 14 | bridge: 'Bridge assets', 15 | verify: 'Verify a contract', 16 | }; 17 | 18 | export type WizardId = keyof typeof actionDescriptionByWizardId; 19 | 20 | export const userContextTable = sqliteTable('user_context', { 21 | id: text('id') 22 | .primaryKey() 23 | .$default(() => singletonId), 24 | updatedAt: int('updated_at') 25 | .notNull() 26 | .$default(() => new Date().getTime()), 27 | createdAt: int('created_at') 28 | .notNull() 29 | .$default(() => new Date().getTime()), 30 | forgeProjectPath: text('forge_project_path'), 31 | lastWizardId: text('last_wizard_id').$type(), 32 | lastWizardState: text('last_wizard_state', {mode: 'json'}), 33 | }); 34 | 35 | export type UserContext = InferSelectModel; 36 | 37 | export const updateUserContext = async ( 38 | db: DB, 39 | context: Partial>, 40 | ) => { 41 | const contextToSave = {...context}; 42 | 43 | // Transform any BigInt values to serializable format 44 | if (contextToSave.lastWizardState !== undefined) { 45 | contextToSave.lastWizardState = transformValueToSerializable( 46 | contextToSave.lastWizardState, 47 | ); 48 | } 49 | 50 | return await db 51 | .update(userContextTable) 52 | .set({ 53 | ...contextToSave, 54 | updatedAt: new Date().getTime(), 55 | }) 56 | .where(eq(userContextTable.id, singletonId)); 57 | }; 58 | 59 | export const getUserContext = async (db: DB): Promise => { 60 | const results = await db 61 | .select() 62 | .from(userContextTable) 63 | .where(eq(userContextTable.id, singletonId)); 64 | 65 | if (results.length === 0) { 66 | await db.insert(userContextTable).values({}); 67 | return getUserContext(db); 68 | } 69 | 70 | if (results.length > 1) { 71 | throw new Error('Multiple user contexts found'); 72 | } 73 | 74 | const context = results[0]!; 75 | 76 | // Transform serialized BigInt values back to actual BigInt 77 | if (context.lastWizardState) { 78 | context.lastWizardState = transformValueToBigInt(context.lastWizardState); 79 | } 80 | 81 | return context; 82 | }; 83 | -------------------------------------------------------------------------------- /packages/cli/src/queries/chainById.ts: -------------------------------------------------------------------------------- 1 | import {queryClient} from '@/commands/_app'; 2 | import {queryChains} from '@/queries/chains'; 3 | import {chainById} from '@/util/chains/chains'; 4 | import {Chain} from 'viem'; 5 | 6 | const getQueryParams = () => { 7 | return { 8 | queryKey: ['chainById'], 9 | queryFn: async () => { 10 | const chains = await queryChains(); 11 | 12 | return chains.reduce((acc, chain) => { 13 | acc[chain.id] = chain; 14 | 15 | // Also set the source chain (L1) 16 | acc[chain.sourceId] = chainById[chain.sourceId]!; 17 | return acc; 18 | }, {} as Record); 19 | }, 20 | staleTime: Infinity, // For the duration of the CLI session, this is cached 21 | }; 22 | }; 23 | 24 | export const queryMappingChainById = async () => { 25 | return queryClient.fetchQuery(getQueryParams()); 26 | }; 27 | 28 | // TODO: remove this entirely 29 | export const useMappingChainById = () => { 30 | return { 31 | data: chainById, 32 | isLoading: false, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/cli/src/queries/chainByIdentifier.ts: -------------------------------------------------------------------------------- 1 | import {queryClient} from '@/commands/_app'; 2 | import {queryMappingChainById} from '@/queries/chainById'; 3 | import {querySuperchainRegistryChainList} from '@/queries/superchainRegistryChainList'; 4 | import {rollupChainByIdentifier} from '@/util/chains/chains'; 5 | import {Chain} from 'viem'; 6 | 7 | const getQueryParams = () => { 8 | return { 9 | queryKey: ['chainByIdentifier'], 10 | queryFn: async () => { 11 | const chainList = await querySuperchainRegistryChainList(); 12 | const chainById = await queryMappingChainById(); 13 | 14 | return chainList.reduce((acc, config) => { 15 | acc[config.identifier] = chainById[config.chainId]!; 16 | return acc; 17 | }, {} as Record); 18 | }, 19 | }; 20 | }; 21 | 22 | export const queryMappingChainByIdentifier = async () => { 23 | return queryClient.fetchQuery(getQueryParams()); 24 | }; 25 | 26 | // TODO: remove this entirely 27 | export const useMappingChainByIdentifier = () => { 28 | return { 29 | data: rollupChainByIdentifier, 30 | isLoading: false, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/cli/src/queries/chains.ts: -------------------------------------------------------------------------------- 1 | import {querySuperchainRegistryAddresses} from '@/queries/superchainRegistryAddresses'; 2 | import {querySuperchainRegistryChainList} from '@/queries/superchainRegistryChainList'; 3 | import {SuperchainRegistryAddresses} from '@/util/fetchSuperchainRegistryAddresses'; 4 | import {ChainListItem} from '@/util/fetchSuperchainRegistryChainList'; 5 | import {chainConfig} from 'viem/op-stack'; 6 | import { 7 | base, 8 | baseSepolia, 9 | mainnet, 10 | optimism, 11 | optimismSepolia, 12 | sepolia, 13 | } from 'viem/chains'; 14 | import {defineChain} from 'viem'; 15 | import {queryClient} from '@/commands/_app'; 16 | import {chainById} from '@/util/chains/chains'; 17 | import {supersimL1} from '@eth-optimism/viem/chains'; 18 | 19 | const TEMP_overrideBlockExplorerUrlByChainId = { 20 | [baseSepolia.id]: 'https://base-sepolia.blockscout.com/', 21 | [base.id]: 'https://base.blockscout.com/', 22 | [optimismSepolia.id]: 'https://optimism-sepolia.blockscout.com/', 23 | [optimism.id]: 'https://optimism.blockscout.com/', 24 | } as Record; 25 | 26 | export const chainIdByParentChainName = { 27 | mainnet: mainnet.id, 28 | sepolia: sepolia.id, 29 | // 'sepolia-dev-0': sepolia.id, 30 | 'interop-alpha': sepolia.id, 31 | supersim: supersimL1.id, 32 | } as const; 33 | 34 | const toViemChain = ( 35 | chainListItem: ChainListItem, 36 | superchainRegistryAddresses: SuperchainRegistryAddresses, 37 | ) => { 38 | const name = chainListItem.identifier.split('/')[1] as string; 39 | const sourceId = chainIdByParentChainName[chainListItem.parent.chain]; 40 | const chainId = chainListItem.chainId; 41 | 42 | // Not all viem chain definitions have this, so manually overriding it here 43 | const parametersToAdd = { 44 | sourceId: chainIdByParentChainName[chainListItem.parent.chain], 45 | contracts: { 46 | ...chainConfig.contracts, 47 | l1StandardBridge: { 48 | [sourceId]: { 49 | // Should always be defined if we trust Superchain Registry 50 | address: superchainRegistryAddresses[chainId]!.L1StandardBridgeProxy, 51 | }, 52 | }, 53 | }, 54 | blockExplorers: { 55 | default: { 56 | name: 'Blockscout', 57 | url: 58 | TEMP_overrideBlockExplorerUrlByChainId[chainId] ?? 59 | (chainListItem.explorers[0] as string), 60 | }, 61 | }, 62 | }; 63 | 64 | const viemChain = chainById[chainListItem.chainId]; 65 | 66 | if (viemChain) { 67 | return defineChain({ 68 | ...viemChain, 69 | ...parametersToAdd, 70 | }); 71 | } 72 | 73 | return defineChain({ 74 | ...chainConfig, 75 | ...parametersToAdd, 76 | id: chainId, 77 | name, 78 | nativeCurrency: { 79 | name: 'Ether', 80 | symbol: 'ETH', 81 | decimals: 18, 82 | }, 83 | blockExplorers: { 84 | default: { 85 | name: 'Blockscout', 86 | url: chainListItem.explorers[0] as string, 87 | }, 88 | }, 89 | rpcUrls: { 90 | default: { 91 | http: [chainListItem.rpc[0] as string], 92 | }, 93 | }, 94 | multicall: { 95 | address: '0xcA11bde05977b3631167028862bE2a173976CA11', 96 | }, 97 | }); 98 | }; 99 | 100 | // Returns viem chains 101 | const fetchChains = async () => { 102 | const [addresses, chainList] = await Promise.all([ 103 | querySuperchainRegistryAddresses(), 104 | querySuperchainRegistryChainList(), 105 | ]); 106 | 107 | return chainList.map(chainListItem => toViemChain(chainListItem, addresses)); 108 | }; 109 | 110 | const getQueryParams = () => { 111 | return { 112 | queryKey: ['chains'], 113 | queryFn: () => fetchChains(), 114 | staleTime: Infinity, // For the duration of the CLI session, this is cached 115 | }; 116 | }; 117 | 118 | export const queryChains = async () => { 119 | return queryClient.fetchQuery(getQueryParams()); 120 | }; 121 | -------------------------------------------------------------------------------- /packages/cli/src/queries/forgeArtifact.ts: -------------------------------------------------------------------------------- 1 | import {queryClient} from '@/commands/_app'; 2 | import {readForgeArtifact} from '@/util/forge/readForgeArtifact'; 3 | import {QueryOptions, useQuery} from '@tanstack/react-query'; 4 | 5 | export const getForgeArtifactQueryParams = (artifactPath: string) => { 6 | return { 7 | queryKey: ['forgeArtifact', artifactPath], 8 | queryFn: () => readForgeArtifact(artifactPath), 9 | } satisfies QueryOptions; 10 | }; 11 | 12 | export const queryForgeArtifact = async (artifactPath: string) => { 13 | return queryClient.fetchQuery(getForgeArtifactQueryParams(artifactPath)); 14 | }; 15 | 16 | export const useForgeArtifact = (artifactPath: string) => { 17 | return useQuery({ 18 | ...getForgeArtifactQueryParams(artifactPath), 19 | staleTime: Infinity, // For the duration of the CLI session, we want to use the cached artifact 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/cli/src/queries/listFoundryProjectSolidityFiles.ts: -------------------------------------------------------------------------------- 1 | import {queryClient} from '@/commands/_app'; 2 | import {findSolidityFiles} from '@/util/forge/findSolidityFiles'; 3 | import {getSrcDir} from '@/util/forge/foundryProject'; 4 | import {QueryOptions, useQuery} from '@tanstack/react-query'; 5 | 6 | const getQueryParams = (foundryProjectPath: string) => { 7 | return { 8 | queryKey: ['listFoundryProjectSolidityFiles', foundryProjectPath], 9 | queryFn: () => findSolidityFiles(getSrcDir(foundryProjectPath)), 10 | } satisfies QueryOptions; 11 | }; 12 | 13 | export const queryFoundryProjectSolidityFiles = async ( 14 | foundryProjectPath: string, 15 | ) => { 16 | return queryClient.fetchQuery(getQueryParams(foundryProjectPath)); 17 | }; 18 | 19 | export const useFoundryProjectSolidityFiles = (foundryProjectPath: string) => { 20 | return useQuery({ 21 | ...getQueryParams(foundryProjectPath), 22 | staleTime: Infinity, // For the duration of the CLI session, we want to use the cached artifact 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/cli/src/queries/superchainRegistryAddresses.ts: -------------------------------------------------------------------------------- 1 | import {queryClient} from '@/commands/_app'; 2 | import { 3 | fetchSuperchainRegistryAddresses, 4 | SUPERCHAIN_REGISTRY_ADDRESSES_URL, 5 | } from '@/util/fetchSuperchainRegistryAddresses'; 6 | 7 | const getQueryParams = (chainListURL: string) => { 8 | return { 9 | queryKey: ['superchainRegistryAddresses', chainListURL], 10 | queryFn: () => 11 | fetchSuperchainRegistryAddresses(SUPERCHAIN_REGISTRY_ADDRESSES_URL), 12 | }; 13 | }; 14 | 15 | export const querySuperchainRegistryAddresses = async ( 16 | addressesUrl: string = SUPERCHAIN_REGISTRY_ADDRESSES_URL, 17 | ) => { 18 | return queryClient.fetchQuery(getQueryParams(addressesUrl)); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/cli/src/queries/superchainRegistryChainList.ts: -------------------------------------------------------------------------------- 1 | import {queryClient} from '@/commands/_app'; 2 | import { 3 | CHAIN_LIST_URL, 4 | fetchSuperchainRegistryChainList, 5 | } from '@/util/fetchSuperchainRegistryChainList'; 6 | import {useQuery} from '@tanstack/react-query'; 7 | 8 | const getQueryParams = (chainListURL: string) => { 9 | return { 10 | queryKey: ['superchainRegistryChainList', chainListURL], 11 | queryFn: () => fetchSuperchainRegistryChainList(chainListURL), 12 | }; 13 | }; 14 | 15 | export const querySuperchainRegistryChainList = async ( 16 | chainListURL: string = CHAIN_LIST_URL, 17 | ) => { 18 | return queryClient.fetchQuery(getQueryParams(chainListURL)); 19 | }; 20 | 21 | export const useSuperchainRegistryChainList = () => { 22 | return useQuery(getQueryParams(CHAIN_LIST_URL)); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/cli/src/queries/userContext.ts: -------------------------------------------------------------------------------- 1 | import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; 2 | import {useDb} from '@/db/dbContext'; 3 | import { 4 | getUserContext, 5 | UserContext, 6 | updateUserContext, 7 | } from '@/models/userContext'; 8 | 9 | export const useUserContext = () => { 10 | const db = useDb(); 11 | return useQuery({ 12 | queryKey: ['userContext'], 13 | queryFn: () => getUserContext(db), 14 | }); 15 | }; 16 | 17 | export const useUpdateUserContext = () => { 18 | const queryClient = useQueryClient(); 19 | const db = useDb(); 20 | 21 | return useMutation({ 22 | mutationFn: (context: Partial) => 23 | updateUserContext(db, context), 24 | onSuccess: () => { 25 | queryClient.invalidateQueries({queryKey: ['userContext']}); 26 | }, 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/cli/src/server/api.ts: -------------------------------------------------------------------------------- 1 | import {Hono} from 'hono'; 2 | import {cors} from 'hono/cors'; 3 | import {validator} from 'hono/validator'; 4 | import { 5 | onTaskSuccess, 6 | useTransactionTaskStore, 7 | } from '@/stores/transactionTaskStore'; 8 | import {zodHash} from '@/util/schemas'; 9 | import {z} from 'zod'; 10 | import {chainById} from '@/util/chains/chains'; 11 | 12 | export const api = new Hono(); 13 | 14 | // TODO: consider just supporting the eth_sendTransaction RPC method - will be easier for integration 15 | // At that point, this API will just be a "remote" wallet that proxies out to other wallets 16 | // Only issue is usually eth_sendTransaction is more of a "synchronous" method than an async one 17 | 18 | // TODO: low priority - consider using websockets 19 | 20 | if (process.env['SUP_DEV_MODE'] === 'true') { 21 | api.use(cors()); 22 | } 23 | 24 | api.get('/healthz', async c => { 25 | return c.json({ 26 | message: 'OK', 27 | }); 28 | }); 29 | 30 | api.post('/getMappingChainById', async c => { 31 | return c.json({ 32 | chainById, 33 | }); 34 | }); 35 | 36 | api.post('/listTransactionTasks', async c => { 37 | const {taskEntryById} = useTransactionTaskStore.getState(); 38 | 39 | return c.json({ 40 | transactionTasks: Object.values(taskEntryById).sort( 41 | (a, b) => b.createdAt.getTime() - a.createdAt.getTime(), 42 | ), 43 | }); 44 | }); 45 | 46 | api.post( 47 | '/completeTransactionTask', 48 | validator('json', (value, c) => { 49 | const parsed = z 50 | .object({ 51 | id: z.string(), 52 | hash: zodHash, 53 | }) 54 | .safeParse(value); 55 | if (!parsed.success) { 56 | return c.text('Invalid request', 400); 57 | } 58 | 59 | if (!useTransactionTaskStore.getState().taskEntryById[parsed.data.id]) { 60 | return c.text('Transaction task not found', 400); 61 | } 62 | 63 | return parsed.data; 64 | }), 65 | async c => { 66 | const {id, hash} = c.req.valid('json'); 67 | 68 | // TODO: fetch the transaction receipt and check that the hash corresponds to the task (check to, data, value, etc) 69 | onTaskSuccess(id, hash); 70 | return c.json({ 71 | signatureRequest: {}, 72 | }); 73 | }, 74 | ); 75 | -------------------------------------------------------------------------------- /packages/cli/src/server/startServer.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import {fileURLToPath} from 'url'; 3 | import {serveStatic} from '@hono/node-server/serve-static'; 4 | import {serve} from '@hono/node-server'; 5 | import {Hono} from 'hono'; 6 | import fs from 'fs/promises'; 7 | import {api} from '@/server/api'; 8 | import type {Server} from 'node:http'; 9 | import {Socket} from 'net'; 10 | 11 | // TODO: fix this terribly hacky thing 12 | // Pretty hacky way to get the frontend dist path 13 | // Fix when we make the signer-frontend a published package 14 | export async function startServer() { 15 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 16 | const isRunningFromDist = __dirname.includes('dist'); 17 | 18 | const absoluteFrontendDistPath = path.resolve( 19 | __dirname, 20 | isRunningFromDist ? '../signer-frontend' : '../../../signer-frontend/dist', 21 | ); 22 | 23 | const relativeFrontendDistPath = path.relative( 24 | process.cwd(), 25 | absoluteFrontendDistPath, 26 | ); 27 | const indexHtmlPath = path.join(relativeFrontendDistPath, 'index.html'); 28 | 29 | // Read the index.html file asynchronously once when starting the server 30 | const indexHtml = await fs.readFile(indexHtmlPath, 'utf-8'); 31 | 32 | const app = new Hono(); 33 | 34 | // Add logging middleware 35 | // app.use('*', async (c, next) => { 36 | // console.log(`[${new Date().toISOString()}] ${c.req.method} ${c.req.url}`); 37 | // await next(); 38 | // }); 39 | 40 | // API ROUTES 41 | app.route('/api', api); 42 | 43 | // FRONTEND ROUTES 44 | 45 | // Serve static assets first 46 | app.get('/assets/*', serveStatic({root: relativeFrontendDistPath})); 47 | 48 | app.get('/favicon.ico', serveStatic({root: relativeFrontendDistPath})); 49 | 50 | // For base route, serve index.html. Fix when there are more client side routes 51 | app.get('/', c => { 52 | return c.html(indexHtml); 53 | }); 54 | 55 | app.onError((err, c) => { 56 | console.error('Server error:', err); 57 | return c.json({message: 'Internal Server Error'}, 500); 58 | }); 59 | 60 | const server = serve({fetch: app.fetch, port: 3000}) as Server; 61 | const connections = new Set(); 62 | server.on('connection', conn => { 63 | connections.add(conn); 64 | conn.on('close', () => connections.delete(conn)); 65 | }); 66 | 67 | const originalClose = server.close.bind(server); 68 | server.close = (callback?: (err?: Error) => void) => { 69 | // Explicitly kill all connections on shutdown 70 | for (const conn of connections) { 71 | conn.destroy(); 72 | } 73 | 74 | return originalClose(callback); 75 | }; 76 | 77 | return server; 78 | } 79 | -------------------------------------------------------------------------------- /packages/cli/src/stores/operationStore.ts: -------------------------------------------------------------------------------- 1 | import {create} from 'zustand'; 2 | 3 | type State = 4 | | {status: 'pending'; data: undefined; error: null} 5 | | {status: 'success'; data: T; error: null} 6 | | {status: 'error'; data: undefined; error: Error} 7 | | {status: 'idle'; data: undefined; error: null}; 8 | 9 | type Operation = { 10 | key: unknown[]; 11 | fn: () => Promise; 12 | }; 13 | 14 | type OperationStore = { 15 | statusById: Record>; 16 | onIdle: (id: unknown[]) => void; 17 | onPending: (id: unknown[]) => void; 18 | onSuccess: (id: unknown[], data: any) => void; 19 | onError: (id: unknown[], error: Error) => void; 20 | }; 21 | 22 | const createKey = (key: unknown[]) => key.join('-'); 23 | 24 | export const useOperationStore = create()(set => ({ 25 | statusById: {}, 26 | onPending: (id: unknown[]) => { 27 | set(state => ({ 28 | statusById: { 29 | ...state.statusById, 30 | [createKey(id)]: {status: 'pending', data: undefined, error: null}, 31 | }, 32 | })); 33 | }, 34 | onSuccess: (id: unknown[], data: any) => { 35 | set(state => ({ 36 | statusById: { 37 | ...state.statusById, 38 | [createKey(id)]: {status: 'success', data, error: null}, 39 | }, 40 | })); 41 | }, 42 | onError: (id: unknown[], error: Error) => { 43 | set(state => ({ 44 | statusById: { 45 | ...state.statusById, 46 | [createKey(id)]: {status: 'error', data: undefined, error}, 47 | }, 48 | })); 49 | }, 50 | onIdle: (id: unknown[]) => { 51 | set(state => ({ 52 | statusById: { 53 | ...state.statusById, 54 | [createKey(id)]: {status: 'idle', data: undefined, error: null}, 55 | }, 56 | })); 57 | }, 58 | })); 59 | 60 | export const runOperation = async (operation: Operation) => { 61 | useOperationStore.getState().onPending(operation.key); 62 | 63 | try { 64 | const data = await operation.fn(); 65 | useOperationStore.getState().onSuccess(operation.key, data); 66 | return data; 67 | } catch (error) { 68 | useOperationStore.getState().onError(operation.key, error as Error); 69 | throw error; 70 | } 71 | }; 72 | 73 | export const runOperationsMany = async []>( 74 | operations: [...T], 75 | ): Promise<{[K in keyof T]: T[K] extends Operation ? R : never}> => { 76 | const results = await Promise.all( 77 | operations.map(operation => runOperation(operation)), 78 | ); 79 | return results as { 80 | [K in keyof T]: T[K] extends Operation ? R : never; 81 | }; 82 | }; 83 | 84 | const idleState = {status: 'idle', data: undefined, error: null} as const; 85 | 86 | export const useOperation = (operation: Operation): State => { 87 | const status = useOperationStore( 88 | state => state.statusById[createKey(operation.key)], 89 | ); 90 | 91 | return status ?? idleState; 92 | }; 93 | 94 | export const useOperationWithKey = (key: unknown[]): State => { 95 | const status = useOperationStore(state => state.statusById[createKey(key)]); 96 | 97 | return status ?? idleState; 98 | }; 99 | 100 | export const useManyOperations = []>( 101 | operations: [...T], 102 | ): {[K in keyof T]: State ? R : never>} => { 103 | const statuses = useOperationStore(state => 104 | operations.map(operation => state.statusById[createKey(operation.key)]), 105 | ); 106 | 107 | return statuses.map(status => status ?? idleState) as any; 108 | }; 109 | -------------------------------------------------------------------------------- /packages/cli/src/stores/transactionTaskStore.ts: -------------------------------------------------------------------------------- 1 | import {Hash} from 'viem'; 2 | import {create} from 'zustand'; 3 | import {immer} from 'zustand/middleware/immer'; 4 | 5 | import {createTransactionTaskId, TransactionTask} from '@/util/transactionTask'; 6 | 7 | export type TransactionTaskEntry = { 8 | id: string; 9 | request: TransactionTask; 10 | result?: TaskResult; 11 | createdAt: Date; 12 | }; 13 | 14 | type TaskResult = 15 | | { 16 | type: 'success'; 17 | hash: Hash; 18 | } 19 | | { 20 | type: 'error'; 21 | error: Error; 22 | }; 23 | 24 | type TransactionTaskStore = { 25 | taskEntryById: Record; 26 | 27 | createTask: (task: TransactionTask) => void; 28 | completeTask: (id: string, result: TaskResult) => void; 29 | }; 30 | 31 | export const useTransactionTaskStore = create( 32 | immer(set => ({ 33 | taskEntryById: {}, 34 | createTask: (task: TransactionTask) => { 35 | set(state => { 36 | const id = createTransactionTaskId(task); 37 | state.taskEntryById[id] = { 38 | id, 39 | request: task, 40 | createdAt: new Date(), 41 | }; 42 | }); 43 | }, 44 | completeTask: (id: string, result: TaskResult) => { 45 | set(state => { 46 | if (state.taskEntryById[id]) { 47 | state.taskEntryById[id].result = result; 48 | } 49 | }); 50 | }, 51 | })), 52 | ); 53 | 54 | const taskListener: Record void> = {}; 55 | 56 | const alertListener = (id: string, result: TaskResult) => { 57 | const listener = taskListener[id]; 58 | delete taskListener[id]; 59 | 60 | if (listener) { 61 | listener(result); 62 | } 63 | }; 64 | 65 | export const requestTransactionTask = async ( 66 | task: TransactionTask, 67 | ): Promise => { 68 | return new Promise((resolve, reject) => { 69 | const id = createTransactionTaskId(task); 70 | useTransactionTaskStore.getState().createTask(task); 71 | 72 | taskListener[id] = (result: TaskResult) => { 73 | if (result.type === 'success') { 74 | resolve(result.hash); 75 | } else { 76 | reject(result.error); 77 | } 78 | }; 79 | }); 80 | }; 81 | 82 | export const onNewTask = (task: TransactionTask) => { 83 | useTransactionTaskStore.getState().createTask(task); 84 | }; 85 | 86 | export const onTaskSuccess = (id: string, hash: Hash) => { 87 | const result = {type: 'success', hash} as const; 88 | useTransactionTaskStore.getState().completeTask(id, result); 89 | 90 | alertListener(id, result); 91 | }; 92 | 93 | export const onTaskError = (id: string, error: Error) => { 94 | const result = {type: 'error', error} as const; 95 | 96 | useTransactionTaskStore.getState().completeTask(id, result); 97 | 98 | alertListener(id, result); 99 | }; 100 | -------------------------------------------------------------------------------- /packages/cli/src/util/AsyncQueue.ts: -------------------------------------------------------------------------------- 1 | export interface AsyncQueueItem { 2 | item: T; 3 | resolve: (value: R) => void; 4 | reject: (error: any) => void; 5 | } 6 | 7 | // Simple queue that processes items sequentially 8 | export class AsyncQueue { 9 | private queue: AsyncQueueItem[] = []; 10 | private processing = false; 11 | private processFn: (item: T) => Promise; 12 | 13 | constructor(processFn: (item: T) => Promise) { 14 | this.processFn = processFn; 15 | } 16 | 17 | public enqueue(item: T): Promise { 18 | return new Promise((resolve, reject) => { 19 | this.queue.push({item, resolve, reject}); 20 | this.processQueue(); 21 | }); 22 | } 23 | 24 | private async processQueue(): Promise { 25 | if (this.processing) { 26 | return; 27 | } 28 | this.processing = true; 29 | 30 | try { 31 | while (this.queue.length > 0) { 32 | const queueItem = this.queue.shift(); 33 | if (!queueItem) continue; 34 | try { 35 | const result = await this.processFn(queueItem.item); 36 | queueItem.resolve(result); 37 | } catch (error) { 38 | queueItem.reject(error); 39 | } 40 | } 41 | } finally { 42 | this.processing = false; 43 | if (this.queue.length > 0) { 44 | this.processQueue(); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/cli/src/util/TxSender.ts: -------------------------------------------------------------------------------- 1 | import {AsyncQueue} from '@/util/AsyncQueue'; 2 | import {chainById} from '@/util/chains/chains'; 3 | import {TransactionTask} from '@/util/transactionTask'; 4 | import {sendTransaction, waitForTransactionReceipt} from '@wagmi/core'; 5 | import { 6 | createWalletClient, 7 | Hash, 8 | http, 9 | PrivateKeyAccount, 10 | zeroAddress, 11 | } from 'viem'; 12 | import {Config} from 'wagmi'; 13 | 14 | type WalletRpcUrlFactory = (chainId: number) => string; 15 | 16 | type TxSenderTx = TransactionTask; 17 | 18 | export interface TxSender { 19 | sendTx: (tx: TxSenderTx) => Promise; 20 | } 21 | 22 | export const createTxSenderFromPrivateKeyAccount = ( 23 | config: Config, 24 | account: PrivateKeyAccount, 25 | ): TxSender => { 26 | const queueByChainId = {} as Record>; 27 | 28 | config.chains.forEach(chain => { 29 | queueByChainId[chain.id] = new AsyncQueue(async tx => { 30 | const hash = await sendTransaction(config, { 31 | chainId: tx.chainId, 32 | to: tx.to, 33 | data: tx.data, 34 | account, 35 | }); 36 | // Prevent replacement tx. 37 | await waitForTransactionReceipt(config, { 38 | hash, 39 | chainId: tx.chainId, 40 | pollingInterval: 1000, 41 | }); 42 | 43 | return hash; 44 | }); 45 | }); 46 | 47 | return { 48 | sendTx: async tx => { 49 | if (!queueByChainId[tx.chainId]) { 50 | throw new Error(`Chain tx queue for ${tx.chainId} not found`); 51 | } 52 | return await queueByChainId[tx.chainId]!.enqueue(tx); 53 | }, 54 | }; 55 | }; 56 | 57 | export const createTxSenderFromCustomWalletRpc = ( 58 | getRpcUrl: WalletRpcUrlFactory, 59 | ): TxSender => { 60 | return { 61 | sendTx: async tx => { 62 | const chain = chainById[tx.chainId]; 63 | if (!chain) { 64 | throw new Error(`Chain not found for ${tx.chainId}`); 65 | } 66 | const walletClient = createWalletClient({ 67 | transport: http(getRpcUrl(tx.chainId)), 68 | }); 69 | return await walletClient.sendTransaction({ 70 | to: tx.to, 71 | data: tx.data, 72 | account: zeroAddress, // will be ignored 73 | chain: chain, 74 | }); 75 | }, 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /packages/cli/src/util/abi.ts: -------------------------------------------------------------------------------- 1 | import {AbiConstructor} from 'abitype'; 2 | import { 3 | Abi, 4 | AbiFunction, 5 | AbiParameter, 6 | encodeAbiParameters, 7 | Hex, 8 | stringToBytes, 9 | } from 'viem'; 10 | 11 | export function preparedParamForEncoding(input: AbiParameter, value: string) { 12 | const {type} = input; 13 | 14 | if (type.startsWith('uint') || type.startsWith('int')) { 15 | return BigInt(value); 16 | } 17 | 18 | if (type === 'boolean') { 19 | return value === 'true'; 20 | } 21 | 22 | if (type.startsWith('bytes')) { 23 | return stringToBytes(value); 24 | } 25 | 26 | return value; 27 | } 28 | 29 | export function getConstructorAbi(abi: Abi): AbiConstructor | undefined { 30 | return abi.find(abi => abi.type === 'constructor'); 31 | } 32 | 33 | export function getEncodedConstructorArgs( 34 | abi: Abi, 35 | args: string[] | undefined, 36 | ) { 37 | let encodedConstructorArgs: Hex | undefined; 38 | 39 | const constructorAbi = abi.find(abi => abi.type === 'constructor'); 40 | if (args?.length && constructorAbi) { 41 | const constructorInputTypes = constructorAbi.inputs.map(input => ({ 42 | type: input.type, 43 | name: input.name, 44 | })); 45 | 46 | if (args.length !== constructorInputTypes.length) { 47 | throw new Error( 48 | `Constructor input types length mismatch: ${args.length} !== ${constructorInputTypes.length}`, 49 | ); 50 | } 51 | 52 | if (args.length && args.length === constructorInputTypes.length) { 53 | const preparedArgs = args.map((arg, i) => 54 | preparedParamForEncoding(constructorInputTypes[i] as AbiParameter, arg), 55 | ); 56 | encodedConstructorArgs = encodeAbiParameters( 57 | constructorInputTypes, 58 | preparedArgs, 59 | ); 60 | } 61 | } 62 | 63 | return encodedConstructorArgs; 64 | } 65 | 66 | export function getEncodedInitializationArgs( 67 | abi: Abi, 68 | args: string[] | undefined, 69 | ) { 70 | let encodedInitializationArgs: Hex | undefined; 71 | 72 | const initializationAbi = abi.find( 73 | abi => abi.type === 'function' && abi.name === 'initialize', 74 | ) as AbiFunction | undefined; 75 | if (initializationAbi) { 76 | const initializationInputTypes = initializationAbi.inputs.map(input => ({ 77 | type: input.type, 78 | name: input.name, 79 | })); 80 | 81 | if (args?.length && args?.length == initializationInputTypes.length) { 82 | const preparedArgs = args.map((arg, i) => 83 | preparedParamForEncoding( 84 | initializationInputTypes[i] as AbiParameter, 85 | arg, 86 | ), 87 | ); 88 | encodedInitializationArgs = encodeAbiParameters( 89 | initializationInputTypes, 90 | preparedArgs, 91 | ); 92 | } 93 | } 94 | 95 | return encodedInitializationArgs; 96 | } 97 | -------------------------------------------------------------------------------- /packages/cli/src/util/blockExplorer.ts: -------------------------------------------------------------------------------- 1 | import {Address, Chain, Hash} from 'viem'; 2 | 3 | const getBaseBlockExplorerUrl = (chain: Chain) => { 4 | const result = 5 | chain.blockExplorers?.['blockscout']?.url || 6 | chain.blockExplorers?.default?.url || 7 | ''; 8 | 9 | // trim / at the end 10 | return result.endsWith('/') ? result.slice(0, -1) : result; 11 | }; 12 | 13 | export const getBlockExplorerAddressLink = (chain: Chain, address: Address) => { 14 | return `${getBaseBlockExplorerUrl(chain)}/address/${address}`; 15 | }; 16 | 17 | export const getBlockExplorerTxHashLink = (chain: Chain, txHash: Hash) => { 18 | return `${getBaseBlockExplorerUrl(chain)}/tx/${txHash}`; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/cli/src/util/chains/chainIdentifier.ts: -------------------------------------------------------------------------------- 1 | import {Chain, mainnet, sepolia} from 'viem/chains'; 2 | import {supersimL1} from '@eth-optimism/viem/chains'; 3 | import {superchainRegistryChainList} from '@/util/chains/superchainRegistryChainList'; 4 | 5 | const sourceChains = [ 6 | { 7 | identifier: 'mainnet', 8 | chain: mainnet, 9 | }, 10 | { 11 | identifier: 'sepolia', 12 | chain: sepolia, 13 | }, 14 | { 15 | identifier: 'supersim', 16 | chain: supersimL1, 17 | }, 18 | { 19 | identifier: 'interop-alpha', 20 | chain: sepolia, 21 | }, 22 | ]; 23 | 24 | export const sourceIdentifierByChainId = sourceChains.reduce((acc, chain) => { 25 | acc[chain.chain.id] = chain.identifier; 26 | return acc; 27 | }, {} as Record); 28 | 29 | export const sourceChainByIdentifier = sourceChains.reduce((acc, chain) => { 30 | acc[chain.identifier] = chain.chain; 31 | return acc; 32 | }, {} as Record); 33 | 34 | // TODO: this is error prone, update @eth-optimism/viem to export a mapping from name to identifier 35 | const supersimIdentifierByChainId: Record = { 36 | 901: 'supersim/supersiml2a', 37 | 902: 'supersim/supersiml2b', 38 | 903: 'supersim/supersiml2c', 39 | 904: 'supersim/supersiml2d', 40 | 905: 'supersim/supersiml2e', 41 | }; 42 | 43 | const interopAlphaIdentifierByChainId: Record = { 44 | 420120000: 'interop-alpha/interop-alpha-0', 45 | 420120001: 'interop-alpha/interop-alpha-1', 46 | }; 47 | 48 | // TODO: this is error prone (and becomes outdated with chainlist updates), update @eth-optimism/viem to export a mapping from name to identifier 49 | const superchainRegistryIdentifierByChainId = 50 | superchainRegistryChainList.reduce((acc, chainListItem) => { 51 | acc[chainListItem.chainId] = chainListItem.identifier; 52 | return acc; 53 | }, {} as Record); 54 | 55 | const identifierByChainId = { 56 | ...superchainRegistryIdentifierByChainId, 57 | ...supersimIdentifierByChainId, 58 | ...interopAlphaIdentifierByChainId, 59 | }; 60 | 61 | export const rollupChainToIdentifier = (chain: Chain) => { 62 | return identifierByChainId[chain.id]!; 63 | }; 64 | -------------------------------------------------------------------------------- /packages/cli/src/util/chains/chains.ts: -------------------------------------------------------------------------------- 1 | import {rollupChainToIdentifier} from '@/util/chains/chainIdentifier'; 2 | import {networks} from '@/util/chains/networks'; 3 | 4 | import {base, baseSepolia, Chain, optimism, optimismSepolia} from 'viem/chains'; 5 | 6 | // TODO: move this override logic into @eth-optimism/viem/chains 7 | const TEMP_overrideBlockExplorerUrlByChainId = { 8 | [baseSepolia.id]: 'https://base-sepolia.blockscout.com/', 9 | [base.id]: 'https://base.blockscout.com/', 10 | [optimismSepolia.id]: 'https://optimism-sepolia.blockscout.com/', 11 | [optimism.id]: 'https://optimism.blockscout.com/', 12 | } as Record; 13 | 14 | export const sourceChains = networks.map(network => network.sourceChain); 15 | 16 | export const rollupChains = networks 17 | .flatMap(network => network.chains as Chain[]) 18 | .map(chain => { 19 | let newChain = { 20 | ...chain, 21 | }; 22 | if (TEMP_overrideBlockExplorerUrlByChainId[chain.id]) { 23 | newChain = { 24 | ...newChain, 25 | blockExplorers: { 26 | default: { 27 | name: 'Blockscout', 28 | url: TEMP_overrideBlockExplorerUrlByChainId[chain.id]!, 29 | }, 30 | }, 31 | } as const; 32 | } 33 | 34 | return newChain; 35 | }); 36 | 37 | export const chains = [...sourceChains, ...rollupChains] as const; 38 | 39 | type RollupChains = typeof rollupChains; 40 | 41 | type Chains = typeof chains; 42 | 43 | export const chainById = chains.reduce((acc, chain) => { 44 | acc[chain.id] = chain; 45 | return acc; 46 | }, {} as Record); 47 | 48 | export const rollupChainByIdentifier = rollupChains.reduce((acc, chain) => { 49 | acc[rollupChainToIdentifier(chain)] = chain; 50 | return acc; 51 | }, {} as Record); 52 | -------------------------------------------------------------------------------- /packages/cli/src/util/chains/networks.ts: -------------------------------------------------------------------------------- 1 | import { 2 | interopAlphaChains, 3 | mainnetChains, 4 | sepoliaChains, 5 | supersimChains, 6 | supersimL1, 7 | } from '@eth-optimism/viem/chains'; 8 | import {Chain, mainnet, sepolia} from 'viem/chains'; 9 | 10 | type Network = { 11 | name: string; 12 | sourceChain: Chain; 13 | chains: Chain[]; 14 | }; 15 | 16 | export const mainnetNetwork = { 17 | name: 'mainnet', 18 | sourceChain: mainnet, 19 | chains: mainnetChains, 20 | } as const satisfies Network; 21 | 22 | export const sepoliaNetwork = { 23 | name: 'sepolia', 24 | sourceChain: sepolia, 25 | chains: sepoliaChains, 26 | } as const satisfies Network; 27 | 28 | export const supersimNetwork = { 29 | name: 'supersim', 30 | sourceChain: supersimL1, 31 | chains: supersimChains, 32 | } as const satisfies Network; 33 | 34 | export const interopAlphaNetwork = { 35 | name: 'interop-alpha', 36 | sourceChain: sepolia, 37 | chains: interopAlphaChains, 38 | } as const satisfies Network; 39 | 40 | export const networks = [ 41 | mainnetNetwork, 42 | sepoliaNetwork, 43 | interopAlphaNetwork, 44 | supersimNetwork, 45 | ]; 46 | 47 | export const networkByName = networks.reduce((acc, network) => { 48 | acc[network.name] = network; 49 | return acc; 50 | }, {} as Record); 51 | -------------------------------------------------------------------------------- /packages/cli/src/util/config.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod'; 2 | import {parse as parseTOML} from 'smol-toml'; 3 | import fs from 'fs'; 4 | 5 | export const zodSuperConfig = z.object({ 6 | rpc_endpoints: z.record(z.string(), z.string()).optional(), 7 | verification_endpoints: z.record(z.string(), z.string()).optional(), 8 | creation_params: z 9 | .array( 10 | z.object({ 11 | salt: z.string(), 12 | chains: z.array(z.string()), 13 | network: z.string(), 14 | verify: z.boolean(), 15 | constructor_args: z.array(z.any()).optional(), 16 | }), 17 | ) 18 | .optional(), 19 | }); 20 | 21 | export function parseSuperConfigFromTOML(pathToConfig: string) { 22 | const toml = fs.readFileSync(pathToConfig, {encoding: 'utf-8'}); 23 | 24 | if (!toml) { 25 | throw new Error('Config file is empty'); 26 | } 27 | 28 | const parsed = parseTOML(toml); 29 | const config = parsed['supercli']; 30 | if (!config) { 31 | throw new Error('[supercli] config not found in toml file.'); 32 | } 33 | 34 | const parsedConfig = zodSuperConfig.safeParse(config); 35 | if (parsedConfig.success === false) { 36 | throw new Error('Config file is invalid'); 37 | } 38 | 39 | return parsedConfig.data; 40 | } 41 | -------------------------------------------------------------------------------- /packages/cli/src/util/createx/computeCreate2Address.ts: -------------------------------------------------------------------------------- 1 | import {Address, concatHex, Hex, keccak256} from 'viem'; 2 | import {CREATEX_ADDRESS} from '@/util/createx/constants'; 3 | 4 | // Replicated logic from 5 | // https://github.com/pcaversaccio/createx/blob/ab60cc031b38111a5fad9358d018240dfa78cb8e/src/CreateX.sol#L575 6 | export function computeCreate2Address({ 7 | guardedSalt, 8 | initCodeHash, 9 | deployer = CREATEX_ADDRESS, 10 | }: { 11 | guardedSalt: Hex; 12 | initCodeHash: Hex; 13 | deployer?: Address; 14 | }): Address { 15 | const packed = concatHex([ 16 | '0xff', // Single byte prefix 17 | deployer, // 20 bytes deployer address 18 | guardedSalt, // 32 bytes salt 19 | initCodeHash, // 32 bytes init code hash 20 | ]); 21 | 22 | // Take last 20 bytes of hash to get the address 23 | const hash = keccak256(packed); 24 | return `0x${hash.slice(26)}` as Address; 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/src/util/createx/constants.ts: -------------------------------------------------------------------------------- 1 | import {parseAbi} from 'viem'; 2 | 3 | export type RedeployProtectionFlag = boolean; 4 | 5 | export const createXABI = parseAbi([ 6 | 'error FailedContractCreation(address emitter)', 7 | 'error FailedContractInitialisation(address emitter, bytes revertData)', 8 | 'error FailedEtherTransfer(address emitter, bytes revertData)', 9 | 'error InvalidNonceValue(address emitter)', 10 | 'error InvalidSalt(address emitter)', 11 | 'event ContractCreation(address indexed newContract, bytes32 indexed salt)', 12 | 'event ContractCreation(address indexed newContract)', 13 | 'event Create3ProxyContractCreation(address indexed newContract, bytes32 indexed salt)', 14 | 'function computeCreate2Address(bytes32 salt, bytes32 initCodeHash) view returns (address computedAddress)', 15 | 'function computeCreate2Address(bytes32 salt, bytes32 initCodeHash, address deployer) pure returns (address computedAddress)', 16 | 'function computeCreate3Address(bytes32 salt, address deployer) pure returns (address computedAddress)', 17 | 'function computeCreate3Address(bytes32 salt) view returns (address computedAddress)', 18 | 'function computeCreateAddress(uint256 nonce) view returns (address computedAddress)', 19 | 'function computeCreateAddress(address deployer, uint256 nonce) view returns (address computedAddress)', 20 | 'function deployCreate(bytes initCode) payable returns (address newContract)', 21 | 'function deployCreate2(bytes32 salt, bytes initCode) payable returns (address newContract)', 22 | 'function deployCreate2(bytes initCode) payable returns (address newContract)', 23 | 'function deployCreate2AndInit(bytes32 salt, bytes initCode, bytes data, (uint256 constructorAmount, uint256 initCallAmount) values, address refundAddress) payable returns (address newContract)', 24 | 'function deployCreate2AndInit(bytes initCode, bytes data, (uint256 constructorAmount, uint256 initCallAmount) values) payable returns (address newContract)', 25 | 'function deployCreate2AndInit(bytes initCode, bytes data, (uint256 constructorAmount, uint256 initCallAmount) values, address refundAddress) payable returns (address newContract)', 26 | 'function deployCreate2AndInit(bytes32 salt, bytes initCode, bytes data, (uint256 constructorAmount, uint256 initCallAmount) values) payable returns (address newContract)', 27 | 'function deployCreate2Clone(bytes32 salt, address implementation, bytes data) payable returns (address proxy)', 28 | 'function deployCreate2Clone(address implementation, bytes data) payable returns (address proxy)', 29 | 'function deployCreate3(bytes initCode) payable returns (address newContract)', 30 | 'function deployCreate3(bytes32 salt, bytes initCode) payable returns (address newContract)', 31 | 'function deployCreate3AndInit(bytes32 salt, bytes initCode, bytes data, (uint256 constructorAmount, uint256 initCallAmount) values) payable returns (address newContract)', 32 | 'function deployCreate3AndInit(bytes initCode, bytes data, (uint256 constructorAmount, uint256 initCallAmount) values) payable returns (address newContract)', 33 | 'function deployCreate3AndInit(bytes32 salt, bytes initCode, bytes data, (uint256 constructorAmount, uint256 initCallAmount) values, address refundAddress) payable returns (address newContract)', 34 | 'function deployCreate3AndInit(bytes initCode, bytes data, (uint256 constructorAmount, uint256 initCallAmount) values, address refundAddress) payable returns (address newContract)', 35 | 'function deployCreateAndInit(bytes initCode, bytes data, (uint256 constructorAmount, uint256 initCallAmount) values) payable returns (address newContract)', 36 | 'function deployCreateAndInit(bytes initCode, bytes data, (uint256 constructorAmount, uint256 initCallAmount) values, address refundAddress) payable returns (address newContract)', 37 | 'function deployCreateClone(address implementation, bytes data) payable returns (address proxy)', 38 | ]); 39 | 40 | export const CREATEX_ADDRESS = '0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed'; 41 | 42 | export const INVALID_SALT = 0x13b3a2a1; 43 | -------------------------------------------------------------------------------- /packages/cli/src/util/createx/deployCreate2Contract.ts: -------------------------------------------------------------------------------- 1 | import {Address, Hash, Hex, WalletClient} from 'viem'; 2 | import {estimateContractGas, simulateContract} from 'viem/actions'; 3 | import {CREATEX_ADDRESS, createXABI} from './constants.js'; 4 | 5 | export type DeployCreate2ContractParameters = { 6 | client: WalletClient; 7 | salt: Hex; 8 | initCode: Hex; 9 | createXAddress?: Address; 10 | }; 11 | 12 | export const deployCreate2Contract = async ({ 13 | client, 14 | salt, 15 | initCode, 16 | createXAddress, 17 | }: DeployCreate2ContractParameters): Promise => { 18 | const hash = await client.writeContract({ 19 | abi: createXABI, 20 | chain: client.chain, 21 | account: client.account!, 22 | address: createXAddress ?? CREATEX_ADDRESS, 23 | functionName: 'deployCreate2', 24 | args: [salt, initCode], 25 | }); 26 | 27 | return hash; 28 | }; 29 | 30 | export const simulateDeployCreate2Contract = async ({ 31 | client, 32 | salt, 33 | initCode, 34 | createXAddress, 35 | }: DeployCreate2ContractParameters): Promise
=> { 36 | const result = await simulateContract(client, { 37 | abi: createXABI, 38 | account: client.account?.address as Address, 39 | address: createXAddress ?? CREATEX_ADDRESS, 40 | functionName: 'deployCreate2', 41 | args: [salt, initCode], 42 | }); 43 | 44 | return result.result as Address; 45 | }; 46 | 47 | export const estimateDeployCreate2Contract = async ({ 48 | client, 49 | salt, 50 | initCode, 51 | createXAddress, 52 | }: DeployCreate2ContractParameters): Promise => { 53 | return await estimateContractGas(client, { 54 | abi: createXABI, 55 | address: createXAddress ?? CREATEX_ADDRESS, 56 | functionName: 'deployCreate2', 57 | args: [salt, initCode], 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /packages/cli/src/util/createx/salt.ts: -------------------------------------------------------------------------------- 1 | import { 2 | keccak256, 3 | encodeAbiParameters, 4 | numberToHex, 5 | Hex, 6 | Address, 7 | zeroAddress, 8 | concat, 9 | toHex, 10 | slice, 11 | pad, 12 | } from 'viem'; 13 | 14 | // https://github.com/pcaversaccio/createx/blob/1d93d19f8ec5edd41aa9a1693e8bb13d9da89f62/src/CreateX.sol#L874 15 | 16 | /** 17 | * @dev Implements different safeguarding mechanisms depending on the encoded values in the salt 18 | * (`||` stands for byte-wise concatenation): 19 | * => salt (32 bytes) = 0xbebebebebebebebebebebebebebebebebebebebe||ff||1212121212121212121212 20 | * - The first 20 bytes (i.e. `bebebebebebebebebebebebebebebebebebebebe`) may be used to 21 | * implement a permissioned deploy protection by setting them equal to `msg.sender`, 22 | * - The 21st byte (i.e. `ff`) may be used to implement a cross-chain redeploy protection by 23 | * setting it equal to `0x01`, 24 | * - The last random 11 bytes (i.e. `1212121212121212121212`) allow for 2**88 bits of entropy 25 | * for mining a salt. 26 | * @param salt The 32-byte random value used to create the contract address. 27 | * @return guardedSalt The guarded 32-byte random value used to create the contract address. 28 | */ 29 | 30 | export const createBaseSalt = ({ 31 | protectedSender, 32 | shouldAddRedeployProtection = false, 33 | additionalEntropy, 34 | }: { 35 | protectedSender?: Address; 36 | shouldAddRedeployProtection?: boolean; 37 | additionalEntropy: Hex; 38 | }) => { 39 | if (protectedSender) { 40 | return slice( 41 | concat([ 42 | protectedSender, 43 | pad(toHex(shouldAddRedeployProtection), {size: 1, dir: 'left'}), 44 | additionalEntropy, 45 | ]), 46 | 0, 47 | 32, 48 | ); 49 | } else if (shouldAddRedeployProtection) { 50 | return slice( 51 | concat([ 52 | zeroAddress, 53 | pad(toHex(shouldAddRedeployProtection), {size: 1, dir: 'left'}), 54 | additionalEntropy, 55 | ]), 56 | 0, 57 | 32, 58 | ); 59 | } 60 | return additionalEntropy; 61 | }; 62 | 63 | export const createGuardedSalt = ({ 64 | baseSalt, 65 | chainId, 66 | msgSender, 67 | }: { 68 | baseSalt: Hex; 69 | chainId?: number; 70 | msgSender?: Address; 71 | }) => { 72 | // Extract the first 20 bytes 73 | const senderBytes = slice(baseSalt, 0, 20); 74 | // Extract the 21st byte 75 | const protectionFlag = slice(baseSalt, 21, 22); 76 | 77 | // Check if sender matches msgSender 78 | const isEnforcedSender = senderBytes === msgSender; 79 | // Check if sender is zero address 80 | const isZeroAddress = senderBytes === zeroAddress; 81 | // Check if redeploy protection is enabled 82 | 83 | let hasRedeployProtection = false; 84 | if (protectionFlag === pad(toHex(true), {size: 1, dir: 'left'})) { 85 | hasRedeployProtection = true; 86 | } else if (protectionFlag === pad(toHex(false), {size: 1, dir: 'left'})) { 87 | hasRedeployProtection = false; 88 | } else { 89 | throw new Error('Invalid salt: protection flag must be 00 or 01'); 90 | } 91 | 92 | // Align with Solidity contract conditions 93 | if (isEnforcedSender && hasRedeployProtection) { 94 | if (chainId === undefined) { 95 | throw new Error('Chain ID is required for redeploy protection'); 96 | } 97 | if (msgSender === undefined) { 98 | throw new Error('Sender is required for sender protection'); 99 | } 100 | return keccak256( 101 | encodeAbiParameters( 102 | [{type: 'address'}, {type: 'uint256'}, {type: 'bytes32'}], 103 | [msgSender, BigInt(chainId), baseSalt], 104 | ), 105 | ); 106 | } else if (isEnforcedSender && !hasRedeployProtection) { 107 | if (msgSender === undefined) { 108 | throw new Error('Sender is required for sender protection'); 109 | } 110 | return keccak256( 111 | encodeAbiParameters( 112 | [{type: 'bytes32'}, {type: 'bytes32'}], 113 | [pad(msgSender, {size: 32, dir: 'left'}), baseSalt], 114 | ), 115 | ); 116 | } else if (isZeroAddress && hasRedeployProtection) { 117 | if (chainId === undefined) { 118 | throw new Error('Chain ID is required for redeploy protection'); 119 | } 120 | return keccak256( 121 | encodeAbiParameters( 122 | [{type: 'bytes32'}, {type: 'bytes32'}], 123 | [numberToHex(BigInt(chainId), {size: 32}), baseSalt], 124 | ), 125 | ); 126 | } else { 127 | // For non-pseudo-random cases, hash the salt 128 | return keccak256(encodeAbiParameters([{type: 'bytes32'}], [baseSalt])); 129 | } 130 | }; 131 | -------------------------------------------------------------------------------- /packages/cli/src/util/fetchSuperchainRegistryAddresses.ts: -------------------------------------------------------------------------------- 1 | import {zodAddress} from '@/util/schemas'; 2 | import {z} from 'zod'; 3 | 4 | export const SUPERCHAIN_REGISTRY_ADDRESSES_URL = 5 | 'https://raw.githubusercontent.com/ethereum-optimism/superchain-registry/refs/heads/main/superchain/extra/addresses/addresses.json'; 6 | 7 | // TODO: There's way more than this but this is all we need for now, and unclear which ones are required vs. not 8 | const zodAddressSet = z.object({ 9 | L1StandardBridgeProxy: zodAddress, 10 | }); 11 | 12 | const zodAddresses = z.record(z.coerce.number(), zodAddressSet); 13 | 14 | export type SuperchainRegistryAddresses = z.infer; 15 | 16 | export const fetchSuperchainRegistryAddresses = async ( 17 | addressesUrl: string, 18 | ) => { 19 | const response = await fetch(addressesUrl); 20 | if (!response.ok) { 21 | throw new Error(`Failed to fetch addresses: ${response.statusText}`); 22 | } 23 | 24 | const addressesJson = await response.json(); 25 | 26 | return zodAddresses.parse(addressesJson); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/cli/src/util/fetchSuperchainRegistryChainList.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod'; 2 | 3 | export const zodSupportedNetwork = z.enum([ 4 | 'mainnet', 5 | 'sepolia', 6 | 'interop-alpha', 7 | 'supersim', 8 | ]); 9 | 10 | export type SupportedNetwork = z.infer; 11 | 12 | export const CHAIN_LIST_URL = 13 | 'https://raw.githubusercontent.com/ethereum-optimism/superchain-registry/refs/heads/main/chainList.json'; 14 | 15 | const zodIdentifier = z.string().refine( 16 | (val): val is `${string}/${string}` => { 17 | const parts = val.split('/'); 18 | const [prefix, suffix] = parts; 19 | if (!prefix || !suffix) { 20 | return false; 21 | } 22 | return prefix.length > 0 && suffix.length > 0; 23 | }, 24 | { 25 | message: 26 | "Identifier must be in the format 'prefix/suffix' with non-empty parts", 27 | }, 28 | ); 29 | 30 | const zodChainListItem = z.object({ 31 | name: z.string(), 32 | identifier: zodIdentifier, 33 | chainId: z.number(), 34 | rpc: z.array(z.string()), 35 | explorers: z.array(z.string()), 36 | parent: z.object({ 37 | type: z.literal('L2'), 38 | chain: zodSupportedNetwork, 39 | }), 40 | }); 41 | 42 | const zodChainList = z.array(zodChainListItem); 43 | 44 | export type ChainListItem = z.infer; 45 | export type ChainList = z.infer; 46 | 47 | const zodChainListResponse = z.array(zodChainListItem); 48 | 49 | export const fetchSuperchainRegistryChainList = async ( 50 | chainListURL: string, 51 | ) => { 52 | const response = await fetch(chainListURL); 53 | if (!response.ok) { 54 | throw new Error(`Failed to fetch chain list: ${response.statusText}`); 55 | } 56 | 57 | const chainListJson = await response.json(); 58 | 59 | const parsedChainList = zodChainListResponse.parse(chainListJson); 60 | 61 | return parsedChainList.filter( 62 | chain => 63 | chain.parent.chain === 'mainnet' || chain.parent.chain === 'sepolia', 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /packages/cli/src/util/forge/findSolidityFiles.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import path from 'path'; 3 | 4 | export const findSolidityFiles = async ( 5 | dir: string, 6 | baseDir: string = dir, 7 | ): Promise => { 8 | const entries = await fs.readdir(dir, {withFileTypes: true}); 9 | const files: string[] = []; 10 | 11 | for (const entry of entries) { 12 | const fullPath = path.join(dir, entry.name); 13 | 14 | if (entry.isDirectory()) { 15 | files.push(...(await findSolidityFiles(fullPath, baseDir))); 16 | } else if (entry.isFile() && entry.name.endsWith('.sol')) { 17 | files.push(path.relative(baseDir, fullPath)); 18 | } 19 | } 20 | 21 | return files; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/cli/src/util/forge/foundryProject.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import path from 'path'; 3 | 4 | // TODO: update to use foundry.toml 5 | 6 | const SRC_DIR = 'src'; 7 | const ARTIFACT_DIR = 'out'; 8 | 9 | export const getSrcDir = (foundryProjectPath: string) => { 10 | return path.join(foundryProjectPath, SRC_DIR); 11 | }; 12 | 13 | export const getArtifactDir = (foundryProjectPath: string) => { 14 | return path.join(foundryProjectPath, ARTIFACT_DIR); 15 | }; 16 | 17 | export const getArtifactPathForContract = ( 18 | foundryProjectPath: string, 19 | contractFileRelativePath: string, 20 | ) => { 21 | const contractFileName = path.basename(contractFileRelativePath); 22 | return path.join( 23 | getArtifactDir(foundryProjectPath), 24 | `${contractFileName}`, 25 | `${ 26 | contractFileName.endsWith('.sol') 27 | ? contractFileName.slice(0, -4) 28 | : contractFileName 29 | }.json`, 30 | ); 31 | }; 32 | 33 | export const findFoundryRootUp = async ( 34 | startPath: string, 35 | maxDepth = 6, 36 | ): Promise => { 37 | let currentPath = startPath; 38 | let depth = 0; 39 | const root = path.parse(currentPath).root; 40 | 41 | while (currentPath !== root && depth < maxDepth) { 42 | try { 43 | await fs.access(path.join(currentPath, 'foundry.toml')); 44 | return currentPath; 45 | } catch { 46 | currentPath = path.dirname(currentPath); 47 | depth++; 48 | } 49 | } 50 | 51 | // Only check root if we haven't exceeded maxDepth 52 | if (depth < maxDepth) { 53 | try { 54 | await fs.access(path.join(root, 'foundry.toml')); 55 | return root; 56 | } catch { 57 | throw new Error('Could not find foundry.toml in any parent directory'); 58 | } 59 | } 60 | 61 | throw new Error( 62 | `Could not find foundry.toml within ${maxDepth} parent directories`, 63 | ); 64 | }; 65 | 66 | export const findFoundryRootDown = async ( 67 | startPath: string, 68 | maxDepth = 6, 69 | ): Promise => { 70 | const searchDir = async ( 71 | currentPath: string, 72 | depth: number, 73 | ): Promise => { 74 | if (depth > maxDepth) { 75 | throw new Error( 76 | `Could not find foundry.toml within ${maxDepth} subdirectories`, 77 | ); 78 | } 79 | 80 | try { 81 | const entries = await fs.readdir(currentPath, {withFileTypes: true}); 82 | 83 | // First check if foundry.toml exists in current directory 84 | if ( 85 | entries.some(entry => entry.isFile() && entry.name === 'foundry.toml') 86 | ) { 87 | return currentPath; 88 | } 89 | 90 | // Then recursively check subdirectories 91 | for (const entry of entries) { 92 | if (entry.isDirectory()) { 93 | try { 94 | const subdirPath = path.join(currentPath, entry.name); 95 | return await searchDir(subdirPath, depth + 1); 96 | } catch (e) { 97 | // Continue searching other directories if one branch fails 98 | continue; 99 | } 100 | } 101 | } 102 | 103 | throw new Error('No foundry.toml found in this directory branch'); 104 | } catch (e) { 105 | throw new Error( 106 | `Could not find foundry.toml within ${maxDepth} subdirectories`, 107 | ); 108 | } 109 | }; 110 | 111 | return searchDir(startPath, 0); 112 | }; 113 | 114 | export type FoundryProject = { 115 | baseDir: string; 116 | srcDir: string; 117 | artifactDir: string; 118 | }; 119 | 120 | export const fromBasePath = (baseDir: string): FoundryProject => { 121 | return { 122 | baseDir, 123 | srcDir: path.join(baseDir, SRC_DIR), 124 | artifactDir: path.join(baseDir, ARTIFACT_DIR), 125 | }; 126 | }; 127 | 128 | // artifact is a .json file in the out/ directory 129 | export const fromFoundryArtifactPath = async (foundryArtifactPath: string) => { 130 | const absolutePath = path.resolve(foundryArtifactPath); 131 | const foundryProjectPath = await findFoundryRootUp( 132 | path.dirname(absolutePath), 133 | ); 134 | const foundryProject = fromBasePath(foundryProjectPath); 135 | 136 | // Get the relative path from the project base to the artifact file 137 | const relativePath = path.relative(foundryProject.srcDir, absolutePath); 138 | 139 | return { 140 | foundryProject, 141 | contractFileName: `${path.basename(relativePath).replace('.json', '')}.sol`, 142 | }; 143 | }; 144 | 145 | export const getContractNameFromFoundryArtifactPath = async ( 146 | foundryArtifactPath: string, 147 | ) => { 148 | return `${foundryArtifactPath.replace('.json', '')}.sol`; 149 | }; 150 | -------------------------------------------------------------------------------- /packages/cli/src/util/forge/readForgeArtifact.ts: -------------------------------------------------------------------------------- 1 | import {zodAddress, zodHash, zodHex} from '@/util/schemas'; 2 | import {Abi} from 'abitype/zod'; 3 | import {z} from 'zod'; 4 | import fs from 'fs'; 5 | 6 | const zodCompilerOutputSource = z.object({ 7 | keccak256: zodHash, 8 | urls: z.array(z.string()), 9 | license: z.string(), 10 | }); 11 | 12 | export const zodForgeArtifact = z.object({ 13 | abi: Abi, 14 | bytecode: z.object({ 15 | object: zodHex, 16 | }), 17 | metadata: z.object({ 18 | compiler: z.object({ 19 | version: z.string(), 20 | }), 21 | language: z.enum(['Solidity', 'Vyper']), 22 | sources: z.record(z.string(), zodCompilerOutputSource), 23 | version: z.number(), 24 | settings: z.object({ 25 | remappings: z.array(z.string()), 26 | optimizer: z.object({ 27 | enabled: z.boolean(), 28 | runs: z.number(), 29 | details: z 30 | .object({ 31 | peephole: z.boolean(), 32 | }) 33 | .optional(), 34 | }), 35 | metadata: z.object({ 36 | bytecodeHash: z.string(), 37 | }), 38 | compilationTarget: z.record(z.string(), z.string()), 39 | evmVersion: z.string(), 40 | libraries: z.record(z.string(), z.record(z.string(), zodAddress)), 41 | }), 42 | }), 43 | }); 44 | 45 | export type CompilerOutputSource = z.infer; 46 | 47 | export type ForgeArtifact = z.infer; 48 | 49 | export const readForgeArtifact = async (artifactPath: string) => { 50 | const artifact = await fs.promises.readFile(artifactPath, 'utf8'); 51 | return zodForgeArtifact.parse(JSON.parse(artifact)); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/cli/src/util/schemas.ts: -------------------------------------------------------------------------------- 1 | import {Address as zAddress} from 'abitype/zod'; 2 | import {isHash, Hash, Hex, isHex, parseEther, parseGwei} from 'viem'; 3 | import {z} from 'zod'; 4 | 5 | export const zodHash = z 6 | .string() 7 | .refine((value): value is Hash => isHash(value), { 8 | message: 'Invalid hash', 9 | }); 10 | 11 | export const zodHex = z.string().refine((value): value is Hex => isHex(value), { 12 | message: 'Invalid hex', 13 | }); 14 | 15 | export const zodPrivateKey = z 16 | .string() 17 | .refine( 18 | (value): value is Hex => 19 | isHex(value) && value.startsWith('0x') && value.length === 66, 20 | { 21 | message: 'Invalid private key', 22 | }, 23 | ); 24 | 25 | export const zodAddress = zAddress; 26 | 27 | export const zodValueAmount = z.string().transform(value => { 28 | if (value.endsWith('ether')) { 29 | return parseEther(value.slice(0, -5)); 30 | } else if (value.endsWith('gwei')) { 31 | return parseGwei(value.slice(0, -4)); 32 | } else if (value.endsWith('wei')) { 33 | return BigInt(value); 34 | } 35 | 36 | return BigInt(value); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/cli/src/util/serialization.ts: -------------------------------------------------------------------------------- 1 | // Converts bigints into special objects that can be serialized and deserialized by JSON.stringify 2 | 3 | // Magic value to identify our serialized format 4 | export const SERIALIZATION_MAGIC = '@sup/serialized-v1' as const; 5 | 6 | export type SerializedValue = { 7 | magic: typeof SERIALIZATION_MAGIC; 8 | type: string; 9 | value: T; 10 | }; 11 | 12 | export type SerializedBigInt = SerializedValue; 13 | 14 | const isBigInt = (value: unknown): value is bigint => { 15 | return typeof value === 'bigint'; 16 | }; 17 | 18 | const isSerializedValue = ( 19 | value: unknown, 20 | type: string, 21 | ): value is SerializedValue => { 22 | return ( 23 | typeof value === 'object' && 24 | value !== null && 25 | 'magic' in value && 26 | 'type' in value && 27 | 'value' in value && 28 | value.magic === SERIALIZATION_MAGIC && 29 | value.type === type && 30 | value.value !== undefined 31 | ); 32 | }; 33 | 34 | const createSerializedValue = ( 35 | type: string, 36 | value: T, 37 | ): SerializedValue => { 38 | return { 39 | magic: SERIALIZATION_MAGIC, 40 | type, 41 | value, 42 | }; 43 | }; 44 | 45 | export const transformValueToBigInt = (value: unknown): unknown => { 46 | if (isSerializedValue(value, 'bigint')) { 47 | return BigInt(value.value); 48 | } 49 | 50 | if (Array.isArray(value)) { 51 | return value.map(transformValueToBigInt); 52 | } 53 | 54 | if (typeof value === 'object' && value !== null) { 55 | return Object.fromEntries( 56 | Object.entries(value).map(([k, v]) => [k, transformValueToBigInt(v)]), 57 | ); 58 | } 59 | 60 | return value; 61 | }; 62 | 63 | export const transformValueToSerializable = (value: unknown): unknown => { 64 | if (isBigInt(value)) { 65 | return createSerializedValue('bigint', value.toString()); 66 | } 67 | 68 | if (Array.isArray(value)) { 69 | return value.map(transformValueToSerializable); 70 | } 71 | 72 | if (typeof value === 'object' && value !== null) { 73 | return Object.fromEntries( 74 | Object.entries(value).map(([k, v]) => [ 75 | k, 76 | transformValueToSerializable(v), 77 | ]), 78 | ); 79 | } 80 | 81 | return value; 82 | }; 83 | -------------------------------------------------------------------------------- /packages/cli/src/util/sponsoredSender.ts: -------------------------------------------------------------------------------- 1 | const SPONSORED_SENDER_BASE_URL = 2 | 'https://dapp-console-api.optimism.io/api/sponsored-sender'; 3 | 4 | export const getSponsoredSenderWalletRpcUrl = ( 5 | apiKey: string, 6 | chainId: number, 7 | ) => { 8 | return `${SPONSORED_SENDER_BASE_URL}/${apiKey}/${chainId}`; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/cli/src/util/toCliFlags.ts: -------------------------------------------------------------------------------- 1 | export const toCliFlags = (options: Record): string => { 2 | return Object.entries(options) 3 | .filter( 4 | ([_, value]) => value !== '' && value !== undefined && value !== null, 5 | ) 6 | .map(([key, value]) => { 7 | return flagToString(key, value); 8 | }) 9 | .join(' ') 10 | .replace(/\n/g, ' '); 11 | }; 12 | 13 | const flagToString = (key: string, value: any) => { 14 | const flag = toKebabCase(key); 15 | 16 | if (Array.isArray(value)) { 17 | return `--${flag} ${value.join(',')}`; 18 | } 19 | 20 | if (typeof value === 'boolean') { 21 | if (value) { 22 | return `--${flag}`; 23 | } 24 | return ''; 25 | } 26 | 27 | return `--${flag} ${value.toString().replace('\n', ' ')}`; 28 | }; 29 | 30 | const toKebabCase = (str: string) => { 31 | return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/cli/src/util/transactionTask.ts: -------------------------------------------------------------------------------- 1 | import {Address, getAddress, Hex} from 'viem'; 2 | import jsonStableStringify from 'fast-json-stable-stringify'; 3 | import crypto from 'crypto'; 4 | 5 | export type TransactionTask = { 6 | chainId: number; 7 | to: Address; 8 | data?: Hex; 9 | value?: Hex; 10 | }; 11 | 12 | export const createTransactionTaskId = (request: TransactionTask): string => { 13 | // Create a normalized object where undefined values are explicitly null 14 | const normalized = { 15 | chainId: request.chainId, 16 | to: getAddress(request.to), 17 | data: request.data ?? null, 18 | value: request.value ?? null, 19 | }; 20 | 21 | const deterministicJson = jsonStableStringify(normalized); 22 | 23 | return crypto.createHash('sha256').update(deterministicJson).digest('hex'); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/cli/src/util/wizard-builder/createWizardStore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InferFieldsAtStep, 3 | InferStateAtStep, 4 | WizardStep, 5 | } from '@/util/wizard-builder/defineWizard'; 6 | import { 7 | capitalizeWords, 8 | CapitalizeWords, 9 | Prettify, 10 | } from '@/util/wizard-builder/utils'; 11 | import {create} from 'zustand'; 12 | 13 | export type WizardPossibleStates[]> = { 14 | [Index in keyof Steps]: Prettify< 15 | { 16 | readonly stepId: Steps[Index]['id']; 17 | } & InferStateAtStep 18 | >; 19 | }[number]; 20 | 21 | export type DefineStoreType[]> = { 22 | wizardState: WizardPossibleStates; 23 | steps: Steps; 24 | setWizardState: (state: WizardPossibleStates) => void; 25 | goToPreviousStep: () => void; 26 | } & { 27 | [Step in Steps[number] as `submit${CapitalizeWords}`]: ( 28 | value: InferFieldsAtStep, 29 | ) => void; 30 | }; 31 | 32 | // Factory function to create the store 33 | export function createWizardStore[]>( 34 | wizard: Steps, 35 | ) { 36 | type WizardType = Steps; 37 | type PossibleStates = WizardPossibleStates; 38 | 39 | type StoreType = DefineStoreType; 40 | 41 | const initialState: PossibleStates = { 42 | stepId: wizard[0]!.id, 43 | } as PossibleStates; 44 | 45 | const store = create((set, get) => { 46 | const goToPreviousStep = () => { 47 | const currentState = get().wizardState; 48 | const currentStepIndex = wizard.findIndex( 49 | step => step.id === currentState.stepId, 50 | ); 51 | 52 | if (currentStepIndex <= 0) return; // Can't go back from first step 53 | 54 | const prevStepId = wizard[currentStepIndex - 1]!.id; 55 | const prevState = { 56 | ...currentState, 57 | stepId: prevStepId, 58 | } as PossibleStates; 59 | 60 | set({wizardState: prevState} as StoreType); 61 | }; 62 | 63 | const submitFunctions = wizard.reduce((acc, step, index) => { 64 | const currentStepId = step.id; 65 | const nextStepId = wizard[index + 1]?.id || 'completed'; 66 | 67 | const functionName = `submit${capitalizeWords( 68 | currentStepId, 69 | )}` as keyof StoreType; 70 | 71 | acc[functionName] = ( 72 | value: InferFieldsAtStep, 73 | ) => { 74 | const currentState = get().wizardState as Extract< 75 | WizardPossibleStates, 76 | {stepId: typeof currentStepId} 77 | >; 78 | 79 | const nextState: Extract< 80 | WizardPossibleStates, 81 | {stepId: typeof nextStepId} 82 | > = { 83 | ...currentState, 84 | ...value, 85 | stepId: nextStepId, 86 | }; 87 | 88 | set({ 89 | wizardState: nextState, 90 | } as StoreType); 91 | }; 92 | 93 | return acc; 94 | }, {} as Record void>); 95 | 96 | return { 97 | wizardState: initialState, 98 | steps: wizard, 99 | setWizardState: (state: WizardPossibleStates) => { 100 | set({wizardState: state} as StoreType); 101 | }, 102 | goToPreviousStep, 103 | ...submitFunctions, 104 | } as StoreType; 105 | }); 106 | 107 | return store; 108 | } 109 | -------------------------------------------------------------------------------- /packages/cli/src/util/wizard-builder/defineWizard.ts: -------------------------------------------------------------------------------- 1 | import {Prettify} from '@/util/wizard-builder/utils'; 2 | import {z} from 'zod'; 3 | 4 | export type WizardStep< 5 | State, 6 | TSchema extends z.ZodTypeAny, 7 | Id extends string = string, 8 | > = { 9 | id: Id; 10 | schema: TSchema; 11 | getSummary?: (state: State & z.infer) => string; 12 | title: string; 13 | }; 14 | 15 | type WizardBuilder[] = []> = { 16 | addStep: ( 17 | wizardStep: WizardStep, 18 | ) => WizardBuilder< 19 | State & z.infer, 20 | [...Steps, WizardStep] 21 | >; 22 | build: () => [ 23 | ...Steps, 24 | WizardStep< 25 | AccumulateStateFromSteps, 26 | typeof completedState.schema, 27 | 'completed' 28 | >, 29 | ]; 30 | }; 31 | 32 | type AccumulateStateFromSteps[]> = 33 | Steps extends [WizardStep, ...infer Rest] 34 | ? Id extends 'completed' 35 | ? {} 36 | : z.infer & 37 | AccumulateStateFromSteps< 38 | Rest extends WizardStep[] ? Rest : [] 39 | > 40 | : {}; 41 | 42 | type AccumulateStateBeforeId< 43 | Steps extends WizardStep[], 44 | Id extends string, 45 | AccumulatedState = {}, 46 | > = Steps extends [infer First, ...infer Rest] 47 | ? First extends WizardStep 48 | ? StepId extends Id 49 | ? AccumulatedState 50 | : AccumulateStateBeforeId< 51 | Rest extends WizardStep[] ? Rest : [], 52 | Id, 53 | AccumulatedState & z.infer 54 | > 55 | : never 56 | : AccumulatedState; 57 | 58 | export type InferStateAtStep< 59 | Steps extends WizardStep[], 60 | StepId extends string, 61 | > = Prettify>; 62 | 63 | export type InferFieldsAtStep< 64 | Steps extends WizardStep[], 65 | StepId extends string, 66 | > = Steps extends [infer First, ...infer Rest] 67 | ? First extends WizardStep 68 | ? Id extends StepId 69 | ? z.infer 70 | : InferFieldsAtStep< 71 | Rest extends WizardStep[] ? Rest : [], 72 | StepId 73 | > 74 | : never 75 | : never; 76 | 77 | export type InferFinalState[]> = Prettify< 78 | AccumulateStateFromSteps 79 | >; 80 | 81 | export type InferStepId[]> = 82 | Steps[number]['id']; 83 | 84 | const completedState = { 85 | id: 'completed' as const, 86 | schema: z.object({}), 87 | } as const; 88 | 89 | export function defineWizard< 90 | State = {}, 91 | Steps extends WizardStep[] = [], 92 | >(steps: Steps = [] as unknown as Steps): WizardBuilder { 93 | return { 94 | addStep( 95 | wizardStep: WizardStep, 96 | ) { 97 | return defineWizard< 98 | State & z.infer, 99 | [...Steps, WizardStep] 100 | >([...steps, wizardStep] as [...Steps, WizardStep]); 101 | }, 102 | build: () => { 103 | return [...steps, completedState] as [ 104 | ...Steps, 105 | WizardStep< 106 | AccumulateStateFromSteps, 107 | typeof completedState.schema, 108 | 'completed' 109 | >, 110 | ]; 111 | }, 112 | }; 113 | } 114 | -------------------------------------------------------------------------------- /packages/cli/src/util/wizard-builder/example.ts: -------------------------------------------------------------------------------- 1 | import {createWizardStore} from '@/util/wizard-builder/createWizardStore'; 2 | import {defineWizard} from '@/util/wizard-builder/defineWizard'; 3 | import {z} from 'zod'; 4 | 5 | const bridgeWizard = defineWizard() 6 | .addStep({ 7 | id: 'selectNetwork', 8 | schema: z.object({ 9 | network: z.string(), 10 | }), 11 | title: 'Select Network', 12 | getSummary: state => `${state.network}`, 13 | }) 14 | .addStep({ 15 | id: 'selectChains', 16 | schema: z.object({ 17 | chainIds: z.array(z.number()), 18 | }), 19 | title: 'Select Chains', 20 | getSummary: state => `${state.chainIds.join(', ')}`, 21 | }) 22 | .addStep({ 23 | id: 'enterAmount', 24 | schema: z.object({ 25 | amount: z.bigint(), 26 | }), 27 | title: 'Enter Amount', 28 | getSummary: state => { 29 | return `Amount: ${state.amount}`; 30 | }, 31 | }) 32 | .build(); 33 | 34 | export const store = createWizardStore(bridgeWizard); 35 | -------------------------------------------------------------------------------- /packages/cli/src/util/wizard-builder/utils.ts: -------------------------------------------------------------------------------- 1 | // converts snake case to camel case 2 | export function capitalizeWords(s: string): string { 3 | return s.replace(/(?:^|-)([a-z])/g, (_, c) => c.toUpperCase()); 4 | } 5 | 6 | // converts snake case to camel case 7 | 8 | export type CapitalizeWords = 9 | S extends `${infer First}-${infer Rest}` 10 | ? `${Capitalize}${CapitalizeWords}` 11 | : Capitalize; 12 | 13 | /** 14 | * @description Combines members of an intersection into a readable type. 15 | * 16 | * @see {@link https://twitter.com/mattpocockuk/status/1622730173446557697?s=20&t=NdpAcmEFXY01xkqU3KO0Mg} 17 | * @example 18 | * Prettify<{ a: string } & { b: string } & { c: number, d: bigint }> 19 | * => { a: string, b: string, c: number, d: bigint } 20 | */ 21 | export type Prettify = { 22 | [K in keyof T]: T[K]; 23 | } & {}; 24 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "outDir": "dist", 5 | 6 | "paths": { 7 | "@/*": [ 8 | "./src/*" 9 | ] 10 | }, 11 | 12 | /* React */ 13 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 14 | "jsx": "react-jsx", 15 | 16 | /* Bundler mode */ 17 | "module": "ESNext", 18 | "moduleResolution": "Bundler", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "strict": true, 22 | "declaration": true, 23 | "esModuleInterop": true, 24 | "skipLibCheck": true, 25 | "forceConsistentCasingInFileNames": true, 26 | "useDefineForClassFields": true, 27 | "pretty": true, 28 | "newLine": "lf", 29 | 30 | /* Linting */ 31 | "noImplicitReturns": true, 32 | "noImplicitOverride": true, 33 | "noUnusedLocals": true, 34 | "noUnusedParameters": true, 35 | "noFallthroughCasesInSwitch": true, 36 | "noUncheckedIndexedAccess": true, 37 | "noPropertyAccessFromIndexSignature": true, 38 | "noEmitOnError": true 39 | }, 40 | "include": ["src"] 41 | } -------------------------------------------------------------------------------- /packages/signer-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/signer-frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /packages/signer-frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /packages/signer-frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /packages/signer-frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SUP 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/signer-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eth-optimism/super-cli-signer-frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "typecheck": "tsc --noEmit", 10 | "lint": "eslint .", 11 | "preview": "vite preview", 12 | "shadcn:add": "pnpm dlx shadcn@latest add" 13 | }, 14 | "dependencies": { 15 | "@radix-ui/react-accordion": "^1.2.2", 16 | "@radix-ui/react-slot": "^1.1.1", 17 | "@radix-ui/react-tabs": "^1.1.2", 18 | "@rainbow-me/rainbowkit": "^2.2.1", 19 | "@tanstack/react-query": "^5.59.20", 20 | "@wagmi/core": "^2.16.0", 21 | "abitype": "^1.0.6", 22 | "class-variance-authority": "^0.7.1", 23 | "clsx": "^2.1.1", 24 | "lucide-react": "^0.468.0", 25 | "next-themes": "^0.4.4", 26 | "react": "^18.3.1", 27 | "react-dom": "^18.3.1", 28 | "sonner": "^1.7.1", 29 | "tailwind-merge": "^2.5.5", 30 | "tailwindcss-animate": "^1.0.7", 31 | "viem": "^2.21.41", 32 | "wagmi": "^2.14.2", 33 | "zod": "^3.21.4" 34 | }, 35 | "devDependencies": { 36 | "@eslint/js": "^9.15.0", 37 | "@types/node": "^22.9.0", 38 | "@types/react": "^18.3.12", 39 | "@types/react-dom": "^18.3.1", 40 | "@vitejs/plugin-react": "^4.3.4", 41 | "autoprefixer": "^10.4.20", 42 | "eslint": "^9.15.0", 43 | "eslint-plugin-react-hooks": "^5.0.0", 44 | "eslint-plugin-react-refresh": "^0.4.14", 45 | "globals": "^15.12.0", 46 | "postcss": "^8.4.49", 47 | "tailwindcss": "^3.4.16", 48 | "typescript": "~5.6.2", 49 | "typescript-eslint": "^8.15.0", 50 | "vite": "^6.0.1" 51 | } 52 | } -------------------------------------------------------------------------------- /packages/signer-frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/signer-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum-optimism/super-cli/57288e4b634159f8705e9d686de5d50768adb15a/packages/signer-frontend/public/favicon.ico -------------------------------------------------------------------------------- /packages/signer-frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { TransactionTasks } from "@/components/TransactionTasks"; 2 | import { Toaster } from "@/components/ui/sonner"; 3 | import { Providers } from "@/Providers"; 4 | import { ConnectButton } from "@rainbow-me/rainbowkit"; 5 | 6 | function App() { 7 | return ( 8 | 9 | 10 |
11 | 12 | 13 |
14 |
15 | ); 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /packages/signer-frontend/src/Providers.tsx: -------------------------------------------------------------------------------- 1 | import "@rainbow-me/rainbowkit/styles.css"; 2 | 3 | import { 4 | QueryClient, 5 | QueryClientProvider, 6 | useQuery, 7 | } from "@tanstack/react-query"; 8 | import { WagmiProvider } from "wagmi"; 9 | import { createWagmiConfig } from "@/wagmiConfig"; 10 | import { getMappingChainById } from "@/api"; 11 | import { lightTheme, RainbowKitProvider } from "@rainbow-me/rainbowkit"; 12 | import { Loader2 } from "lucide-react"; 13 | import { Alert, AlertDescription } from "@/components/ui/alert"; 14 | 15 | const queryClient = new QueryClient(); 16 | 17 | const WagmiProviderWrapper = ({ children }: { children: React.ReactNode }) => { 18 | const { data, isLoading, error } = useQuery({ 19 | queryKey: ["getMappingChainById"], 20 | queryFn: getMappingChainById, 21 | staleTime: 1000 * 60 * 60 * 24, // 24 hours 22 | }); 23 | 24 | if (isLoading) { 25 | return ( 26 |
27 | 28 |
29 | ); 30 | } 31 | 32 | if (error) { 33 | return ( 34 | 35 | Unable to connect to the CLI. 36 | 37 | ); 38 | } 39 | 40 | if (!data) { 41 | return ( 42 | 43 | No chain configuration found. 44 | 45 | ); 46 | } 47 | 48 | const wagmiConfig = createWagmiConfig(data.chainById); 49 | 50 | return {children}; 51 | }; 52 | 53 | export const Providers = ({ children }: { children: React.ReactNode }) => { 54 | return ( 55 | 56 | 57 | 64 | {children} 65 | 66 | 67 | 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /packages/signer-frontend/src/api.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { Address as zodAddress } from "abitype/zod"; 3 | import { Chain, Hash, Hex, isHash, isHex } from "viem"; 4 | 5 | // In prod, the api is hosted on the same domain as the frontend 6 | const apiBaseUrl = 7 | import.meta.env.VITE_FRONTEND_MODE === "dev" ? "http://localhost:3000" : ""; 8 | 9 | // TODO: move to shared package 10 | const zodHex = z.string().refine((value): value is Hex => isHex(value), { 11 | message: "Invalid hex", 12 | }); 13 | 14 | export const zodHash = z 15 | .string() 16 | .refine((value): value is Hash => isHash(value), { 17 | message: "Invalid hash", 18 | }); 19 | 20 | // TODO: move shared API definitions to shared package or use trpc or sth. 21 | const zodTransactionTask = z.object({ 22 | chainId: z.number(), 23 | to: zodAddress.optional(), 24 | data: zodHex.optional(), 25 | value: zodHex.optional(), 26 | }); 27 | 28 | const zodTaskResult = z.union([ 29 | z.object({ 30 | type: z.literal("success"), 31 | hash: zodHash, 32 | }), 33 | z.object({ 34 | type: z.literal("error"), 35 | error: z.string(), 36 | }), 37 | ]); 38 | 39 | const zodTransactionTaskEntry = z.object({ 40 | id: z.string(), 41 | request: zodTransactionTask, 42 | result: zodTaskResult.optional(), 43 | createdAt: z.coerce.date(), 44 | }); 45 | 46 | export type TransactionTaskEntry = z.infer; 47 | 48 | const zodListTransactionTaskResponse = z.object({ 49 | transactionTasks: z.array(zodTransactionTaskEntry), 50 | }); 51 | 52 | export const getMappingChainById = async () => { 53 | const response = await fetch(`${apiBaseUrl}/api/getMappingChainById`, { 54 | method: "POST", 55 | }); 56 | 57 | if (!response.ok) { 58 | throw new Error( 59 | `Failed to get mapping chain by id: ${response.statusText}` 60 | ); 61 | } 62 | 63 | // TODO: validate using zod 64 | const json = (await response.json()) as { 65 | chainById: Record; 66 | }; 67 | return json; 68 | }; 69 | 70 | export const listTransactionTasks = async () => { 71 | const response = await fetch(`${apiBaseUrl}/api/listTransactionTasks`, { 72 | method: "POST", 73 | }); 74 | 75 | if (!response.ok) { 76 | throw new Error(`Failed to list transaction tasks: ${response.statusText}`); 77 | } 78 | 79 | const json = await response.json(); 80 | const parsed = zodListTransactionTaskResponse.parse(json); 81 | return parsed.transactionTasks; 82 | }; 83 | 84 | export const completeTransactionTask = async (id: string, hash: Hash) => { 85 | const response = await fetch(`${apiBaseUrl}/api/completeTransactionTask`, { 86 | headers: { 87 | "Content-Type": "application/json", 88 | }, 89 | method: "POST", 90 | body: JSON.stringify({ id, hash }), 91 | }); 92 | 93 | if (!response.ok) { 94 | throw new Error( 95 | `Failed to complete transaction task: ${response.statusText}` 96 | ); 97 | } 98 | 99 | return response.json(); 100 | }; 101 | -------------------------------------------------------------------------------- /packages/signer-frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/signer-frontend/src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 3 | import { ChevronDown } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Accordion = AccordionPrimitive.Root 8 | 9 | const AccordionItem = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 18 | )) 19 | AccordionItem.displayName = "AccordionItem" 20 | 21 | const AccordionTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, children, ...props }, ref) => ( 25 | 26 | svg]:rotate-180", 30 | className 31 | )} 32 | {...props} 33 | > 34 | {children} 35 | 36 | 37 | 38 | )) 39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 40 | 41 | const AccordionContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, children, ...props }, ref) => ( 45 | 50 |
{children}
51 |
52 | )) 53 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 54 | 55 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 56 | -------------------------------------------------------------------------------- /packages/signer-frontend/src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /packages/signer-frontend/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /packages/signer-frontend/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /packages/signer-frontend/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "next-themes" 2 | import { Toaster as Sonner } from "sonner" 3 | 4 | type ToasterProps = React.ComponentProps 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 26 | ) 27 | } 28 | 29 | export { Toaster } 30 | -------------------------------------------------------------------------------- /packages/signer-frontend/src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )) 52 | TableFooter.displayName = "TableFooter" 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )) 67 | TableRow.displayName = "TableRow" 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
[role=checkbox]]:translate-y-[2px]", 77 | className 78 | )} 79 | {...props} 80 | /> 81 | )) 82 | TableHead.displayName = "TableHead" 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | [role=checkbox]]:translate-y-[2px]", 92 | className 93 | )} 94 | {...props} 95 | /> 96 | )) 97 | TableCell.displayName = "TableCell" 98 | 99 | const TableCaption = React.forwardRef< 100 | HTMLTableCaptionElement, 101 | React.HTMLAttributes 102 | >(({ className, ...props }, ref) => ( 103 |
108 | )) 109 | TableCaption.displayName = "TableCaption" 110 | 111 | export { 112 | Table, 113 | TableHeader, 114 | TableBody, 115 | TableFooter, 116 | TableHead, 117 | TableRow, 118 | TableCell, 119 | TableCaption, 120 | } 121 | -------------------------------------------------------------------------------- /packages/signer-frontend/src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as TabsPrimitive from "@radix-ui/react-tabs" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Tabs = TabsPrimitive.Root 7 | 8 | const TabsList = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | TabsList.displayName = TabsPrimitive.List.displayName 22 | 23 | const TabsTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 35 | )) 36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 37 | 38 | const TabsContent = React.forwardRef< 39 | React.ElementRef, 40 | React.ComponentPropsWithoutRef 41 | >(({ className, ...props }, ref) => ( 42 | 50 | )) 51 | TabsContent.displayName = TabsPrimitive.Content.displayName 52 | 53 | export { Tabs, TabsList, TabsTrigger, TabsContent } 54 | -------------------------------------------------------------------------------- /packages/signer-frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @layer base { 5 | :root { 6 | --background: 0 0% 100%; 7 | --foreground: 0 0% 3.9%; 8 | --card: 0 0% 100%; 9 | --card-foreground: 0 0% 3.9%; 10 | --popover: 0 0% 100%; 11 | --popover-foreground: 0 0% 3.9%; 12 | --primary: 0 0% 9%; 13 | --primary-foreground: 0 0% 98%; 14 | --secondary: 0 0% 96.1%; 15 | --secondary-foreground: 0 0% 9%; 16 | --muted: 0 0% 96.1%; 17 | --muted-foreground: 0 0% 45.1%; 18 | --accent: 0 0% 96.1%; 19 | --accent-foreground: 0 0% 9%; 20 | --destructive: 0 84.2% 60.2%; 21 | --destructive-foreground: 0 0% 98%; 22 | --border: 0 0% 89.8%; 23 | --input: 0 0% 89.8%; 24 | --ring: 0 0% 3.9%; 25 | --chart-1: 12 76% 61%; 26 | --chart-2: 173 58% 39%; 27 | --chart-3: 197 37% 24%; 28 | --chart-4: 43 74% 66%; 29 | --chart-5: 27 87% 67%; 30 | --radius: 0.5rem 31 | } 32 | .dark { 33 | --background: 0 0% 3.9%; 34 | --foreground: 0 0% 98%; 35 | --card: 0 0% 3.9%; 36 | --card-foreground: 0 0% 98%; 37 | --popover: 0 0% 3.9%; 38 | --popover-foreground: 0 0% 98%; 39 | --primary: 0 0% 98%; 40 | --primary-foreground: 0 0% 9%; 41 | --secondary: 0 0% 14.9%; 42 | --secondary-foreground: 0 0% 98%; 43 | --muted: 0 0% 14.9%; 44 | --muted-foreground: 0 0% 63.9%; 45 | --accent: 0 0% 14.9%; 46 | --accent-foreground: 0 0% 98%; 47 | --destructive: 0 62.8% 30.6%; 48 | --destructive-foreground: 0 0% 98%; 49 | --border: 0 0% 14.9%; 50 | --input: 0 0% 14.9%; 51 | --ring: 0 0% 83.1%; 52 | --chart-1: 220 70% 50%; 53 | --chart-2: 160 60% 45%; 54 | --chart-3: 30 80% 55%; 55 | --chart-4: 280 65% 60%; 56 | --chart-5: 340 75% 55% 57 | } 58 | } 59 | @layer base { 60 | * { 61 | @apply border-border; 62 | } 63 | body { 64 | @apply bg-background text-foreground; 65 | } 66 | } -------------------------------------------------------------------------------- /packages/signer-frontend/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /packages/signer-frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /packages/signer-frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/signer-frontend/src/wagmiConfig.ts: -------------------------------------------------------------------------------- 1 | import { Chain } from "wagmi/chains"; 2 | import { getDefaultConfig } from "@rainbow-me/rainbowkit"; 3 | 4 | export const createWagmiConfig = (chainById: Record) => { 5 | return getDefaultConfig({ 6 | chains: Object.values(chainById) as [Chain, ...Chain[]], 7 | appName: "SUP Signer UI", 8 | // TODO: use a real one 9 | projectId: "1234567890", 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/signer-frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"], 5 | theme: { 6 | extend: { 7 | borderRadius: { 8 | lg: 'var(--radius)', 9 | md: 'calc(var(--radius) - 2px)', 10 | sm: 'calc(var(--radius) - 4px)' 11 | }, 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))' 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))' 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))' 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))' 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))' 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))' 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))' 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))' 52 | } 53 | }, 54 | keyframes: { 55 | 'accordion-down': { 56 | from: { 57 | height: '0' 58 | }, 59 | to: { 60 | height: 'var(--radix-accordion-content-height)' 61 | } 62 | }, 63 | 'accordion-up': { 64 | from: { 65 | height: 'var(--radix-accordion-content-height)' 66 | }, 67 | to: { 68 | height: '0' 69 | } 70 | } 71 | }, 72 | animation: { 73 | 'accordion-down': 'accordion-down 0.2s ease-out', 74 | 'accordion-up': 'accordion-up 0.2s ease-out' 75 | } 76 | } 77 | }, 78 | plugins: [require("tailwindcss-animate")], 79 | }; 80 | -------------------------------------------------------------------------------- /packages/signer-frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true, 24 | 25 | "baseUrl": ".", 26 | "paths": { 27 | "@/*": [ 28 | "./src/*" 29 | ] 30 | } 31 | }, 32 | "include": ["src"] 33 | } 34 | -------------------------------------------------------------------------------- /packages/signer-frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/signer-frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/signer-frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import react from "@vitejs/plugin-react"; 3 | import { defineConfig } from "vite"; 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | resolve: { 8 | alias: { 9 | "@": path.resolve(__dirname, "./src"), 10 | }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' --------------------------------------------------------------------------------