diff --git a/.github/actions/restore-build-artifacts/action.yml b/.github/actions/restore-build-artifacts/action.yml index 070f4cdbc6..f9f1665977 100644 --- a/.github/actions/restore-build-artifacts/action.yml +++ b/.github/actions/restore-build-artifacts/action.yml @@ -8,6 +8,6 @@ runs: uses: actions/cache/restore@v4 with: path: | - ./packages/arb-token-bridge-ui/build + ./packages/app/build key: build-artifacts-${{ github.run_id }} fail-on-cache-miss: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d3ba67de5..da193e92e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: uses: actions/cache/restore@v4 with: path: | - ./packages/arb-token-bridge-ui/build + ./packages/app/build key: build-artifacts-${{ github.run_id }} - name: Install node_modules @@ -58,5 +58,5 @@ jobs: uses: actions/cache/save@v4 with: path: | - ./packages/arb-token-bridge-ui/build + ./packages/app/build key: build-artifacts-${{ github.run_id }} diff --git a/audit-ci.jsonc b/audit-ci.jsonc index b05e3a17be..66e76d9ae6 100644 --- a/audit-ci.jsonc +++ b/audit-ci.jsonc @@ -22,6 +22,21 @@ // GHSA-95m3-7q98-8xr5|arb-token-bridge-ui>wagmi>@wagmi/connectors>cbw-sdk>sha.js // Reason to ignore: Low risk client-side vulnerability in Coinbase Wallet connector only. // Does not affect core bridge functionality, limited to wallet-specific operations. - "GHSA-95m3-7q98-8xr5" + "GHSA-95m3-7q98-8xr5", + // https://github.com/advisories/GHSA-v6h2-p8h4-qcjw + // Vulnerability was found in juliangruber brace-expansion up to 1.1.11/2.0.1/3.0.0/4.0.0. It has been rated as problematic. + // Affected by this issue is the function expand of the file index.js. + // + // Reason to ignore: This is a development issue only (eslint) + // GHSA-v6h2-p8h4-qcjw|@typescript-eslint/parser>@typescript-eslint/typescript-estree>minimatch>brace-expansion + // GHSA-v6h2-p8h4-qcjw|@typescript-eslint/eslint-plugin>@typescript-eslint/type-utils>@typescript-eslint/typescript-estree>minimatch>brace-expansion + // GHSA-v6h2-p8h4-qcjw|typescript-eslint>@typescript-eslint/eslint-plugin>@typescript-eslint/type-utils>@typescript-eslint/typescript-estree>minimatch>brace-expansion + // GHSA-v6h2-p8h4-qcjw|typescript-eslint>@typescript-eslint/eslint-plugin>@typescript-eslint/type-utils>@typescript-eslint/utils>@typescript-eslint/typescript-estree>minimatch>brace-expansion + // GHSA-v6h2-p8h4-qcjw|eslint-config-next>@typescript-eslint/parser>@typescript-eslint/typescript-estree>minimatch>brace-expansion + // GHSA-v6h2-p8h4-qcjw|eslint-config-next>@typescript-eslint/eslint-plugin>@typescript-eslint/type-utils>@typescript-eslint/typescript-estree>minimatch>brace-expansion + // GHSA-v6h2-p8h4-qcjw|eslint-config-next>@typescript-eslint/eslint-plugin>@typescript-eslint/type-utils>@typescript-eslint/utils>@typescript-eslint/typescript-estree>minimatch>brace-expansion + // GHSA-v6h2-p8h4-qcjw|@eslint/eslintrc>minimatch>brace-expansion + // GHSA-v6h2-p8h4-qcjw|eslint>@eslint/eslintrc>minimatch>brace-expansion + "GHSA-v6h2-p8h4-qcjw" ] } diff --git a/eslint.config.mjs b/eslint.config.mjs index 28b512c3fa..6e05ef19bf 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,117 +1,121 @@ -import { globalIgnores } from "eslint/config"; -import path from "path"; -import { fileURLToPath } from "url"; -import typescriptEslint from "@typescript-eslint/eslint-plugin"; -import zustandRules from "eslint-plugin-zustand-rules"; -import tsParser from "@typescript-eslint/parser"; -import prettierRecommended from "eslint-plugin-prettier/recommended"; -import { flatConfig as nextFlatConfig } from "@next/eslint-plugin-next"; -import tseslint from "typescript-eslint"; +import { globalIgnores } from 'eslint/config' +import path from 'path' +import { fileURLToPath } from 'url' +import typescriptEslint from '@typescript-eslint/eslint-plugin' +import zustandRules from 'eslint-plugin-zustand-rules' +import tsParser from '@typescript-eslint/parser' +import prettierRecommended from 'eslint-plugin-prettier/recommended' +import { flatConfig as nextFlatConfig } from '@next/eslint-plugin-next' +import tseslint from 'typescript-eslint' -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) export default [ ...tseslint.configs.recommended, prettierRecommended, nextFlatConfig.recommended, { - files: ["**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}"], + files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], plugins: { - "@typescript-eslint": typescriptEslint, - "zustand-rules": zustandRules, + '@typescript-eslint': typescriptEslint, + 'zustand-rules': zustandRules }, languageOptions: { parser: tsParser, - sourceType: "module", + sourceType: 'module', parserOptions: { - project: ["./tsconfig.eslint.json", "./packages/**/tsconfig.json"], - tsconfigRootDir: __dirname, - }, + project: ['./tsconfig.eslint.json', './packages/**/tsconfig.json'], + tsconfigRootDir: __dirname + } }, settings: { react: { - version: "detect", + version: 'detect' }, next: { - rootDir: "packages/arb-token-bridge-ui/", - }, + rootDir: 'packages/app/' + } }, rules: { - "@typescript-eslint/no-non-null-assertion": "warn", - "@typescript-eslint/no-unused-vars": [ - "error", + '@typescript-eslint/no-non-null-assertion': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'error', { - vars: "all", - args: "after-used", - caughtErrors: "none", + vars: 'all', + args: 'after-used', + caughtErrors: 'none', ignoreRestSiblings: false, - reportUsedIgnorePattern: false, - }, + reportUsedIgnorePattern: false + } ], - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/explicit-module-boundary-types": "off", + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/explicit-module-boundary-types': 'off', - "@typescript-eslint/ban-ts-comment": [ - "error", + '@typescript-eslint/ban-ts-comment': [ + 'error', { - "ts-expect-error": "allow-with-description", - "ts-ignore": "allow-with-description", - "ts-nocheck": "allow-with-description", - "ts-check": "allow-with-description", - }, + 'ts-expect-error': 'allow-with-description', + 'ts-ignore': 'allow-with-description', + 'ts-nocheck': 'allow-with-description', + 'ts-check': 'allow-with-description' + } ], - "@typescript-eslint/await-thenable": "error", + '@typescript-eslint/await-thenable': 'error', - "zustand-rules/use-store-selectors": "error", - "zustand-rules/no-state-mutation": "error", - "zustand-rules/enforce-use-setstate": "error", + 'zustand-rules/use-store-selectors': 'error', + 'zustand-rules/no-state-mutation': 'error', + 'zustand-rules/enforce-use-setstate': 'error', - "@next/next/no-html-link-for-pages": [ - "error", - path.join(__dirname, "/packages/arb-token-bridge-ui/src/pages"), - ], - }, + '@next/next/no-html-link-for-pages': [ + 'error', + [ + path.join(__dirname, '/packages/arb-token-bridge-ui/src/components'), + path.join(__dirname, '/packages/app/pages') + ] + ] + } }, { - files: ["**/tests/e2e/**/*.ts", "**/tests/support/**/*.js"], + files: ['**/tests/e2e/**/*.ts', '**/tests/support/**/*.js'], languageOptions: { parser: tsParser, - sourceType: "module", + sourceType: 'module', parserOptions: { - project: ["packages/arb-token-bridge-ui/tests/tsconfig.json"], - }, + project: ['packages/arb-token-bridge-ui/tests/tsconfig.json'] + } }, rules: { // Cypress awaiting by default - "no-debugger": 0, - "no-console": 0, - "@typescript-eslint/no-unused-vars": [ - "error", + 'no-debugger': 0, + 'no-console': 0, + '@typescript-eslint/no-unused-vars': [ + 'error', { - vars: "all", - args: "after-used", - caughtErrors: "none", + vars: 'all', + args: 'after-used', + caughtErrors: 'none', ignoreRestSiblings: false, - reportUsedIgnorePattern: false, - }, + reportUsedIgnorePattern: false + } ], - "@typescript-eslint/no-empty-function": "off", - }, + '@typescript-eslint/no-empty-function': 'off' + } }, globalIgnores([ - "**/node_modules", - "**/dist", - "**/build/", - ".github/", - "**/cypress/", - ]), -]; + '**/node_modules', + '**/dist', + '**/build/', + '**/.next/', + '.github/', + '**/cypress/' + ]) +] diff --git a/package.json b/package.json index 0d2bea47aa..63bd16a802 100644 --- a/package.json +++ b/package.json @@ -6,15 +6,15 @@ "description": "", "main": "index.js", "scripts": { - "dev": "yarn workspace arb-token-bridge-ui dev", - "build": "yarn workspace arb-token-bridge-ui build", - "start": "yarn workspace arb-token-bridge-ui start", + "dev": "yarn workspace app dev", + "build": "yarn workspace app build", + "start": "yarn workspace app start", "audit:ci": "audit-ci --config ./audit-ci.jsonc", "test:ci": "yarn workspace arb-token-bridge-ui test:ci", "prettier:check": "./node_modules/.bin/prettier --check .", "prettier:format": "./node_modules/.bin/prettier --write .", - "lint": "yarn workspace arb-token-bridge-ui lint", - "lint:fix": "yarn workspace arb-token-bridge-ui lint:fix", + "lint": "yarn workspace app lint && yarn workspace arb-token-bridge-ui lint", + "lint:fix": "yarn workspace app lint:fix && yarn workspace arb-token-bridge-ui lint:fix", "postinstall": "yarn install:chromium", "install:chromium": "npx @puppeteer/browsers install chrome@$npm_package_config_chromeVersion --path $npm_package_config_chromePath", "test:e2e": "yarn workspace arb-token-bridge-ui env-cmd --silent --file .e2e.env yarn synpress run --configFile synpress.config.ts", @@ -34,7 +34,8 @@ "**/tar-fs": "2.1.3", "sharp/tar-fs": "3.0.9", "**/form-data": "4.0.4", - "**/axios": "1.11.0" + "**/axios": "1.11.0", + "@eslint/plugin-kit": "0.3.4" }, "keywords": [], "author": "", @@ -42,7 +43,29 @@ "packages/*" ], "devDependencies": { - "audit-ci": "^6.3.0" + "audit-ci": "^6.3.0", + "prettier": "^2.7.1", + "prettier-plugin-tailwindcss": "^0.1.11", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.26.0", + "@next/eslint-plugin-next": "^15.3.2", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", + "eslint": "^9.26.0", + "eslint-config-next": "^15.3.2", + "eslint-config-prettier": "^10.1.5", + "eslint-import-resolver-alias": "^1.1.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-prettier": "^5.4.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-zustand-rules": "https://github.com/OffchainLabs/eslint-plugin-zustand-rules", + "typescript-eslint": "^8.32.1", + "typescript": "^5.2.2", + "@types/node": "^16.6.1", + "@types/react": "^18.2.6", + "@types/react-dom": "^18.2.4" }, "config": { "chromeVersion": "128.0.6613.137", diff --git a/packages/arb-token-bridge-ui/.env.local.sample b/packages/app/.env.local.sample similarity index 100% rename from packages/arb-token-bridge-ui/.env.local.sample rename to packages/app/.env.local.sample diff --git a/packages/app/.gitignore b/packages/app/.gitignore new file mode 100644 index 0000000000..c11e5a25db --- /dev/null +++ b/packages/app/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage +/cypress + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local +.e2e.env + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pre-commit-config.yaml + +# built hooks files +/dist +.env + +# Local Netlify folder +.netlify + +/src/styles/tailwind.output.css + +# Local network config file generated by @arbitrum/sdk +/src/util/localNetwork.json + +# Next.js +.next +next-env.d.ts diff --git a/packages/arb-token-bridge-ui/next.config.js b/packages/app/next.config.js similarity index 80% rename from packages/arb-token-bridge-ui/next.config.js rename to packages/app/next.config.js index 51f8167984..3c96daf957 100644 --- a/packages/arb-token-bridge-ui/next.config.js +++ b/packages/app/next.config.js @@ -8,6 +8,13 @@ module.exports = { distDir: 'build', productionBrowserSourceMaps: true, reactStrictMode: true, + experimental: { + externalDir: true + }, + webpack: config => { + config.externals.push('pino-pretty', 'lokijs', 'encoding') + return config + }, images: { remotePatterns: [ { @@ -36,7 +43,8 @@ module.exports = { async redirects() { return [ { - source: '/:slug((?!^$|api/|_next/|public/)(?!.*\\.[^/]+$).+)', + source: + '/:slug((?!^$|api/|_next/|public/|restricted)(?!.*\\.[^/]+$).+)', missing: [ { type: 'query', diff --git a/packages/app/package.json b/packages/app/package.json new file mode 100644 index 0000000000..ead631b499 --- /dev/null +++ b/packages/app/package.json @@ -0,0 +1,25 @@ +{ + "name": "app", + "version": "1.0.0", + "license": "Apache-2.0", + "private": true, + "dependencies": { + "next": "^14.2.32", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tippy.js": "^6.3.7", + "use-query-params": "^2.2.1", + "@rainbow-me/rainbowkit": "^2.2.4", + "react-toastify": "^9.1.1" + }, + "scripts": { + "predev": "yarn workspace arb-token-bridge-ui generateDenylist", + "dev": "next dev", + "prebuild": "yarn workspace arb-token-bridge-ui generateDenylist", + "build": "next build", + "start": "next start", + "lint": "tsc && eslint", + "lint:fix": "tsc && eslint --quiet --fix" + }, + "devDependencies": {} +} diff --git a/packages/arb-token-bridge-ui/postcss.config.js b/packages/app/postcss.config.js similarity index 100% rename from packages/arb-token-bridge-ui/postcss.config.js rename to packages/app/postcss.config.js diff --git a/packages/arb-token-bridge-ui/public/_headers b/packages/app/public/_headers similarity index 100% rename from packages/arb-token-bridge-ui/public/_headers rename to packages/app/public/_headers diff --git a/packages/arb-token-bridge-ui/public/_redirects b/packages/app/public/_redirects similarity index 100% rename from packages/arb-token-bridge-ui/public/_redirects rename to packages/app/public/_redirects diff --git a/packages/arb-token-bridge-ui/public/icons/arbitrum.svg b/packages/app/public/icons/arbitrum.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/icons/arbitrum.svg rename to packages/app/public/icons/arbitrum.svg diff --git a/packages/arb-token-bridge-ui/public/icons/bridge.svg b/packages/app/public/icons/bridge.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/icons/bridge.svg rename to packages/app/public/icons/bridge.svg diff --git a/packages/arb-token-bridge-ui/public/icons/gas.svg b/packages/app/public/icons/gas.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/icons/gas.svg rename to packages/app/public/icons/gas.svg diff --git a/packages/arb-token-bridge-ui/public/icons/history.svg b/packages/app/public/icons/history.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/icons/history.svg rename to packages/app/public/icons/history.svg diff --git a/packages/arb-token-bridge-ui/public/icons/layerzero.svg b/packages/app/public/icons/layerzero.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/icons/layerzero.svg rename to packages/app/public/icons/layerzero.svg diff --git a/packages/arb-token-bridge-ui/public/icons/lifi.svg b/packages/app/public/icons/lifi.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/icons/lifi.svg rename to packages/app/public/icons/lifi.svg diff --git a/packages/arb-token-bridge-ui/public/images/AlephZeroEVM_Logo.webp b/packages/app/public/images/AlephZeroEVM_Logo.webp similarity index 100% rename from packages/arb-token-bridge-ui/public/images/AlephZeroEVM_Logo.webp rename to packages/app/public/images/AlephZeroEVM_Logo.webp diff --git a/packages/arb-token-bridge-ui/public/images/ApeChainLogo.svg b/packages/app/public/images/ApeChainLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/ApeChainLogo.svg rename to packages/app/public/images/ApeChainLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/ApeTokenLogo.svg b/packages/app/public/images/ApeTokenLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/ApeTokenLogo.svg rename to packages/app/public/images/ApeTokenLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/ArbitrumLogo.svg b/packages/app/public/images/ArbitrumLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/ArbitrumLogo.svg rename to packages/app/public/images/ArbitrumLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/ArbitrumNovaLogo.svg b/packages/app/public/images/ArbitrumNovaLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/ArbitrumNovaLogo.svg rename to packages/app/public/images/ArbitrumNovaLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/ArbitrumOneLogo.svg b/packages/app/public/images/ArbitrumOneLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/ArbitrumOneLogo.svg rename to packages/app/public/images/ArbitrumOneLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/BaseWhite.svg b/packages/app/public/images/BaseWhite.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/BaseWhite.svg rename to packages/app/public/images/BaseWhite.svg diff --git a/packages/arb-token-bridge-ui/public/images/BirdLayer_Logo.png b/packages/app/public/images/BirdLayer_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/BirdLayer_Logo.png rename to packages/app/public/images/BirdLayer_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/CctpLogoColor.svg b/packages/app/public/images/CctpLogoColor.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/CctpLogoColor.svg rename to packages/app/public/images/CctpLogoColor.svg diff --git a/packages/arb-token-bridge-ui/public/images/ChainBounty_Logo.svg b/packages/app/public/images/ChainBounty_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/ChainBounty_Logo.svg rename to packages/app/public/images/ChainBounty_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/CheeseChain_Logo.png b/packages/app/public/images/CheeseChain_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/CheeseChain_Logo.png rename to packages/app/public/images/CheeseChain_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/CheeseChain_NativeTokenLogo.jpg b/packages/app/public/images/CheeseChain_NativeTokenLogo.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/CheeseChain_NativeTokenLogo.jpg rename to packages/app/public/images/CheeseChain_NativeTokenLogo.jpg diff --git a/packages/arb-token-bridge-ui/public/images/Conwai_Logo.svg b/packages/app/public/images/Conwai_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/Conwai_Logo.svg rename to packages/app/public/images/Conwai_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/Conwai_NativeTokenLogo.svg b/packages/app/public/images/Conwai_NativeTokenLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/Conwai_NativeTokenLogo.svg rename to packages/app/public/images/Conwai_NativeTokenLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/DODOchain.png b/packages/app/public/images/DODOchain.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/DODOchain.png rename to packages/app/public/images/DODOchain.png diff --git a/packages/arb-token-bridge-ui/public/images/DataLakeMainnet_Logo.png b/packages/app/public/images/DataLakeMainnet_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/DataLakeMainnet_Logo.png rename to packages/app/public/images/DataLakeMainnet_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/DataLakeMainnet_NativeTokenLogo.png b/packages/app/public/images/DataLakeMainnet_NativeTokenLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/DataLakeMainnet_NativeTokenLogo.png rename to packages/app/public/images/DataLakeMainnet_NativeTokenLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/EarnmMainnet_Logo.svg b/packages/app/public/images/EarnmMainnet_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/EarnmMainnet_Logo.svg rename to packages/app/public/images/EarnmMainnet_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/EarnmMainnet_NativeTokenLogo.svg b/packages/app/public/images/EarnmMainnet_NativeTokenLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/EarnmMainnet_NativeTokenLogo.svg rename to packages/app/public/images/EarnmMainnet_NativeTokenLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/EarnmSepolia_Logo.svg b/packages/app/public/images/EarnmSepolia_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/EarnmSepolia_Logo.svg rename to packages/app/public/images/EarnmSepolia_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/EarnmSepolia_NativeTokenLogo.jpg b/packages/app/public/images/EarnmSepolia_NativeTokenLogo.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/EarnmSepolia_NativeTokenLogo.jpg rename to packages/app/public/images/EarnmSepolia_NativeTokenLogo.jpg diff --git a/packages/arb-token-bridge-ui/public/images/EduChainLogo.png b/packages/app/public/images/EduChainLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/EduChainLogo.png rename to packages/app/public/images/EduChainLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/EduChainTokenLogo.png b/packages/app/public/images/EduChainTokenLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/EduChainTokenLogo.png rename to packages/app/public/images/EduChainTokenLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/EthereumLogo.svg b/packages/app/public/images/EthereumLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/EthereumLogo.svg rename to packages/app/public/images/EthereumLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/EthereumLogoRound.svg b/packages/app/public/images/EthereumLogoRound.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/EthereumLogoRound.svg rename to packages/app/public/images/EthereumLogoRound.svg diff --git a/packages/arb-token-bridge-ui/public/images/EthereumLogoRoundLight.svg b/packages/app/public/images/EthereumLogoRoundLight.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/EthereumLogoRoundLight.svg rename to packages/app/public/images/EthereumLogoRoundLight.svg diff --git a/packages/arb-token-bridge-ui/public/images/GalacticaCassiopeia_Logo.svg b/packages/app/public/images/GalacticaCassiopeia_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/GalacticaCassiopeia_Logo.svg rename to packages/app/public/images/GalacticaCassiopeia_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/GalacticaMainnet_Logo.svg b/packages/app/public/images/GalacticaMainnet_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/GalacticaMainnet_Logo.svg rename to packages/app/public/images/GalacticaMainnet_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/GoogleCalendar.svg b/packages/app/public/images/GoogleCalendar.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/GoogleCalendar.svg rename to packages/app/public/images/GoogleCalendar.svg diff --git a/packages/arb-token-bridge-ui/public/images/GravityAlpha_Logo.png b/packages/app/public/images/GravityAlpha_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/GravityAlpha_Logo.png rename to packages/app/public/images/GravityAlpha_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/GravityAlpha_NativeTokenLogo.png b/packages/app/public/images/GravityAlpha_NativeTokenLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/GravityAlpha_NativeTokenLogo.png rename to packages/app/public/images/GravityAlpha_NativeTokenLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/HPPMainnet_Logo.svg b/packages/app/public/images/HPPMainnet_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/HPPMainnet_Logo.svg rename to packages/app/public/images/HPPMainnet_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/HPPSepolia_Logo.svg b/packages/app/public/images/HPPSepolia_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/HPPSepolia_Logo.svg rename to packages/app/public/images/HPPSepolia_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/HumanityMainnet_Logo.svg b/packages/app/public/images/HumanityMainnet_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/HumanityMainnet_Logo.svg rename to packages/app/public/images/HumanityMainnet_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/HumanityTestnet_Logo.svg b/packages/app/public/images/HumanityTestnet_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/HumanityTestnet_Logo.svg rename to packages/app/public/images/HumanityTestnet_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/HumanityTestnet_NativeTokenLogo.svg b/packages/app/public/images/HumanityTestnet_NativeTokenLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/HumanityTestnet_NativeTokenLogo.svg rename to packages/app/public/images/HumanityTestnet_NativeTokenLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/Hychain_Logo.jpg b/packages/app/public/images/Hychain_Logo.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/Hychain_Logo.jpg rename to packages/app/public/images/Hychain_Logo.jpg diff --git a/packages/arb-token-bridge-ui/public/images/Hychain_NativeTokenLogo.jpg b/packages/app/public/images/Hychain_NativeTokenLogo.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/Hychain_NativeTokenLogo.jpg rename to packages/app/public/images/Hychain_NativeTokenLogo.jpg diff --git a/packages/arb-token-bridge-ui/public/images/JASMYChain_Logo.png b/packages/app/public/images/JASMYChain_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/JASMYChain_Logo.png rename to packages/app/public/images/JASMYChain_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/LayerZeroIcon.png b/packages/app/public/images/LayerZeroIcon.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/LayerZeroIcon.png rename to packages/app/public/images/LayerZeroIcon.png diff --git a/packages/arb-token-bridge-ui/public/images/LayerZeroLogo.svg b/packages/app/public/images/LayerZeroLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/LayerZeroLogo.svg rename to packages/app/public/images/LayerZeroLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/MeerChain_Logo.png b/packages/app/public/images/MeerChain_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/MeerChain_Logo.png rename to packages/app/public/images/MeerChain_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/MeerChain_NativeTokenLogo.png b/packages/app/public/images/MeerChain_NativeTokenLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/MeerChain_NativeTokenLogo.png rename to packages/app/public/images/MeerChain_NativeTokenLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/MindChainLogo.webp b/packages/app/public/images/MindChainLogo.webp similarity index 100% rename from packages/arb-token-bridge-ui/public/images/MindChainLogo.webp rename to packages/app/public/images/MindChainLogo.webp diff --git a/packages/arb-token-bridge-ui/public/images/MiracleChain_Logo.png b/packages/app/public/images/MiracleChain_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/MiracleChain_Logo.png rename to packages/app/public/images/MiracleChain_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/MiracleChain_NativeTokenLogo.png b/packages/app/public/images/MiracleChain_NativeTokenLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/MiracleChain_NativeTokenLogo.png rename to packages/app/public/images/MiracleChain_NativeTokenLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/Molten_Logo.png b/packages/app/public/images/Molten_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/Molten_Logo.png rename to packages/app/public/images/Molten_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/MusterLogo.svg b/packages/app/public/images/MusterLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/MusterLogo.svg rename to packages/app/public/images/MusterLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/NexusDevnet_Logo.png b/packages/app/public/images/NexusDevnet_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/NexusDevnet_Logo.png rename to packages/app/public/images/NexusDevnet_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/NexusDevnet_NativeTokenLogo.png b/packages/app/public/images/NexusDevnet_NativeTokenLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/NexusDevnet_NativeTokenLogo.png rename to packages/app/public/images/NexusDevnet_NativeTokenLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/NexusTestnetIII_Logo.png b/packages/app/public/images/NexusTestnetIII_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/NexusTestnetIII_Logo.png rename to packages/app/public/images/NexusTestnetIII_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/OpenLootSepolia_Logo.png b/packages/app/public/images/OpenLootSepolia_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/OpenLootSepolia_Logo.png rename to packages/app/public/images/OpenLootSepolia_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/OpenLootSepolia_NativeTokenLogo.png b/packages/app/public/images/OpenLootSepolia_NativeTokenLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/OpenLootSepolia_NativeTokenLogo.png rename to packages/app/public/images/OpenLootSepolia_NativeTokenLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/OrbitLogo.svg b/packages/app/public/images/OrbitLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/OrbitLogo.svg rename to packages/app/public/images/OrbitLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/PivotalTestnet_Logo.svg b/packages/app/public/images/PivotalTestnet_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/PivotalTestnet_Logo.svg rename to packages/app/public/images/PivotalTestnet_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/PlumeMainnet_Logo.svg b/packages/app/public/images/PlumeMainnet_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/PlumeMainnet_Logo.svg rename to packages/app/public/images/PlumeMainnet_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/PlumeMainnet_NativeTokenLogo.png b/packages/app/public/images/PlumeMainnet_NativeTokenLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/PlumeMainnet_NativeTokenLogo.png rename to packages/app/public/images/PlumeMainnet_NativeTokenLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/PlumeTestnet_Logo.svg b/packages/app/public/images/PlumeTestnet_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/PlumeTestnet_Logo.svg rename to packages/app/public/images/PlumeTestnet_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/PlumeTestnet_NativeTokenLogo.png b/packages/app/public/images/PlumeTestnet_NativeTokenLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/PlumeTestnet_NativeTokenLogo.png rename to packages/app/public/images/PlumeTestnet_NativeTokenLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/Plume_Logo.svg b/packages/app/public/images/Plume_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/Plume_Logo.svg rename to packages/app/public/images/Plume_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/PopApexLogo.webp b/packages/app/public/images/PopApexLogo.webp similarity index 100% rename from packages/arb-token-bridge-ui/public/images/PopApexLogo.webp rename to packages/app/public/images/PopApexLogo.webp diff --git a/packages/arb-token-bridge-ui/public/images/RARIMainnetLogo.svg b/packages/app/public/images/RARIMainnetLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/RARIMainnetLogo.svg rename to packages/app/public/images/RARIMainnetLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/RCADE_Logo.png b/packages/app/public/images/RCADE_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/RCADE_Logo.png rename to packages/app/public/images/RCADE_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/RoundedTab.svg b/packages/app/public/images/RoundedTab.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/RoundedTab.svg rename to packages/app/public/images/RoundedTab.svg diff --git a/packages/arb-token-bridge-ui/public/images/SXToronto_Logo.png b/packages/app/public/images/SXToronto_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/SXToronto_Logo.png rename to packages/app/public/images/SXToronto_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/SXToronto_NativeTokenLogo.png b/packages/app/public/images/SXToronto_NativeTokenLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/SXToronto_NativeTokenLogo.png rename to packages/app/public/images/SXToronto_NativeTokenLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/SankoLogo.png b/packages/app/public/images/SankoLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/SankoLogo.png rename to packages/app/public/images/SankoLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/SpotlightLogo.svg b/packages/app/public/images/SpotlightLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/SpotlightLogo.svg rename to packages/app/public/images/SpotlightLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/SuperpositionLogo.svg b/packages/app/public/images/SuperpositionLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/SuperpositionLogo.svg rename to packages/app/public/images/SuperpositionLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/T-REX_Logo.png b/packages/app/public/images/T-REX_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/T-REX_Logo.png rename to packages/app/public/images/T-REX_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/T-Rex_Logo.png b/packages/app/public/images/T-Rex_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/T-Rex_Logo.png rename to packages/app/public/images/T-Rex_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/WidgetTxHistoryIcon.svg b/packages/app/public/images/WidgetTxHistoryIcon.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/WidgetTxHistoryIcon.svg rename to packages/app/public/images/WidgetTxHistoryIcon.svg diff --git a/packages/arb-token-bridge-ui/public/images/WorldMobileChainMainnet_Logo.svg b/packages/app/public/images/WorldMobileChainMainnet_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/WorldMobileChainMainnet_Logo.svg rename to packages/app/public/images/WorldMobileChainMainnet_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/WorldMobileChainMainnet_NativeTokenLogo.svg b/packages/app/public/images/WorldMobileChainMainnet_NativeTokenLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/WorldMobileChainMainnet_NativeTokenLogo.svg rename to packages/app/public/images/WorldMobileChainMainnet_NativeTokenLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/WorldMobileTestnet_Logo.svg b/packages/app/public/images/WorldMobileTestnet_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/WorldMobileTestnet_Logo.svg rename to packages/app/public/images/WorldMobileTestnet_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/WorldMobileTestnet_NativeTokenLogo.svg b/packages/app/public/images/WorldMobileTestnet_NativeTokenLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/WorldMobileTestnet_NativeTokenLogo.svg rename to packages/app/public/images/WorldMobileTestnet_NativeTokenLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/WylerchainSepolia_Logo.png b/packages/app/public/images/WylerchainSepolia_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/WylerchainSepolia_Logo.png rename to packages/app/public/images/WylerchainSepolia_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/XCHAINLogo.png b/packages/app/public/images/XCHAINLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/XCHAINLogo.png rename to packages/app/public/images/XCHAINLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/XMTPMainnet_Logo.png b/packages/app/public/images/XMTPMainnet_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/XMTPMainnet_Logo.png rename to packages/app/public/images/XMTPMainnet_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/XProtocolSepolia_Logo.svg b/packages/app/public/images/XProtocolSepolia_Logo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/XProtocolSepolia_Logo.svg rename to packages/app/public/images/XProtocolSepolia_Logo.svg diff --git a/packages/arb-token-bridge-ui/public/images/XaiLogo.svg b/packages/app/public/images/XaiLogo.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/XaiLogo.svg rename to packages/app/public/images/XaiLogo.svg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/.gitkeep b/packages/app/public/images/__auto-generated/open-graph/.gitkeep similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/.gitkeep rename to packages/app/public/images/__auto-generated/open-graph/.gitkeep diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1-to-42161.jpg b/packages/app/public/images/__auto-generated/open-graph/1-to-42161.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1-to-42161.jpg rename to packages/app/public/images/__auto-generated/open-graph/1-to-42161.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1-to-42170.jpg b/packages/app/public/images/__auto-generated/open-graph/1-to-42170.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1-to-42170.jpg rename to packages/app/public/images/__auto-generated/open-graph/1-to-42170.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/10058111.jpg b/packages/app/public/images/__auto-generated/open-graph/10058111.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/10058111.jpg rename to packages/app/public/images/__auto-generated/open-graph/10058111.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/10058112.jpg b/packages/app/public/images/__auto-generated/open-graph/10058112.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/10058112.jpg rename to packages/app/public/images/__auto-generated/open-graph/10058112.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/101069.jpg b/packages/app/public/images/__auto-generated/open-graph/101069.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/101069.jpg rename to packages/app/public/images/__auto-generated/open-graph/101069.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/11155111-to-421614.jpg b/packages/app/public/images/__auto-generated/open-graph/11155111-to-421614.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/11155111-to-421614.jpg rename to packages/app/public/images/__auto-generated/open-graph/11155111-to-421614.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/132766.jpg b/packages/app/public/images/__auto-generated/open-graph/132766.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/132766.jpg rename to packages/app/public/images/__auto-generated/open-graph/132766.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1380012617.jpg b/packages/app/public/images/__auto-generated/open-graph/1380012617.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1380012617.jpg rename to packages/app/public/images/__auto-generated/open-graph/1380012617.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/140.jpg b/packages/app/public/images/__auto-generated/open-graph/140.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/140.jpg rename to packages/app/public/images/__auto-generated/open-graph/140.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/149.jpg b/packages/app/public/images/__auto-generated/open-graph/149.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/149.jpg rename to packages/app/public/images/__auto-generated/open-graph/149.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1625.jpg b/packages/app/public/images/__auto-generated/open-graph/1625.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1625.jpg rename to packages/app/public/images/__auto-generated/open-graph/1625.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1628.jpg b/packages/app/public/images/__auto-generated/open-graph/1628.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1628.jpg rename to packages/app/public/images/__auto-generated/open-graph/1628.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1712.jpg b/packages/app/public/images/__auto-generated/open-graph/1712.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1712.jpg rename to packages/app/public/images/__auto-generated/open-graph/1712.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/181228.jpg b/packages/app/public/images/__auto-generated/open-graph/181228.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/181228.jpg rename to packages/app/public/images/__auto-generated/open-graph/181228.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/190415.jpg b/packages/app/public/images/__auto-generated/open-graph/190415.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/190415.jpg rename to packages/app/public/images/__auto-generated/open-graph/190415.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1918988905.jpg b/packages/app/public/images/__auto-generated/open-graph/1918988905.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1918988905.jpg rename to packages/app/public/images/__auto-generated/open-graph/1918988905.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1962.jpg b/packages/app/public/images/__auto-generated/open-graph/1962.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1962.jpg rename to packages/app/public/images/__auto-generated/open-graph/1962.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1996.jpg b/packages/app/public/images/__auto-generated/open-graph/1996.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/1996.jpg rename to packages/app/public/images/__auto-generated/open-graph/1996.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/2022091.jpg b/packages/app/public/images/__auto-generated/open-graph/2022091.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/2022091.jpg rename to packages/app/public/images/__auto-generated/open-graph/2022091.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/228.jpg b/packages/app/public/images/__auto-generated/open-graph/228.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/228.jpg rename to packages/app/public/images/__auto-generated/open-graph/228.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/241320161.jpg b/packages/app/public/images/__auto-generated/open-graph/241320161.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/241320161.jpg rename to packages/app/public/images/__auto-generated/open-graph/241320161.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/241320162.jpg b/packages/app/public/images/__auto-generated/open-graph/241320162.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/241320162.jpg rename to packages/app/public/images/__auto-generated/open-graph/241320162.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/2730.jpg b/packages/app/public/images/__auto-generated/open-graph/2730.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/2730.jpg rename to packages/app/public/images/__auto-generated/open-graph/2730.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/2911.jpg b/packages/app/public/images/__auto-generated/open-graph/2911.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/2911.jpg rename to packages/app/public/images/__auto-generated/open-graph/2911.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/323432.jpg b/packages/app/public/images/__auto-generated/open-graph/323432.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/323432.jpg rename to packages/app/public/images/__auto-generated/open-graph/323432.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/32766.jpg b/packages/app/public/images/__auto-generated/open-graph/32766.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/32766.jpg rename to packages/app/public/images/__auto-generated/open-graph/32766.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/33139.jpg b/packages/app/public/images/__auto-generated/open-graph/33139.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/33139.jpg rename to packages/app/public/images/__auto-generated/open-graph/33139.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/360.jpg b/packages/app/public/images/__auto-generated/open-graph/360.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/360.jpg rename to packages/app/public/images/__auto-generated/open-graph/360.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/37714555429.jpg b/packages/app/public/images/__auto-generated/open-graph/37714555429.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/37714555429.jpg rename to packages/app/public/images/__auto-generated/open-graph/37714555429.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/383353.jpg b/packages/app/public/images/__auto-generated/open-graph/383353.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/383353.jpg rename to packages/app/public/images/__auto-generated/open-graph/383353.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/393.jpg b/packages/app/public/images/__auto-generated/open-graph/393.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/393.jpg rename to packages/app/public/images/__auto-generated/open-graph/393.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/394.jpg b/packages/app/public/images/__auto-generated/open-graph/394.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/394.jpg rename to packages/app/public/images/__auto-generated/open-graph/394.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/3940.jpg b/packages/app/public/images/__auto-generated/open-graph/3940.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/3940.jpg rename to packages/app/public/images/__auto-generated/open-graph/3940.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/4078.jpg b/packages/app/public/images/__auto-generated/open-graph/4078.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/4078.jpg rename to packages/app/public/images/__auto-generated/open-graph/4078.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/41455.jpg b/packages/app/public/images/__auto-generated/open-graph/41455.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/41455.jpg rename to packages/app/public/images/__auto-generated/open-graph/41455.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/4162.jpg b/packages/app/public/images/__auto-generated/open-graph/4162.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/4162.jpg rename to packages/app/public/images/__auto-generated/open-graph/4162.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/41923.jpg b/packages/app/public/images/__auto-generated/open-graph/41923.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/41923.jpg rename to packages/app/public/images/__auto-generated/open-graph/41923.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/42161-to-1.jpg b/packages/app/public/images/__auto-generated/open-graph/42161-to-1.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/42161-to-1.jpg rename to packages/app/public/images/__auto-generated/open-graph/42161-to-1.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/421614-to-11155111.jpg b/packages/app/public/images/__auto-generated/open-graph/421614-to-11155111.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/421614-to-11155111.jpg rename to packages/app/public/images/__auto-generated/open-graph/421614-to-11155111.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/42170-to-1.jpg b/packages/app/public/images/__auto-generated/open-graph/42170-to-1.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/42170-to-1.jpg rename to packages/app/public/images/__auto-generated/open-graph/42170-to-1.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/510.jpg b/packages/app/public/images/__auto-generated/open-graph/510.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/510.jpg rename to packages/app/public/images/__auto-generated/open-graph/510.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/5113.jpg b/packages/app/public/images/__auto-generated/open-graph/5113.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/5113.jpg rename to packages/app/public/images/__auto-generated/open-graph/5113.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/51828.jpg b/packages/app/public/images/__auto-generated/open-graph/51828.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/51828.jpg rename to packages/app/public/images/__auto-generated/open-graph/51828.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/53456.jpg b/packages/app/public/images/__auto-generated/open-graph/53456.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/53456.jpg rename to packages/app/public/images/__auto-generated/open-graph/53456.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/53457.jpg b/packages/app/public/images/__auto-generated/open-graph/53457.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/53457.jpg rename to packages/app/public/images/__auto-generated/open-graph/53457.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/55244.jpg b/packages/app/public/images/__auto-generated/open-graph/55244.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/55244.jpg rename to packages/app/public/images/__auto-generated/open-graph/55244.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/613419.jpg b/packages/app/public/images/__auto-generated/open-graph/613419.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/613419.jpg rename to packages/app/public/images/__auto-generated/open-graph/613419.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/63157.jpg b/packages/app/public/images/__auto-generated/open-graph/63157.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/63157.jpg rename to packages/app/public/images/__auto-generated/open-graph/63157.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/631571.jpg b/packages/app/public/images/__auto-generated/open-graph/631571.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/631571.jpg rename to packages/app/public/images/__auto-generated/open-graph/631571.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/660279.jpg b/packages/app/public/images/__auto-generated/open-graph/660279.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/660279.jpg rename to packages/app/public/images/__auto-generated/open-graph/660279.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/668668.jpg b/packages/app/public/images/__auto-generated/open-graph/668668.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/668668.jpg rename to packages/app/public/images/__auto-generated/open-graph/668668.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/681.jpg b/packages/app/public/images/__auto-generated/open-graph/681.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/681.jpg rename to packages/app/public/images/__auto-generated/open-graph/681.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/6985385.jpg b/packages/app/public/images/__auto-generated/open-graph/6985385.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/6985385.jpg rename to packages/app/public/images/__auto-generated/open-graph/6985385.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/70700.jpg b/packages/app/public/images/__auto-generated/open-graph/70700.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/70700.jpg rename to packages/app/public/images/__auto-generated/open-graph/70700.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/70701.jpg b/packages/app/public/images/__auto-generated/open-graph/70701.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/70701.jpg rename to packages/app/public/images/__auto-generated/open-graph/70701.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/7080969.jpg b/packages/app/public/images/__auto-generated/open-graph/7080969.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/7080969.jpg rename to packages/app/public/images/__auto-generated/open-graph/7080969.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/749.jpg b/packages/app/public/images/__auto-generated/open-graph/749.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/749.jpg rename to packages/app/public/images/__auto-generated/open-graph/749.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/79479957.jpg b/packages/app/public/images/__auto-generated/open-graph/79479957.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/79479957.jpg rename to packages/app/public/images/__auto-generated/open-graph/79479957.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/83868.jpg b/packages/app/public/images/__auto-generated/open-graph/83868.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/83868.jpg rename to packages/app/public/images/__auto-generated/open-graph/83868.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/843843.jpg b/packages/app/public/images/__auto-generated/open-graph/843843.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/843843.jpg rename to packages/app/public/images/__auto-generated/open-graph/843843.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/869.jpg b/packages/app/public/images/__auto-generated/open-graph/869.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/869.jpg rename to packages/app/public/images/__auto-generated/open-graph/869.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/88899.jpg b/packages/app/public/images/__auto-generated/open-graph/88899.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/88899.jpg rename to packages/app/public/images/__auto-generated/open-graph/88899.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/888991.jpg b/packages/app/public/images/__auto-generated/open-graph/888991.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/888991.jpg rename to packages/app/public/images/__auto-generated/open-graph/888991.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/905905.jpg b/packages/app/public/images/__auto-generated/open-graph/905905.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/905905.jpg rename to packages/app/public/images/__auto-generated/open-graph/905905.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/9116.jpg b/packages/app/public/images/__auto-generated/open-graph/9116.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/9116.jpg rename to packages/app/public/images/__auto-generated/open-graph/9116.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/92278.jpg b/packages/app/public/images/__auto-generated/open-graph/92278.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/92278.jpg rename to packages/app/public/images/__auto-generated/open-graph/92278.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/94524.jpg b/packages/app/public/images/__auto-generated/open-graph/94524.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/94524.jpg rename to packages/app/public/images/__auto-generated/open-graph/94524.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/98215.jpg b/packages/app/public/images/__auto-generated/open-graph/98215.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/98215.jpg rename to packages/app/public/images/__auto-generated/open-graph/98215.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/98864.jpg b/packages/app/public/images/__auto-generated/open-graph/98864.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/98864.jpg rename to packages/app/public/images/__auto-generated/open-graph/98864.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/98865.jpg b/packages/app/public/images/__auto-generated/open-graph/98865.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/98865.jpg rename to packages/app/public/images/__auto-generated/open-graph/98865.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/98866.jpg b/packages/app/public/images/__auto-generated/open-graph/98866.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/98866.jpg rename to packages/app/public/images/__auto-generated/open-graph/98866.jpg diff --git a/packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/98867.jpg b/packages/app/public/images/__auto-generated/open-graph/98867.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/__auto-generated/open-graph/98867.jpg rename to packages/app/public/images/__auto-generated/open-graph/98867.jpg diff --git a/packages/arb-token-bridge-ui/public/images/arbinaut-fixing-spaceship.webp b/packages/app/public/images/arbinaut-fixing-spaceship.webp similarity index 100% rename from packages/arb-token-bridge-ui/public/images/arbinaut-fixing-spaceship.webp rename to packages/app/public/images/arbinaut-fixing-spaceship.webp diff --git a/packages/arb-token-bridge-ui/public/images/arrows.svg b/packages/app/public/images/arrows.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/arrows.svg rename to packages/app/public/images/arrows.svg diff --git a/packages/arb-token-bridge-ui/public/images/bridge/across.png b/packages/app/public/images/bridge/across.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/bridge/across.png rename to packages/app/public/images/bridge/across.png diff --git a/packages/arb-token-bridge-ui/public/images/bridge/celer.png b/packages/app/public/images/bridge/celer.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/bridge/celer.png rename to packages/app/public/images/bridge/celer.png diff --git a/packages/arb-token-bridge-ui/public/images/bridge/connext.png b/packages/app/public/images/bridge/connext.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/bridge/connext.png rename to packages/app/public/images/bridge/connext.png diff --git a/packages/arb-token-bridge-ui/public/images/bridge/deBridge.svg b/packages/app/public/images/bridge/deBridge.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/bridge/deBridge.svg rename to packages/app/public/images/bridge/deBridge.svg diff --git a/packages/arb-token-bridge-ui/public/images/bridge/hop.png b/packages/app/public/images/bridge/hop.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/bridge/hop.png rename to packages/app/public/images/bridge/hop.png diff --git a/packages/arb-token-bridge-ui/public/images/bridge/lifi.webp b/packages/app/public/images/bridge/lifi.webp similarity index 100% rename from packages/arb-token-bridge-ui/public/images/bridge/lifi.webp rename to packages/app/public/images/bridge/lifi.webp diff --git a/packages/arb-token-bridge-ui/public/images/bridge/router.webp b/packages/app/public/images/bridge/router.webp similarity index 100% rename from packages/arb-token-bridge-ui/public/images/bridge/router.webp rename to packages/app/public/images/bridge/router.webp diff --git a/packages/arb-token-bridge-ui/public/images/bridge/stargate.png b/packages/app/public/images/bridge/stargate.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/bridge/stargate.png rename to packages/app/public/images/bridge/stargate.png diff --git a/packages/arb-token-bridge-ui/public/images/bridge/synapse.png b/packages/app/public/images/bridge/synapse.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/bridge/synapse.png rename to packages/app/public/images/bridge/synapse.png diff --git a/packages/arb-token-bridge-ui/public/images/bridge/wormhole.svg b/packages/app/public/images/bridge/wormhole.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/bridge/wormhole.svg rename to packages/app/public/images/bridge/wormhole.svg diff --git a/packages/arb-token-bridge-ui/public/images/copy.svg b/packages/app/public/images/copy.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/copy.svg rename to packages/app/public/images/copy.svg diff --git a/packages/arb-token-bridge-ui/public/images/eclipse_bottom.png b/packages/app/public/images/eclipse_bottom.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/eclipse_bottom.png rename to packages/app/public/images/eclipse_bottom.png diff --git a/packages/arb-token-bridge-ui/public/images/lists/ArbitrumLogo.png b/packages/app/public/images/lists/ArbitrumLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/lists/ArbitrumLogo.png rename to packages/app/public/images/lists/ArbitrumLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/lists/cmc.png b/packages/app/public/images/lists/cmc.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/lists/cmc.png rename to packages/app/public/images/lists/cmc.png diff --git a/packages/arb-token-bridge-ui/public/images/lists/coinGecko.svg b/packages/app/public/images/lists/coinGecko.svg similarity index 100% rename from packages/arb-token-bridge-ui/public/images/lists/coinGecko.svg rename to packages/app/public/images/lists/coinGecko.svg diff --git a/packages/arb-token-bridge-ui/public/images/lists/uniswap.png b/packages/app/public/images/lists/uniswap.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/lists/uniswap.png rename to packages/app/public/images/lists/uniswap.png diff --git a/packages/arb-token-bridge-ui/public/images/sxLogo.png b/packages/app/public/images/sxLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/sxLogo.png rename to packages/app/public/images/sxLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/sxTokenLogo.png b/packages/app/public/images/sxTokenLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/sxTokenLogo.png rename to packages/app/public/images/sxTokenLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/unite-mainnet_Logo.png b/packages/app/public/images/unite-mainnet_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/unite-mainnet_Logo.png rename to packages/app/public/images/unite-mainnet_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/unite-testnet_Logo.png b/packages/app/public/images/unite-testnet_Logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/unite-testnet_Logo.png rename to packages/app/public/images/unite-testnet_Logo.png diff --git a/packages/arb-token-bridge-ui/public/images/xrLogo.png b/packages/app/public/images/xrLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/xrLogo.png rename to packages/app/public/images/xrLogo.png diff --git a/packages/arb-token-bridge-ui/public/images/xrTokenLogo.png b/packages/app/public/images/xrTokenLogo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/images/xrTokenLogo.png rename to packages/app/public/images/xrTokenLogo.png diff --git a/packages/arb-token-bridge-ui/public/logo.png b/packages/app/public/logo.png similarity index 100% rename from packages/arb-token-bridge-ui/public/logo.png rename to packages/app/public/logo.png diff --git a/packages/arb-token-bridge-ui/public/manifest.json b/packages/app/public/manifest.json similarity index 67% rename from packages/arb-token-bridge-ui/public/manifest.json rename to packages/app/public/manifest.json index 71e88f2251..32044fd265 100644 --- a/packages/arb-token-bridge-ui/public/manifest.json +++ b/packages/app/public/manifest.json @@ -3,9 +3,9 @@ "name": "Arbitrum Token Bridge", "icons": [ { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" + "src": "logo.png", + "sizes": "512x512", + "type": "image/png" } ], "start_url": ".", diff --git a/packages/arb-token-bridge-ui/public/og-image.jpg b/packages/app/public/og-image.jpg similarity index 100% rename from packages/arb-token-bridge-ui/public/og-image.jpg rename to packages/app/public/og-image.jpg diff --git a/packages/arb-token-bridge-ui/public/robots.txt b/packages/app/public/robots.txt similarity index 100% rename from packages/arb-token-bridge-ui/public/robots.txt rename to packages/app/public/robots.txt diff --git a/packages/arb-token-bridge-ui/public/tokenLists/33139_lifi.json b/packages/app/public/tokenLists/33139_lifi.json similarity index 100% rename from packages/arb-token-bridge-ui/public/tokenLists/33139_lifi.json rename to packages/app/public/tokenLists/33139_lifi.json diff --git a/packages/arb-token-bridge-ui/public/tokenLists/42161_lifi.json b/packages/app/public/tokenLists/42161_lifi.json similarity index 100% rename from packages/arb-token-bridge-ui/public/tokenLists/42161_lifi.json rename to packages/app/public/tokenLists/42161_lifi.json diff --git a/packages/arb-token-bridge-ui/public/tokenLists/55244_lifi.json b/packages/app/public/tokenLists/55244_lifi.json similarity index 100% rename from packages/arb-token-bridge-ui/public/tokenLists/55244_lifi.json rename to packages/app/public/tokenLists/55244_lifi.json diff --git a/packages/arb-token-bridge-ui/public/tokenLists/660279_default.json b/packages/app/public/tokenLists/660279_default.json similarity index 100% rename from packages/arb-token-bridge-ui/public/tokenLists/660279_default.json rename to packages/app/public/tokenLists/660279_default.json diff --git a/packages/app/src/app/(bridge)/BridgeClient.tsx b/packages/app/src/app/(bridge)/BridgeClient.tsx new file mode 100644 index 0000000000..9d5703554e --- /dev/null +++ b/packages/app/src/app/(bridge)/BridgeClient.tsx @@ -0,0 +1,55 @@ +'use client' +import { + isE2eTestingEnvironment, + isProductionEnvironment +} from '@/bridge/util/CommonUtils' +import { initializeSentry } from '@/bridge/util/SentryUtils' +import posthog from 'posthog-js' +import { + addOrbitChainsToArbitrumSDK, + initializeDayjs +} from '../../initialization' +import { ComponentType } from 'react' +import { registerLocalNetwork } from '@/bridge/util/networks' +import dynamic from 'next/dynamic' + +// Configure dayjs plugins +initializeDayjs() + +// Initialize Sentry for error tracking +initializeSentry(process.env.NEXT_PUBLIC_SENTRY_DSN) + +// Initialize PostHog +if (typeof process.env.NEXT_PUBLIC_POSTHOG_KEY === 'string') { + posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { + api_host: 'https://app.posthog.com', + loaded: posthog => { + if (!isProductionEnvironment) { + // when in dev, you can see data that would be sent in prod (in devtools) + posthog.debug() + } + }, + // store data in temporary memory that expires with each session + persistence: 'memory', + // by default posthog autocaptures (sends) events such as onClick, etc + // we set up our own events instead + autocapture: false, + disable_session_recording: true + }) +} + +const App = dynamic(() => { + return new Promise<{ default: ComponentType }>(async resolve => { + if (!isProductionEnvironment || isE2eTestingEnvironment) { + await registerLocalNetwork() + } + + addOrbitChainsToArbitrumSDK() + const AppComponent = await import('@/bridge/components/App/App') + resolve(AppComponent) + }) +}) + +export default function Index() { + return +} diff --git a/packages/app/src/app/(bridge)/page.tsx b/packages/app/src/app/(bridge)/page.tsx new file mode 100644 index 0000000000..c71b79b130 --- /dev/null +++ b/packages/app/src/app/(bridge)/page.tsx @@ -0,0 +1,255 @@ +import { redirect } from 'next/navigation' +import type { Metadata } from 'next' +import { + ChainKeyQueryParam, + getChainForChainKeyQueryParam +} from '@/bridge/types/ChainQueryParam' + +import { isNetwork, registerLocalNetwork } from '@/bridge/util/networks' +import BridgeClient from './BridgeClient' +import { + decodeChainQueryParam, + encodeChainQueryParam, + DisabledFeaturesParam, + ModeParamEnum, + sanitizeQueryParams, + encodeString, + sanitizeTokenQueryParam, + sanitizeTabQueryParam +} from '@/bridge/util/queryParamUtils' +import { sanitizeExperimentalFeaturesQueryParam } from '@/bridge/util' +import { + isE2eTestingEnvironment, + isProductionEnvironment +} from '@/bridge/util/CommonUtils' +import { addOrbitChainsToArbitrumSDK } from '../../initialization' + +import 'tippy.js/dist/tippy.css' +import 'tippy.js/themes/light.css' +import '@rainbow-me/rainbowkit/styles.css' +import 'react-toastify/dist/ReactToastify.css' + +type Props = { + searchParams: { [key: string]: string | string[] | undefined } +} + +export async function generateMetadata({ + searchParams +}: Props): Promise { + const sourceChainSlug = ( + typeof searchParams.sourceChain === 'string' + ? searchParams.sourceChain + : 'ethereum' + ) as ChainKeyQueryParam + const destinationChainSlug = ( + typeof searchParams.destinationChain === 'string' + ? searchParams.destinationChain + : 'arbitrum-one' + ) as ChainKeyQueryParam + + let sourceChainInfo + let destinationChainInfo + + try { + sourceChainInfo = getChainForChainKeyQueryParam(sourceChainSlug) + destinationChainInfo = getChainForChainKeyQueryParam(destinationChainSlug) + } catch (error) { + sourceChainInfo = getChainForChainKeyQueryParam('ethereum') + destinationChainInfo = getChainForChainKeyQueryParam('arbitrum-one') + } + + const { isOrbitChain: isSourceOrbitChain } = isNetwork(sourceChainInfo.id) + const { isOrbitChain: isDestinationOrbitChain } = isNetwork( + destinationChainInfo.id + ) + + const siteTitle = `Bridge to ${destinationChainInfo.name}` + const siteDescription = `Bridge from ${sourceChainInfo.name} to ${destinationChainInfo.name} using the Arbitrum Bridge. Built to scale Ethereum, Arbitrum brings you 10x lower costs while inheriting Ethereum's security model. Arbitrum is a Layer 2 Optimistic Rollup.` + const siteDomain = 'https://bridge.arbitrum.io' + + let metaImagePath = `${sourceChainInfo.id}-to-${destinationChainInfo.id}.jpg` + + if (isSourceOrbitChain) { + metaImagePath = `${sourceChainInfo.id}.jpg` + } + + if (isDestinationOrbitChain) { + metaImagePath = `${destinationChainInfo.id}.jpg` + } + + const imageUrl = `${siteDomain}/images/__auto-generated/open-graph/${metaImagePath}` + + return { + title: siteTitle, + description: siteDescription, + openGraph: { + url: siteDomain, + type: 'website', + title: siteTitle, + description: siteDescription, + images: [imageUrl] + }, + twitter: { + card: 'summary_large_image', + site: siteDomain, + title: siteTitle, + description: siteDescription, + images: [imageUrl] + } + } +} + +function getDestinationWithSanitizedQueryParams( + sanitized: { + sourceChainId: number + destinationChainId: number + experiments: string | undefined + token: string | undefined + tab: string + disabledFeatures: string[] | undefined + mode: string | undefined + }, + query: Record +) { + const params = new URLSearchParams() + + for (const key in query) { + // don't copy "sourceChain" and "destinationChain" query params + if ( + key === 'sourceChain' || + key === 'destinationChain' || + key === 'experiments' || + key === 'token' || + key === 'tab' || + key === 'disabledFeatures' || + key === 'mode' + ) { + continue + } + + const value = query[key] + + // copy everything else + if (typeof value === 'string') { + params.set(key, value) + } + } + + const encodedSource = encodeChainQueryParam(sanitized.sourceChainId) + const encodedDestination = encodeChainQueryParam(sanitized.destinationChainId) + const encodedExperiments = encodeString(sanitized.experiments) + const encodedToken = encodeString(sanitized.token) + const encodedTab = encodeString(sanitized.tab) + const encodedMode = encodeString(sanitized.mode) + + if (encodedSource) { + params.set('sourceChain', encodedSource) + + if (encodedDestination) { + params.set('destinationChain', encodedDestination) + } + } + + if (encodedExperiments) { + params.set('experiments', encodedExperiments) + } + + if (encodedToken) { + params.set('token', encodedToken) + } + + if (encodedTab) { + params.set('tab', encodedTab) + } + + if (encodedMode) { + params.set('mode', encodedMode) + } + + if (sanitized.disabledFeatures) { + for (const disabledFeature of sanitized.disabledFeatures) { + params.append('disabledFeatures', disabledFeature) + } + } + + return `/?${params.toString()}` +} + +async function sanitizeAndRedirect(searchParams: { + [key: string]: string | string[] | undefined +}) { + const sourceChainId = decodeChainQueryParam(searchParams.sourceChain) + const destinationChainId = decodeChainQueryParam( + searchParams.destinationChain + ) + const experiments = + typeof searchParams.experiments === 'string' + ? searchParams.experiments + : undefined + const token = + typeof searchParams.token === 'string' ? searchParams.token : undefined + const tab = + typeof searchParams.tab === 'string' ? searchParams.tab : undefined + const mode = + typeof searchParams.mode === 'string' ? searchParams.mode : undefined + const disabledFeatures = + typeof searchParams.disabledFeatures === 'string' + ? [searchParams.disabledFeatures] + : searchParams.disabledFeatures + + // If both sourceChain and destinationChain are not present, let the client sync with Metamask + if (!sourceChainId && !destinationChainId) { + return + } + + if (!isProductionEnvironment || isE2eTestingEnvironment) { + await registerLocalNetwork() + } + + const sanitizedChainIds = sanitizeQueryParams({ + sourceChainId, + destinationChainId, + disableTransfersToNonArbitrumChains: mode === ModeParamEnum.EMBED + }) + const sanitized = { + ...sanitizedChainIds, + experiments: sanitizeExperimentalFeaturesQueryParam(experiments), + token: sanitizeTokenQueryParam({ + token, + sourceChainId: sanitizedChainIds.sourceChainId, + destinationChainId: sanitizedChainIds.destinationChainId + }), + tab: sanitizeTabQueryParam(tab), + disabledFeatures: DisabledFeaturesParam.decode(disabledFeatures), + mode: mode ? mode : undefined + } + + // if the sanitized query params are different from the initial values, redirect to the url with sanitized query params + if ( + sourceChainId !== sanitized.sourceChainId || + destinationChainId !== sanitized.destinationChainId || + experiments !== sanitized.experiments || + token !== sanitized.token || + tab !== sanitized.tab || + (disabledFeatures?.length || 0) !== + (sanitized.disabledFeatures?.length || 0) || + mode !== sanitized.mode + ) { + console.log(`[sanitizeAndRedirect] sanitizing query params`) + console.log( + `[sanitizeAndRedirect] sourceChain=${sourceChainId}&destinationChain=${destinationChainId}&experiments=${experiments}&token=${token}&tab=${tab}&disabledFeatures=${disabledFeatures}&mode=${mode} (before)` + ) + console.log( + `[sanitizeAndRedirect] sourceChain=${sanitized.sourceChainId}&destinationChain=${sanitized.destinationChainId}&experiments=${sanitized.experiments}&token=${sanitized.token}&tab=${sanitized.tab}&disabledFeatures=${sanitized.disabledFeatures}&mode=${sanitized.mode} (after)` + ) + + redirect(getDestinationWithSanitizedQueryParams(sanitized, searchParams)) + } +} + +export default async function HomePage({ searchParams }: Props) { + addOrbitChainsToArbitrumSDK() + await sanitizeAndRedirect(searchParams) + + return +} diff --git a/packages/app/src/app/api/cctp/[type]/route.ts b/packages/app/src/app/api/cctp/[type]/route.ts new file mode 100644 index 0000000000..ef70bf4223 --- /dev/null +++ b/packages/app/src/app/api/cctp/[type]/route.ts @@ -0,0 +1 @@ +export { GET } from '@/bridge/app/api/cctp/[type]' diff --git a/packages/app/src/app/api/chains/[chainId]/block-number/route.ts b/packages/app/src/app/api/chains/[chainId]/block-number/route.ts new file mode 100644 index 0000000000..1304550e83 --- /dev/null +++ b/packages/app/src/app/api/chains/[chainId]/block-number/route.ts @@ -0,0 +1 @@ +export { GET } from '@/bridge/app/api/chains/[chainId]/block-number' diff --git a/packages/app/src/app/api/crosschain-transfers/lifi/route.ts b/packages/app/src/app/api/crosschain-transfers/lifi/route.ts new file mode 100644 index 0000000000..2a142355d3 --- /dev/null +++ b/packages/app/src/app/api/crosschain-transfers/lifi/route.ts @@ -0,0 +1 @@ +export { GET } from '@/bridge/app/api/crosschain-transfers/lifi' diff --git a/packages/app/src/app/api/denylist/route.ts b/packages/app/src/app/api/denylist/route.ts new file mode 100644 index 0000000000..de8e26862a --- /dev/null +++ b/packages/app/src/app/api/denylist/route.ts @@ -0,0 +1 @@ +export { GET } from '@/bridge/app/api/denylist' diff --git a/packages/app/src/app/api/deposits/route.ts b/packages/app/src/app/api/deposits/route.ts new file mode 100644 index 0000000000..d8a2df4644 --- /dev/null +++ b/packages/app/src/app/api/deposits/route.ts @@ -0,0 +1 @@ +export { GET } from '@/bridge/app/api/deposits' diff --git a/packages/app/src/app/api/eth-deposits-custom-destination/route.ts b/packages/app/src/app/api/eth-deposits-custom-destination/route.ts new file mode 100644 index 0000000000..71253c563c --- /dev/null +++ b/packages/app/src/app/api/eth-deposits-custom-destination/route.ts @@ -0,0 +1 @@ +export { GET } from '@/bridge/app/api/eth-deposits-custom-destination' diff --git a/packages/app/src/app/api/status/route.ts b/packages/app/src/app/api/status/route.ts new file mode 100644 index 0000000000..dc1e6a9bca --- /dev/null +++ b/packages/app/src/app/api/status/route.ts @@ -0,0 +1 @@ +export { GET } from '@/bridge/app/api/status' diff --git a/packages/app/src/app/api/teleports/erc20/route.ts b/packages/app/src/app/api/teleports/erc20/route.ts new file mode 100644 index 0000000000..672232decc --- /dev/null +++ b/packages/app/src/app/api/teleports/erc20/route.ts @@ -0,0 +1 @@ +export { GET } from '@/bridge/app/api/teleports/erc20' diff --git a/packages/app/src/app/api/teleports/eth/route.ts b/packages/app/src/app/api/teleports/eth/route.ts new file mode 100644 index 0000000000..a68cd0a19a --- /dev/null +++ b/packages/app/src/app/api/teleports/eth/route.ts @@ -0,0 +1 @@ +export { GET } from '@/bridge/app/api/teleports/eth' diff --git a/packages/app/src/app/api/withdrawals/route.ts b/packages/app/src/app/api/withdrawals/route.ts new file mode 100644 index 0000000000..7c5ed9775a --- /dev/null +++ b/packages/app/src/app/api/withdrawals/route.ts @@ -0,0 +1 @@ +export { GET } from '@/bridge/app/api/withdrawals' diff --git a/packages/app/src/app/layout.tsx b/packages/app/src/app/layout.tsx new file mode 100644 index 0000000000..249781aeab --- /dev/null +++ b/packages/app/src/app/layout.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from 'next' +import { PropsWithChildren } from 'react' +import { twMerge } from 'tailwind-merge' +import { unica } from '@/bridge/components/common/Font' + +import '@/bridge/styles/tailwind.css' + +export const metadata: Metadata = { + title: 'Arbitrum Token Bridge', + description: 'Bridge tokens between Ethereum and Arbitrum networks', + icons: { + icon: '/logo.png' + } +} + +export default function RootLayout({ children }: PropsWithChildren) { + return ( + + + {children} + + + ) +} diff --git a/packages/arb-token-bridge-ui/src/pages/404.tsx b/packages/app/src/app/not-found.tsx similarity index 92% rename from packages/arb-token-bridge-ui/src/pages/404.tsx rename to packages/app/src/app/not-found.tsx index 05b3b5ef26..b817e22fe9 100644 --- a/packages/arb-token-bridge-ui/src/pages/404.tsx +++ b/packages/app/src/app/not-found.tsx @@ -1,7 +1,7 @@ import Image from 'next/image' import FixingSpaceship from '@/images/arbinaut-fixing-spaceship.webp' -export default function Custom404Page() { +export default function NotFound() { return (
404 diff --git a/packages/arb-token-bridge-ui/src/pages/restricted.tsx b/packages/app/src/app/restricted/page.tsx similarity index 100% rename from packages/arb-token-bridge-ui/src/pages/restricted.tsx rename to packages/app/src/app/restricted/page.tsx diff --git a/packages/app/src/initialization.ts b/packages/app/src/initialization.ts new file mode 100644 index 0000000000..be76639996 --- /dev/null +++ b/packages/app/src/initialization.ts @@ -0,0 +1,46 @@ +import dayjs from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' +import advancedFormat from 'dayjs/plugin/advancedFormat' +import timeZone from 'dayjs/plugin/timezone' +import utc from 'dayjs/plugin/utc' +import { registerCustomArbitrumNetwork } from '@arbitrum/sdk' +import { getOrbitChains } from '@/bridge/util/orbitChainsList' +import { + mapCustomChainToNetworkData, + getCustomChainsFromLocalStorage +} from '@/bridge/util/networks' + +let arbitrumSdkInitialized = false + +/** + * This file include initialization that need to be performed on client or on server + * + * Server side initialization is done in src/app/(bridge)/page.tsx + * Client side initialization is done in BridgeClient.tsx + */ + +export function initializeDayjs() { + dayjs.extend(utc) + dayjs.extend(relativeTime) + dayjs.extend(timeZone) + dayjs.extend(advancedFormat) +} + +export function addOrbitChainsToArbitrumSDK() { + if (arbitrumSdkInitialized) { + return + } + + ;[...getOrbitChains(), ...getCustomChainsFromLocalStorage()].forEach( + chain => { + try { + registerCustomArbitrumNetwork(chain) + mapCustomChainToNetworkData(chain) + } catch (_) { + // already added + } + } + ) + + arbitrumSdkInitialized = true +} diff --git a/packages/app/tailwind.config.js b/packages/app/tailwind.config.js new file mode 100644 index 0000000000..218344f94d --- /dev/null +++ b/packages/app/tailwind.config.js @@ -0,0 +1,13 @@ +/** @type {import('tailwindcss').Config} */ +// eslint-disable-next-line @typescript-eslint/no-require-imports +const rootConfig = require('../../tailwind.config.js') + +module.exports = { + ...rootConfig, + content: [ + './src/app/**/*.{js,ts,jsx,tsx}', + './pages/**/*.{js,ts,jsx,tsx}', + '../arb-token-bridge-ui/src/**/*.{js,ts,jsx,tsx}', + '../../node_modules/@offchainlabs/cobalt/**/*.{js,ts,jsx,tsx}' + ] +} diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json new file mode 100644 index 0000000000..b8da93c388 --- /dev/null +++ b/packages/app/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "plugins": [ + { + "name": "next" + } + ], + "noEmit": true, + "incremental": true, + "jsx": "preserve" + }, + "include": [ + "next-env.d.ts", + "src/app/**/*.ts", + "src/app/**/*.tsx", + ".next/types/**/*.ts", + "*.js", + "../arb-token-bridge-ui/additional.d.ts", + "build/types/**/*.ts" + ], + "exclude": ["node_modules"] +} diff --git a/packages/arb-token-bridge-ui/package.json b/packages/arb-token-bridge-ui/package.json index fd85a98cad..fef837db9a 100644 --- a/packages/arb-token-bridge-ui/package.json +++ b/packages/arb-token-bridge-ui/package.json @@ -31,15 +31,12 @@ "exponential-backoff": "^3.1.2", "graphql": "^16.10.0", "lodash-es": "^4.17.21", - "next": "^14.2.32", "next-query-params": "^5.1.0", "overmind": "^28.0.1", "overmind-react": "^29.0.1", "p-limit": "^6.2.0", "posthog-js": "^1.236.5", "query-string": "^9.1.1", - "react": "^18.3.1", - "react-dom": "^18.3.1", "react-loader-spinner": "^5.4.5", "react-syntax-highlighter": "^15.6.1", "react-toastify": "^9.1.1", @@ -55,6 +52,11 @@ "zod": "^3.24.3", "zustand": "^4.3.9" }, + "peerDependencies": { + "next": "^14.2.32", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, "scripts": { "predev": "yarn generateDenylist", "dev": "next dev", @@ -64,9 +66,9 @@ "start": "next start", "test": "vitest --config vitest.config.ts --watch", "test:ci": "vitest --config vitest.config.ts --run", - "lint": "tsc && eslint", + "lint": "eslint", "lint:fix": "tsc && eslint --quiet --fix", - "prettier:format": "prettier --config-precedence file-override --write \"src/**/*.{tsx,ts,scss,md,json}\"", + "prettier:format": "prettier --write \"src/**/*.{tsx,ts,scss,md,json}\"", "generateDenylist": "ts-node --project ./scripts/tsconfig.json ./scripts/generateDenylist.ts", "generateOpenGraphImages": "ts-node --project ./scripts/tsconfig.json ./src/generateOpenGraphImages.tsx generate", "generateOpenGraphImages:core": "yarn generateOpenGraphImages --core", @@ -90,45 +92,24 @@ }, "devDependencies": { "@babel/preset-env": "^7.26.9", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.26.0", - "@next/eslint-plugin-next": "^15.3.2", "@synthetixio/synpress": "3.7.3", "@testing-library/react": "^16.3.0", "@types/lodash-es": "^4.17.12", - "@types/node": "^16.6.1", - "@types/react": "^18.2.6", - "@types/react-dom": "^18.2.4", "@types/react-syntax-highlighter": "^15.5.13", "@types/react-virtualized": "^9.22.2", "@types/wcag-contrast": "^3.0.3", "@types/yargs": "^17.0.33", - "@typescript-eslint/eslint-plugin": "^8.32.1", - "@typescript-eslint/parser": "^8.32.1", "autoprefixer": "^10.4.13", "cypress-terminal-report": "^7.1.0", "env-cmd": "^10.1.0", - "eslint": "^9.26.0", - "eslint-config-prettier": "^10.1.5", - "eslint-import-resolver-alias": "^1.1.2", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-prettier": "^5.4.0", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-zustand-rules": "https://github.com/OffchainLabs/eslint-plugin-zustand-rules", "happy-dom": "^17.4.4", "patch-package": "^8.0.0", "postcss": "^8.5.3", "postinstall-postinstall": "^2.1.0", - "prettier": "^2.7.1", - "prettier-plugin-tailwindcss": "^0.1.11", "satori": "^0.12.2", "start-server-and-test": "^2.0.11", "tailwindcss": "^3.4.16", "ts-node": "^10.9.2", - "typescript": "^5.2.2", - "typescript-eslint": "^8.32.1", "vitest": "^3.1.1", "yargs": "^18.0.0" } diff --git a/packages/arb-token-bridge-ui/scripts/generateDenylist.ts b/packages/arb-token-bridge-ui/scripts/generateDenylist.ts index 22ae50fcdb..1a8dfd10b7 100644 --- a/packages/arb-token-bridge-ui/scripts/generateDenylist.ts +++ b/packages/arb-token-bridge-ui/scripts/generateDenylist.ts @@ -1,4 +1,5 @@ import fs from 'fs' +import path from 'path' import axios from 'axios' import { TokenList } from '@uniswap/token-lists' import { getArbitrumNetworks } from '@arbitrum/sdk' @@ -153,7 +154,10 @@ async function main() { 2 ) + '\n' - fs.writeFileSync('./public/__auto-generated-denylist.json', resultJson) + fs.writeFileSync( + path.join(__dirname, '../../app/public/__auto-generated-denylist.json'), + resultJson + ) } main() diff --git a/packages/arb-token-bridge-ui/src/pages/api/cctp/[type].ts b/packages/arb-token-bridge-ui/src/app/api/cctp/[type].ts similarity index 78% rename from packages/arb-token-bridge-ui/src/pages/api/cctp/[type].ts rename to packages/arb-token-bridge-ui/src/app/api/cctp/[type].ts index 4ff9a8591f..6eed61e70f 100644 --- a/packages/arb-token-bridge-ui/src/pages/api/cctp/[type].ts +++ b/packages/arb-token-bridge-ui/src/app/api/cctp/[type].ts @@ -1,5 +1,5 @@ import { gql } from '@apollo/client' -import { NextApiRequest, NextApiResponse } from 'next' +import { NextRequest, NextResponse } from 'next/server' import { ChainId } from '../../../types/ChainId' import { Address } from '../../../util/AddressUtils' @@ -9,16 +9,6 @@ import { getSourceFromSubgraphClient } from '../../../api-utils/ServerSubgraphUtils' -// Extending the standard NextJs request with CCTP params -export type NextApiRequestWithCCTPParams = NextApiRequest & { - query: { - walletAddress: Address - l1ChainId: string - pageNumber?: string - pageSize?: string - } -} - export enum ChainDomain { Ethereum = 0, ArbitrumOne = 3 @@ -77,44 +67,33 @@ export type Response = error: string } -export default async function handler( - req: NextApiRequestWithCCTPParams, - res: NextApiResponse -) { +export async function GET( + request: NextRequest, + { params }: { params: { type: string } } +): Promise> { try { - const { - walletAddress, - l1ChainId: l1ChainIdString, - pageNumber = '0', - pageSize = '10', - type - } = req.query + const { searchParams } = new URL(request.url) + const walletAddress = searchParams.get('walletAddress') as Address + const l1ChainIdString = searchParams.get('l1ChainId') || '1' + const pageNumber = searchParams.get('pageNumber') || '0' + const pageSize = searchParams.get('pageSize') || '10' + const type = params.type const l1ChainId = parseInt(l1ChainIdString, 10) if ( typeof type !== 'string' || (type !== 'deposits' && type !== 'withdrawals') ) { - res.status(400).send({ - error: `invalid API route: ${type}`, - data: { - pending: [], - completed: [] - } - }) - return - } - - // validate method - if (req.method !== 'GET') { - res.status(400).send({ - error: `invalid_method: ${req.method}`, - data: { - pending: [], - completed: [] - } - }) - return + return NextResponse.json( + { + error: `invalid API route: ${type}`, + data: { + pending: [], + completed: [] + } + }, + { status: 400 } + ) } // validate the request parameters @@ -123,26 +102,30 @@ export default async function handler( if (!walletAddress) errorMessage.push(' is required') if (errorMessage.length) { - res.status(400).json({ - error: `incomplete request: ${errorMessage.join(', ')}`, - data: { - pending: [], - completed: [] - } - }) - return + return NextResponse.json( + { + error: `incomplete request: ${errorMessage.join(', ')}`, + data: { + pending: [], + completed: [] + } + }, + { status: 400 } + ) } // if invalid pageSize, send empty data instead of error if (isNaN(Number(pageSize)) || Number(pageSize) === 0) { - res.status(200).json({ - data: { - pending: [], - completed: [] + return NextResponse.json( + { + data: { + pending: [], + completed: [] + }, + error: null }, - error: null - }) - return + { status: 200 } + ) } const l2ChainId = @@ -264,23 +247,29 @@ export default async function handler( } ) - res.status(200).json({ - meta: { - source: getSourceFromSubgraphClient(l1Subgraph) - }, - data: { - pending, - completed + return NextResponse.json( + { + meta: { + source: getSourceFromSubgraphClient(l1Subgraph) + }, + data: { + pending, + completed + }, + error: null }, - error: null - }) + { status: 200 } + ) } catch (error: unknown) { - res.status(500).json({ - data: { - pending: [], - completed: [] + return NextResponse.json( + { + data: { + pending: [], + completed: [] + }, + error: (error as Error)?.message ?? 'Something went wrong' }, - error: (error as Error)?.message ?? 'Something went wrong' - }) + { status: 500 } + ) } } diff --git a/packages/arb-token-bridge-ui/src/pages/api/chains/[chainId]/block-number.ts b/packages/arb-token-bridge-ui/src/app/api/chains/[chainId]/block-number.ts similarity index 71% rename from packages/arb-token-bridge-ui/src/pages/api/chains/[chainId]/block-number.ts rename to packages/arb-token-bridge-ui/src/app/api/chains/[chainId]/block-number.ts index f23b2fd4c3..3492a38de9 100644 --- a/packages/arb-token-bridge-ui/src/pages/api/chains/[chainId]/block-number.ts +++ b/packages/arb-token-bridge-ui/src/app/api/chains/[chainId]/block-number.ts @@ -1,4 +1,4 @@ -import { NextApiRequest, NextApiResponse } from 'next' +import { NextRequest, NextResponse } from 'next/server' import { gql } from '@apollo/client' import { ChainId } from '../../../../types/ChainId' @@ -31,19 +31,15 @@ function getSubgraphClient(chainId: number) { } } -export default async function handler( - req: NextApiRequest & { query: { chainId: string } }, - res: NextApiResponse< +export async function GET( + request: NextRequest, + { params }: { params: { chainId: string } } +): Promise< + NextResponse< { data: number; meta?: { source: string | null } } | { message: string } > -) { - const { chainId } = req.query - - // validate method - if (req.method !== 'GET') { - res.status(400).json({ message: `invalid method: ${req.method}` }) - return - } +> { + const { chainId } = params try { const subgraphClient = getSubgraphClient(Number(chainId)) @@ -68,11 +64,14 @@ export default async function handler( ` }) - res.status(200).json({ - meta: { source: getSourceFromSubgraphClient(subgraphClient) }, - data: result.data._meta.block.number - }) + return NextResponse.json( + { + meta: { source: getSourceFromSubgraphClient(subgraphClient) }, + data: result.data._meta.block.number + }, + { status: 200 } + ) } catch (error) { - res.status(200).json({ data: 0 }) + return NextResponse.json({ data: 0 }, { status: 200 }) } } diff --git a/packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/constants.ts b/packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/constants.ts similarity index 100% rename from packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/constants.ts rename to packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/constants.ts diff --git a/packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/lifi.ts b/packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/lifi.ts similarity index 78% rename from packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/lifi.ts rename to packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/lifi.ts index 7f0ba9d524..19afb9476f 100644 --- a/packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/lifi.ts +++ b/packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/lifi.ts @@ -1,4 +1,4 @@ -import { NextApiRequest, NextApiResponse } from 'next' +import { NextRequest, NextResponse } from 'next/server' import { createConfig, TransactionRequest as LiFiTransactionRequest, @@ -95,7 +95,7 @@ function parseLifiRouteToCrosschainTransfersQuoteWithLifiData({ }: { route: Route fromAddress?: string - toAddress: string + toAddress?: string fromChainId: string toChainId: string }): LifiCrosschainTransfersRoute { @@ -212,55 +212,56 @@ export type LifiParams = QueryParams & { } /** Extending the standard NextJs request with fast bridge transfer params */ -export type NextApiRequestWithLifiParams = NextApiRequest & { - query: LifiParams -} export const INTEGRATOR_ID = '_arbitrum' -export default async function handler( - req: NextApiRequestWithLifiParams, - res: NextApiResponse -) { +export async function GET( + request: NextRequest +): Promise> { createConfig({ integrator: INTEGRATOR_ID, apiKey: process.env.LIFI_KEY }) - const { - fromToken, - toToken, - fromChainId, - toChainId, - fromAmount, - fromAddress, - toAddress, - denyBridges, - denyExchanges, - slippage - } = req.query + const { searchParams } = new URL(request.url) + const fromToken = searchParams.get('fromToken') + const toToken = searchParams.get('toToken') + const fromChainId = searchParams.get('fromChainId') + const toChainId = searchParams.get('toChainId') + const fromAmount = searchParams.get('fromAmount') || '0' + const fromAddress = searchParams.get('fromAddress') || undefined + const toAddress = searchParams.get('toAddress') || undefined + const denyBridges = searchParams.get('denyBridges') + const denyExchanges = searchParams.get('denyExchanges') + const slippage = searchParams.get('slippage') try { - // validate method - if (req.method !== 'GET') { - res - .status(400) - .send({ message: `invalid_method: ${req.method}`, data: null }) - return - } - // Validate parameters if (!fromToken || !utils.isAddress(fromToken)) { - res - .status(400) - .send({ message: 'fromToken is not a valid address', data: null }) - return + return NextResponse.json( + { message: 'fromToken is not a valid address', data: null }, + { status: 400 } + ) } if (!toToken || !utils.isAddress(toToken)) { - res - .status(400) - .send({ message: 'toToken is not a valid address', data: null }) - return + return NextResponse.json( + { message: 'toToken is not a valid address', data: null }, + { status: 400 } + ) + } + + if (!fromChainId) { + return NextResponse.json( + { message: 'fromChainId is required', data: null }, + { status: 400 } + ) + } + + if (!toChainId) { + return NextResponse.json( + { message: 'toChainId is required', data: null }, + { status: 400 } + ) } if ( @@ -270,11 +271,13 @@ export default async function handler( destinationChainId: Number(toChainId) }) ) { - res.status(400).send({ - message: `Sending fromToken (${fromToken}) from chain ${fromChainId} to chain ${toChainId} is not supported`, - data: null - }) - return + return NextResponse.json( + { + message: `Sending fromToken (${fromToken}) from chain ${fromChainId} to chain ${toChainId} is not supported`, + data: null + }, + { status: 400 } + ) } // Validate options @@ -284,11 +287,13 @@ export default async function handler( parsedSlippage <= 0 || parsedSlippage > 100 ) { - res.status(400).send({ - message: `Slippage is invalid`, - data: null - }) - return + return NextResponse.json( + { + message: `Slippage is invalid`, + data: null + }, + { status: 400 } + ) } let bridgesToExclude: string[] = [] @@ -348,7 +353,7 @@ export default async function handler( parseLifiRouteToCrosschainTransfersQuoteWithLifiData({ route, fromAddress, - toAddress, + toAddress: toAddress || fromAddress, fromChainId, toChainId }) @@ -370,35 +375,38 @@ export default async function handler( // We didn't filter route with tags if (tags.length === 2) { - res.status(200).json({ - data: filteredRoutes.filter( - route => route.protocolData.orders.length > 0 - ) - }) - return + return NextResponse.json( + { + data: filteredRoutes.filter( + route => route.protocolData.orders.length > 0 + ) + }, + { status: 200 } + ) } const cheapestRoute = findCheapestRoute(filteredRoutes) const fastestRoute = findFastestRoute(filteredRoutes) if (!cheapestRoute && !fastestRoute) { - res.status(204).json({ data: [] }) - return + return NextResponse.json({ data: [] }, { status: 204 }) } if (cheapestRoute && fastestRoute && cheapestRoute === fastestRoute) { - res.status(200).json({ - data: [ - { - ...cheapestRoute, - protocolData: { - ...cheapestRoute.protocolData, - orders: [Order.Cheapest, Order.Fastest] + return NextResponse.json( + { + data: [ + { + ...cheapestRoute, + protocolData: { + ...cheapestRoute.protocolData, + orders: [Order.Cheapest, Order.Fastest] + } } - } - ] - }) - return + ] + }, + { status: 200 } + ) } const data: LifiCrosschainTransfersRoute[] = [] @@ -420,13 +428,19 @@ export default async function handler( } }) } - res.status(200).json({ - data - }) + return NextResponse.json( + { + data + }, + { status: 200 } + ) } catch (error: any) { - res.status(500).json({ - message: error?.message ?? 'Something went wrong', - data: null - }) + return NextResponse.json( + { + message: error?.message ?? 'Something went wrong', + data: null + }, + { status: 500 } + ) } } diff --git a/packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/types.ts b/packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/types.ts similarity index 97% rename from packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/types.ts rename to packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/types.ts index 11a844ffbb..5a7f0e23fd 100644 --- a/packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/types.ts +++ b/packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/types.ts @@ -41,6 +41,6 @@ export interface CrosschainTransfersRouteBase { fromChainId: number toChainId: number fromAddress?: string - toAddress: string + toAddress?: string spenderAddress: string } diff --git a/packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/utils.test.ts b/packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/utils.test.ts similarity index 100% rename from packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/utils.test.ts rename to packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/utils.test.ts diff --git a/packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/utils.ts b/packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/utils.ts similarity index 100% rename from packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/utils.ts rename to packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/utils.ts diff --git a/packages/arb-token-bridge-ui/src/app/api/denylist.ts b/packages/arb-token-bridge-ui/src/app/api/denylist.ts new file mode 100644 index 0000000000..db614dc644 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/app/api/denylist.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from 'next/server' +import denylist from '@/public/__auto-generated-denylist.json' + +const ONE_WEEK_IN_SECONDS = 60 * 60 * 24 * 7 + +export async function GET( + request: NextRequest +): Promise> { + try { + const { searchParams } = new URL(request.url) + const address = searchParams.get('address') + + if (typeof address !== 'string') { + return NextResponse.json( + { + message: `invalid_parameter: expected 'address' to be a string but got ${typeof address}`, + data: undefined + }, + { status: 400 } + ) + } + + const isDenylisted = new Set(denylist.content).has(address.toLowerCase()) + + return NextResponse.json( + { data: isDenylisted }, + { + status: 200, + headers: { + 'Cache-Control': `max-age=0, s-maxage=${ONE_WEEK_IN_SECONDS}` + } + } + ) + } catch (error: any) { + return NextResponse.json( + { + message: error?.message ?? 'Something went wrong', + data: undefined + }, + { status: 500 } + ) + } +} diff --git a/packages/arb-token-bridge-ui/src/pages/api/deposits.ts b/packages/arb-token-bridge-ui/src/app/api/deposits.ts similarity index 65% rename from packages/arb-token-bridge-ui/src/pages/api/deposits.ts rename to packages/arb-token-bridge-ui/src/app/api/deposits.ts index 0250082bde..7b22a7b68e 100644 --- a/packages/arb-token-bridge-ui/src/pages/api/deposits.ts +++ b/packages/arb-token-bridge-ui/src/app/api/deposits.ts @@ -1,4 +1,4 @@ -import { NextApiRequest, NextApiResponse } from 'next' +import { NextRequest, NextResponse } from 'next/server' import { gql } from '@apollo/client' import { FetchDepositsFromSubgraphResult } from '../../util/deposits/fetchDepositsFromSubgraph' @@ -7,49 +7,25 @@ import { getSourceFromSubgraphClient } from '../../api-utils/ServerSubgraphUtils' -// Extending the standard NextJs request with Deposit-params -type NextApiRequestWithDepositParams = NextApiRequest & { - query: { - sender?: string - receiver?: string - l2ChainId: string - search?: string - page?: string - pageSize?: string - fromBlock?: string - toBlock?: string - } -} - type DepositsResponse = { meta?: { source: string | null } data: FetchDepositsFromSubgraphResult[] message?: string // in case of any error } -export default async function handler( - req: NextApiRequestWithDepositParams, - res: NextApiResponse -) { +export async function GET( + request: NextRequest +): Promise> { try { - const { - sender, - receiver, - search = '', - l2ChainId, - page = '0', - pageSize = '10', - fromBlock, - toBlock - } = req.query - - // validate method - if (req.method !== 'GET') { - res - .status(400) - .send({ message: `invalid_method: ${req.method}`, data: [] }) - return - } + const { searchParams } = new URL(request.url) + const sender = searchParams.get('sender') || undefined + const receiver = searchParams.get('receiver') || undefined + const search = searchParams.get('search') || '' + const l2ChainId = searchParams.get('l2ChainId') + const page = searchParams.get('page') || '0' + const pageSize = searchParams.get('pageSize') || '10' + const fromBlock = searchParams.get('fromBlock') || undefined + const toBlock = searchParams.get('toBlock') || undefined // validate the request parameters const errorMessage = [] @@ -58,19 +34,18 @@ export default async function handler( errorMessage.push(' or is required') if (errorMessage.length) { - res.status(400).json({ - message: `incomplete request: ${errorMessage.join(', ')}`, - data: [] - }) - return + return NextResponse.json( + { + message: `incomplete request: ${errorMessage.join(', ')}`, + data: [] + }, + { status: 400 } + ) } // if invalid pageSize, send empty data instead of error if (isNaN(Number(pageSize)) || Number(pageSize) === 0) { - res.status(200).json({ - data: [] - }) - return + return NextResponse.json({ data: [] }, { status: 200 }) } const additionalFilters = `${ @@ -91,11 +66,13 @@ export default async function handler( subgraphClient = getL1SubgraphClient(Number(l2ChainId)) } catch (error: any) { // catch attempt to query unsupported networks and throw a 400 - res.status(400).json({ - message: error?.message ?? 'Something went wrong', - data: [] - }) - return + return NextResponse.json( + { + message: error?.message ?? 'Something went wrong', + data: [] + }, + { status: 400 } + ) } const subgraphResult = await subgraphClient.query({ @@ -142,14 +119,20 @@ export default async function handler( const transactions: FetchDepositsFromSubgraphResult[] = subgraphResult.data.deposits - res.status(200).json({ - meta: { source: getSourceFromSubgraphClient(subgraphClient) }, - data: transactions - }) + return NextResponse.json( + { + meta: { source: getSourceFromSubgraphClient(subgraphClient) }, + data: transactions + }, + { status: 200 } + ) } catch (error: any) { - res.status(500).json({ - message: error?.message ?? 'Something went wrong', - data: [] - }) + return NextResponse.json( + { + message: error?.message ?? 'Something went wrong', + data: [] + }, + { status: 500 } + ) } } diff --git a/packages/arb-token-bridge-ui/src/pages/api/eth-deposits-custom-destination.ts b/packages/arb-token-bridge-ui/src/app/api/eth-deposits-custom-destination.ts similarity index 69% rename from packages/arb-token-bridge-ui/src/pages/api/eth-deposits-custom-destination.ts rename to packages/arb-token-bridge-ui/src/app/api/eth-deposits-custom-destination.ts index b0e5b084a9..cff831cecd 100644 --- a/packages/arb-token-bridge-ui/src/pages/api/eth-deposits-custom-destination.ts +++ b/packages/arb-token-bridge-ui/src/app/api/eth-deposits-custom-destination.ts @@ -1,4 +1,4 @@ -import { NextApiRequest, NextApiResponse } from 'next' +import { NextRequest, NextResponse } from 'next/server' import { gql } from '@apollo/client' import { @@ -7,19 +7,6 @@ import { } from '../../api-utils/ServerSubgraphUtils' import { FetchEthDepositsToCustomDestinationFromSubgraphResult } from '../../util/deposits/fetchEthDepositsToCustomDestinationFromSubgraph' -type NextApiRequestWithDepositParams = NextApiRequest & { - query: { - sender?: string - receiver?: string - l2ChainId: string - search?: string - page?: string - pageSize?: string - fromBlock?: string - toBlock?: string - } -} - type RetryableFromSubgraph = { destAddr: string sender: string @@ -36,28 +23,19 @@ type EthDepositsToCustomDestinationResponse = { message?: string } -export default async function handler( - req: NextApiRequestWithDepositParams, - res: NextApiResponse -) { +export async function GET( + request: NextRequest +): Promise> { try { - const { - sender, - receiver, - search = '', - l2ChainId, - page = '0', - pageSize = '10', - fromBlock, - toBlock - } = req.query - - if (req.method !== 'GET') { - res - .status(400) - .send({ message: `invalid_method: ${req.method}`, data: [] }) - return - } + const { searchParams } = new URL(request.url) + const sender = searchParams.get('sender') || undefined + const receiver = searchParams.get('receiver') || undefined + const search = searchParams.get('search') || '' + const l2ChainId = searchParams.get('l2ChainId') + const page = searchParams.get('page') || '0' + const pageSize = searchParams.get('pageSize') || '10' + const fromBlock = searchParams.get('fromBlock') || undefined + const toBlock = searchParams.get('toBlock') || undefined const errorMessage = [] if (!l2ChainId) errorMessage.push(' is required') @@ -65,19 +43,18 @@ export default async function handler( errorMessage.push(' or is required') if (errorMessage.length) { - res.status(400).json({ - message: `incomplete request: ${errorMessage.join(', ')}`, - data: [] - }) - return + return NextResponse.json( + { + message: `incomplete request: ${errorMessage.join(', ')}`, + data: [] + }, + { status: 400 } + ) } // if invalid pageSize, send empty data instead of error if (isNaN(Number(pageSize)) || Number(pageSize) === 0) { - res.status(200).json({ - data: [] - }) - return + return NextResponse.json({ data: [] }, { status: 200 }) } const additionalFilters = `${ @@ -144,14 +121,20 @@ export default async function handler( } }) - res.status(200).json({ - meta: { source: getSourceFromSubgraphClient(subgraphClient) }, - data: transactions - }) + return NextResponse.json( + { + meta: { source: getSourceFromSubgraphClient(subgraphClient) }, + data: transactions + }, + { status: 200 } + ) } catch (error: any) { - res.status(500).json({ - message: error?.message ?? 'Something went wrong', - data: [] - }) + return NextResponse.json( + { + message: error?.message ?? 'Something went wrong', + data: [] + }, + { status: 500 } + ) } } diff --git a/packages/arb-token-bridge-ui/src/pages/api/status.ts b/packages/arb-token-bridge-ui/src/app/api/status.ts similarity index 52% rename from packages/arb-token-bridge-ui/src/pages/api/status.ts rename to packages/arb-token-bridge-ui/src/app/api/status.ts index 4ddf3d889e..a49d3674bc 100644 --- a/packages/arb-token-bridge-ui/src/pages/api/status.ts +++ b/packages/arb-token-bridge-ui/src/app/api/status.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import { NextApiRequest, NextApiResponse } from 'next' +import { NextResponse } from 'next/server' export type ArbitrumStatusResponse = { content: { @@ -19,22 +19,13 @@ export type ArbitrumStatusResponse = { const STATUS_URL = 'https://status.arbitrum.io/v2/components.json' -export default async function handler( - req: NextApiRequest, - res: NextApiResponse<{ +export async function GET(): Promise< + NextResponse<{ data: ArbitrumStatusResponse | undefined message?: string }> -) { +> { try { - // validate method - if (req.method !== 'GET') { - res - .status(400) - .send({ message: `invalid_method: ${req.method}`, data: undefined }) - return - } - const statusSummary = (await axios.get(STATUS_URL)).data const resultJson = { meta: { @@ -43,12 +34,22 @@ export default async function handler( content: statusSummary } - res.setHeader('Cache-Control', `max-age=0, s-maxage=${10 * 60}`) // cache response for 10 minutes - res.status(200).json({ data: resultJson as ArbitrumStatusResponse }) + return NextResponse.json( + { data: resultJson as ArbitrumStatusResponse }, + { + status: 200, + headers: { + 'Cache-Control': `max-age=0, s-maxage=${10 * 60}` + } + } + ) } catch (error: any) { - res.status(500).json({ - message: error?.message ?? 'Something went wrong', - data: undefined - }) + return NextResponse.json( + { + message: error?.message ?? 'Something went wrong', + data: undefined + }, + { status: 500 } + ) } } diff --git a/packages/arb-token-bridge-ui/src/pages/api/teleports/erc20.ts b/packages/arb-token-bridge-ui/src/app/api/teleports/erc20.ts similarity index 60% rename from packages/arb-token-bridge-ui/src/pages/api/teleports/erc20.ts rename to packages/arb-token-bridge-ui/src/app/api/teleports/erc20.ts index 7914b1b2af..7d9ecb198a 100644 --- a/packages/arb-token-bridge-ui/src/pages/api/teleports/erc20.ts +++ b/packages/arb-token-bridge-ui/src/app/api/teleports/erc20.ts @@ -1,4 +1,4 @@ -import { NextApiRequest, NextApiResponse } from 'next' +import { NextRequest, NextResponse } from 'next/server' import { gql } from '@apollo/client' import { @@ -7,36 +7,21 @@ import { } from '../../../api-utils/ServerSubgraphUtils' import { FetchErc20TeleportsFromSubgraphResult } from '../../../util/teleports/fetchErc20TeleportsFromSubgraph' -// Extending the standard NextJs request with Deposit-params -type NextApiRequestWithErc20TeleportParams = NextApiRequest & { - query: { - sender?: string - l1ChainId: string - page?: string - pageSize?: string - } -} - type Erc20TeleportResponse = { meta?: { source: string | null } data: FetchErc20TeleportsFromSubgraphResult[] message?: string // in case of any error } -export default async function handler( - req: NextApiRequestWithErc20TeleportParams, - res: NextApiResponse -) { +export async function GET( + request: NextRequest +): Promise> { try { - const { sender, l1ChainId, page = '0', pageSize = '10' } = req.query - - // validate method - if (req.method !== 'GET') { - res - .status(400) - .send({ message: `invalid_method: ${req.method}`, data: [] }) - return - } + const { searchParams } = new URL(request.url) + const sender = searchParams.get('sender') || undefined + const l1ChainId = searchParams.get('l1ChainId') + const page = searchParams.get('page') || '0' + const pageSize = searchParams.get('pageSize') || '10' // validate the request parameters const errorMessage = [] @@ -44,19 +29,18 @@ export default async function handler( if (!sender) errorMessage.push(' is required') if (errorMessage.length) { - res.status(400).json({ - message: `incomplete request: ${errorMessage.join(', ')}`, - data: [] - }) - return + return NextResponse.json( + { + message: `incomplete request: ${errorMessage.join(', ')}`, + data: [] + }, + { status: 400 } + ) } // if invalid pageSize, send empty data instead of error if (isNaN(Number(pageSize)) || Number(pageSize) === 0) { - res.status(200).json({ - data: [] - }) - return + return NextResponse.json({ data: [] }, { status: 200 }) } let subgraphClient @@ -64,11 +48,13 @@ export default async function handler( subgraphClient = getTeleporterSubgraphClient(Number(l1ChainId)) } catch (error: any) { // catch attempt to query unsupported networks and throw a 400 - res.status(400).json({ - message: error?.message ?? 'Something went wrong', - data: [] - }) - return + return NextResponse.json( + { + message: error?.message ?? 'Something went wrong', + data: [] + }, + { status: 400 } + ) } const subgraphResult = await subgraphClient.query({ @@ -99,14 +85,20 @@ export default async function handler( const transactions: FetchErc20TeleportsFromSubgraphResult[] = subgraphResult.data.teleporteds - res.status(200).json({ - meta: { source: getSourceFromSubgraphClient(subgraphClient) }, - data: transactions - }) + return NextResponse.json( + { + meta: { source: getSourceFromSubgraphClient(subgraphClient) }, + data: transactions + }, + { status: 200 } + ) } catch (error: any) { - res.status(500).json({ - message: error?.message ?? 'Something went wrong', - data: [] - }) + return NextResponse.json( + { + message: error?.message ?? 'Something went wrong', + data: [] + }, + { status: 500 } + ) } } diff --git a/packages/arb-token-bridge-ui/src/pages/api/teleports/eth.ts b/packages/arb-token-bridge-ui/src/app/api/teleports/eth.ts similarity index 61% rename from packages/arb-token-bridge-ui/src/pages/api/teleports/eth.ts rename to packages/arb-token-bridge-ui/src/app/api/teleports/eth.ts index d73592bad8..72cf6edd1a 100644 --- a/packages/arb-token-bridge-ui/src/pages/api/teleports/eth.ts +++ b/packages/arb-token-bridge-ui/src/app/api/teleports/eth.ts @@ -1,4 +1,4 @@ -import { NextApiRequest, NextApiResponse } from 'next' +import { NextRequest, NextResponse } from 'next/server' import { gql } from '@apollo/client' import { getL1SubgraphClient, @@ -7,50 +7,24 @@ import { import { getInboxAddressFromOrbitChainId } from '../../../util/orbitChainsList' import { FetchEthTeleportsFromSubgraphResult } from '../../../util/teleports/fetchEthTeleportsFromSubgraph' -// Extending the standard NextJs request with Deposit-params -type NextApiRequestWithDepositParams = NextApiRequest & { - query: { - sender?: string - receiver?: string - l1ChainId: string - l2ChainId: string - l3ChainId: string - search?: string - page?: string - pageSize?: string - fromBlock?: string - toBlock?: string - } -} - type EthTeleportResponse = { meta?: { source: string | null } data: FetchEthTeleportsFromSubgraphResult[] message?: string // in case of any error } -export default async function handler( - req: NextApiRequestWithDepositParams, - res: NextApiResponse -) { +export async function GET( + request: NextRequest +): Promise> { try { - const { - sender, - receiver, - l1ChainId, - l2ChainId, - l3ChainId, - page = '0', - pageSize = '10' - } = req.query - - // validate method - if (req.method !== 'GET') { - res - .status(400) - .send({ message: `invalid_method: ${req.method}`, data: [] }) - return - } + const { searchParams } = new URL(request.url) + const sender = searchParams.get('sender') || undefined + const receiver = searchParams.get('receiver') || undefined + const l1ChainId = searchParams.get('l1ChainId') + const l2ChainId = searchParams.get('l2ChainId') + const l3ChainId = searchParams.get('l3ChainId') + const page = searchParams.get('page') || '0' + const pageSize = searchParams.get('pageSize') || '10' // validate the request parameters const errorMessage = [] @@ -65,19 +39,18 @@ export default async function handler( errorMessage.push(' or is required') if (errorMessage.length) { - res.status(400).json({ - message: `incomplete request: ${errorMessage.join(', ')}`, - data: [] - }) - return + return NextResponse.json( + { + message: `incomplete request: ${errorMessage.join(', ')}`, + data: [] + }, + { status: 400 } + ) } // if invalid pageSize, send empty data instead of error if (isNaN(Number(pageSize)) || Number(pageSize) === 0) { - res.status(200).json({ - data: [] - }) - return + return NextResponse.json({ data: [] }, { status: 200 }) } let subgraphClient @@ -85,21 +58,25 @@ export default async function handler( subgraphClient = getL1SubgraphClient(Number(l2ChainId)) } catch (error: any) { // catch attempt to query unsupported networks and throw a 400 - res.status(400).json({ - message: error?.message ?? 'Something went wrong', - data: [] - }) - return + return NextResponse.json( + { + message: error?.message ?? 'Something went wrong', + data: [] + }, + { status: 400 } + ) } const l3InboxAddress = getInboxAddressFromOrbitChainId(Number(l3ChainId)) if (typeof l3InboxAddress === 'undefined') { - res.status(400).json({ - message: `inbox address not found for chain-id: ${l1ChainId} -> ${l2ChainId} -> ${l3ChainId}`, - data: [] - }) - return + return NextResponse.json( + { + message: `inbox address not found for chain-id: ${l1ChainId} -> ${l2ChainId} -> ${l3ChainId}`, + data: [] + }, + { status: 400 } + ) } const createRetryableFunctionSelector = '0x679b6ded' @@ -140,14 +117,20 @@ export default async function handler( }) ) - res.status(200).json({ - meta: { source: getSourceFromSubgraphClient(subgraphClient) }, - data: transactions - }) + return NextResponse.json( + { + meta: { source: getSourceFromSubgraphClient(subgraphClient) }, + data: transactions + }, + { status: 200 } + ) } catch (error: any) { - res.status(500).json({ - message: error?.message ?? 'Something went wrong', - data: [] - }) + return NextResponse.json( + { + message: error?.message ?? 'Something went wrong', + data: [] + }, + { status: 500 } + ) } } diff --git a/packages/arb-token-bridge-ui/src/pages/api/withdrawals.ts b/packages/arb-token-bridge-ui/src/app/api/withdrawals.ts similarity index 68% rename from packages/arb-token-bridge-ui/src/pages/api/withdrawals.ts rename to packages/arb-token-bridge-ui/src/app/api/withdrawals.ts index 6f203fc06a..3000017e39 100644 --- a/packages/arb-token-bridge-ui/src/pages/api/withdrawals.ts +++ b/packages/arb-token-bridge-ui/src/app/api/withdrawals.ts @@ -1,4 +1,4 @@ -import { NextApiRequest, NextApiResponse } from 'next' +import { NextRequest, NextResponse } from 'next/server' import { gql } from '@apollo/client' import { WithdrawalFromSubgraph } from '../../util/withdrawals/fetchWithdrawalsFromSubgraph' @@ -7,49 +7,25 @@ import { getSourceFromSubgraphClient } from '../../api-utils/ServerSubgraphUtils' -// Extending the standard NextJs request with Withdrawal-params -type NextApiRequestWithWithdrawalParams = NextApiRequest & { - query: { - sender?: string - receiver?: string - l2ChainId: string - search?: string - page?: string - pageSize?: string - fromBlock?: string - toBlock?: string - } -} - type WithdrawalResponse = { meta?: { source: string | null } data: WithdrawalFromSubgraph[] message?: string // in case of any error } -export default async function handler( - req: NextApiRequestWithWithdrawalParams, - res: NextApiResponse -) { +export async function GET( + request: NextRequest +): Promise> { try { - const { - sender, - receiver, - search = '', - l2ChainId, - page = '0', - pageSize = '10', - fromBlock, - toBlock - } = req.query - - // validate method - if (req.method !== 'GET') { - res - .status(400) - .send({ message: `invalid_method: ${req.method}`, data: [] }) - return - } + const { searchParams } = new URL(request.url) + const sender = searchParams.get('sender') || undefined + const receiver = searchParams.get('receiver') || undefined + const search = searchParams.get('search') || '' + const l2ChainId = searchParams.get('l2ChainId') + const page = searchParams.get('page') || '0' + const pageSize = searchParams.get('pageSize') || '10' + const fromBlock = searchParams.get('fromBlock') || undefined + const toBlock = searchParams.get('toBlock') || undefined // validate the request parameters const errorMessage = [] @@ -58,19 +34,18 @@ export default async function handler( errorMessage.push(' or is required') if (errorMessage.length) { - res.status(400).json({ - message: `incomplete request: ${errorMessage.join(', ')}`, - data: [] - }) - return + return NextResponse.json( + { + message: `incomplete request: ${errorMessage.join(', ')}`, + data: [] + }, + { status: 400 } + ) } // if invalid pageSize, send empty data instead of error if (isNaN(Number(pageSize)) || Number(pageSize) === 0) { - res.status(200).json({ - data: [] - }) - return + return NextResponse.json({ data: [] }, { status: 200 }) } const additionalFilters = `${ @@ -91,11 +66,13 @@ export default async function handler( subgraphClient = getL2SubgraphClient(Number(l2ChainId)) } catch (error: any) { // catch attempt to query unsupported networks and throw a 400 - res.status(400).json({ - message: error?.message ?? 'Something went wrong', - data: [] - }) - return + return NextResponse.json( + { + message: error?.message ?? 'Something went wrong', + data: [] + }, + { status: 400 } + ) } const subgraphResult = await subgraphClient.query({ @@ -164,14 +141,20 @@ export default async function handler( } }) - res.status(200).json({ - meta: { source: getSourceFromSubgraphClient(subgraphClient) }, - data: transactions - }) + return NextResponse.json( + { + meta: { source: getSourceFromSubgraphClient(subgraphClient) }, + data: transactions + }, + { status: 200 } + ) } catch (error: any) { - res.status(500).json({ - message: error?.message ?? 'Something went wrong', - data: [] - }) + return NextResponse.json( + { + message: error?.message ?? 'Something went wrong', + data: [] + }, + { status: 500 } + ) } } diff --git a/packages/arb-token-bridge-ui/src/components/App/App.tsx b/packages/arb-token-bridge-ui/src/components/App/App.tsx index e81b648124..20cc91739e 100644 --- a/packages/arb-token-bridge-ui/src/components/App/App.tsx +++ b/packages/arb-token-bridge-ui/src/components/App/App.tsx @@ -17,8 +17,9 @@ import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { useSyncConnectedChainToAnalytics } from './useSyncConnectedChainToAnalytics' import { useSyncConnectedChainToQueryParams } from './useSyncConnectedChainToQueryParams' import { Layout } from '../common/Layout' -import { AppProviders } from './AppProviders' import { useTheme } from '../../hooks/useTheme' +import dynamic from 'next/dynamic' +import { Loader } from '../common/atoms/Loader' declare global { interface Window { @@ -128,6 +129,21 @@ const AppContent = React.memo(() => { AppContent.displayName = 'AppContent' +const AppProviders = dynamic( + () => import('./AppProviders').then(mod => mod.AppProviders), + { + ssr: false, // use-query-params provider doesn't support SSR + loading: () => ( +
+
+
+ +
+
+ ) + } +) + export default function App() { return ( diff --git a/packages/arb-token-bridge-ui/src/components/App/AppProviders.tsx b/packages/arb-token-bridge-ui/src/components/App/AppProviders.tsx index c93663f38d..2340542551 100644 --- a/packages/arb-token-bridge-ui/src/components/App/AppProviders.tsx +++ b/packages/arb-token-bridge-ui/src/components/App/AppProviders.tsx @@ -11,7 +11,7 @@ import { AppContextProvider } from './AppContext' import { getProps } from '../../util/wagmi/setup' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createConfig } from '@lifi/sdk' -import { INTEGRATOR_ID } from '../../pages/api/crosschain-transfers/lifi' +import { INTEGRATOR_ID } from '@/bridge/app/api/crosschain-transfers/lifi' const rainbowkitTheme = merge(darkTheme(), { colors: { diff --git a/packages/arb-token-bridge-ui/src/components/Sidebar/AppMobileSidebar.tsx b/packages/arb-token-bridge-ui/src/components/Sidebar/AppMobileSidebar.tsx index fa862d02dc..0b1d0f3150 100644 --- a/packages/arb-token-bridge-ui/src/components/Sidebar/AppMobileSidebar.tsx +++ b/packages/arb-token-bridge-ui/src/components/Sidebar/AppMobileSidebar.tsx @@ -1,20 +1,10 @@ -'use client' import { PlusCircleIcon } from '@heroicons/react/24/outline' import { MenuItem } from '@offchainlabs/cobalt' import { ConnectButton } from '@rainbow-me/rainbowkit' -import dynamic from 'next/dynamic' import { usePostHog } from 'posthog-js/react' import { useAccount } from 'wagmi' import { AccountMenuItem } from './AccountMenuItem' - -// Dynamically import the MobileSidebar component with SSR disabled -const DynamicMobileSidebar = dynamic( - () => - import('@offchainlabs/cobalt').then(mod => ({ - default: mod.MobileSidebar - })), - { ssr: false } -) +import { MobileSidebar } from '@offchainlabs/cobalt' export const AppMobileSidebar: React.FC = () => { const posthog = usePostHog() @@ -22,7 +12,7 @@ export const AppMobileSidebar: React.FC = () => { return (
- = () => { )} )} - +
) } diff --git a/packages/arb-token-bridge-ui/src/components/Sidebar/AppSidebar.tsx b/packages/arb-token-bridge-ui/src/components/Sidebar/AppSidebar.tsx index c6cb166b3f..1444bf07ab 100644 --- a/packages/arb-token-bridge-ui/src/components/Sidebar/AppSidebar.tsx +++ b/packages/arb-token-bridge-ui/src/components/Sidebar/AppSidebar.tsx @@ -1,18 +1,11 @@ -'use client' -import dynamic from 'next/dynamic' import { usePostHog } from 'posthog-js/react' - -// Dynamically import the Sidebar component with SSR disabled -const DynamicSidebar = dynamic( - () => import('@offchainlabs/cobalt').then(mod => ({ default: mod.Sidebar })), - { ssr: false } -) +import { Sidebar } from '@offchainlabs/cobalt' export const AppSidebar = () => { const posthog = usePostHog() return (
- +
) } diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/HighSlippageWarningDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/HighSlippageWarningDialog.tsx index 2096aef006..5e205affc1 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/HighSlippageWarningDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/HighSlippageWarningDialog.tsx @@ -3,7 +3,7 @@ import { InformationCircleIcon } from '@heroicons/react/24/outline' import { useRouteStore } from './hooks/useRouteStore' import { formatAmount, formatUSD } from '../../util/NumberUtils' import { BigNumber } from 'ethers' -import { Token } from '../../pages/api/crosschain-transfers/types' +import { Token } from '@/bridge/app/api/crosschain-transfers/types' import { getAmountToPay } from './useTransferReadiness' type AmountProps = { diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/LifiSettings.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/LifiSettings.tsx index dea2d2545e..37724b66f7 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/LifiSettings.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/LifiSettings.tsx @@ -23,7 +23,7 @@ import { } from '@heroicons/react/24/outline' import { shallow } from 'zustand/shallow' import { ExternalLink } from '../common/ExternalLink' -import { getTokenOverride } from '../../pages/api/crosschain-transfers/utils' +import { getTokenOverride } from '../../app/api/crosschain-transfers/utils' function useIsLifiSupported() { const [networks] = useNetworks() diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/LifiRoute.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/LifiRoute.tsx index 0331d9d4e1..37c69cbfb5 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/LifiRoute.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/LifiRoute.tsx @@ -11,7 +11,7 @@ import { useArbQueryParams } from '../../../hooks/useArbQueryParams' import { LifiCrosschainTransfersRoute, Order -} from '../../../pages/api/crosschain-transfers/lifi' +} from '../../../app/api/crosschain-transfers/lifi' import { useLifiCrossTransfersRoute, UseLifiCrossTransfersRouteParams @@ -26,7 +26,7 @@ import { useCallback, useEffect, useMemo } from 'react' import { useAmountBigNumber } from '../hooks/useAmountBigNumber' import { shallow } from 'zustand/shallow' import { Address } from 'viem' -import { getTokenOverride } from '../../../pages/api/crosschain-transfers/utils' +import { getTokenOverride } from '../../../app/api/crosschain-transfers/utils' import { ERC20BridgeToken } from '../../../hooks/arbTokenBridge.types' import { useRoutes } from './Routes' import { NoteBox } from '../../common/NoteBox' diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Route.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Route.tsx index c824ecf916..a835df9ff6 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Route.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Route.tsx @@ -25,7 +25,7 @@ import { getConfirmationTime } from '../../../util/WithdrawalUtils' import { shortenAddress } from '../../../util/CommonUtils' import { useAppContextState } from '../../App/AppContext' import { useMode } from '../../../hooks/useMode' -import { Token } from '../../../pages/api/crosschain-transfers/types' +import { Token } from '@/bridge/app/api/crosschain-transfers/types' import { ERC20BridgeToken } from '../../../hooks/arbTokenBridge.types' // Types diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Routes.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Routes.tsx index d768f35204..95f337ce76 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Routes.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Routes.tsx @@ -18,7 +18,7 @@ import { useSelectedToken } from '../../../hooks/useSelectedToken' import { ERC20BridgeToken } from '../../../hooks/arbTokenBridge.types' import { twMerge } from 'tailwind-merge' import { useMode } from '../../../hooks/useMode' -import { isValidLifiTransfer } from '../../../pages/api/crosschain-transfers/utils' +import { isValidLifiTransfer } from '../../../app/api/crosschain-transfers/utils' import { useIsArbitrumCanonicalTransfer } from '../hooks/useIsCanonicalTransfer' function Wrapper({ children }: PropsWithChildren) { @@ -151,7 +151,13 @@ export function getRoutes({ } return { - ChildRoutes: <>{ChildRoutes.map(ChildRoute => ChildRoute)}, + ChildRoutes: ( + <> + {ChildRoutes.map((ChildRoute, index) => + React.cloneElement(ChildRoute, { key: index }) + )} + + ), routes } } diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/getGasCostAndToken.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/getGasCostAndToken.ts index ee1e387982..5d0f368009 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/getGasCostAndToken.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/getGasCostAndToken.ts @@ -1,7 +1,7 @@ import { constants } from 'ethers' import { UseGasSummaryResult } from '../../../hooks/TransferPanel/useGasSummary' import { NativeCurrency } from '../../../hooks/useNativeCurrency' -import { Token } from '../../../pages/api/crosschain-transfers/types' +import { Token } from '@/bridge/app/api/crosschain-transfers/types' export function getGasCostAndToken({ childChainNativeCurrency, diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/SettingsDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/SettingsDialog.tsx index fee5251723..a24eb1ae5c 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/SettingsDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/SettingsDialog.tsx @@ -23,7 +23,7 @@ import { useArbQueryParams } from '../../hooks/useArbQueryParams' import { useDestinationAddressError } from './hooks/useDestinationAddressError' import { useAccountType } from '../../hooks/useAccountType' import { Dialog, UseDialogProps } from '../common/Dialog' -import { isValidLifiTransfer } from '../../pages/api/crosschain-transfers/utils' +import { isValidLifiTransfer } from '../../app/api/crosschain-transfers/utils' import { isDepositMode as isDepositModeUtil } from '../../util/isDepositMode' function useTools() { diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx index b5fe26152d..b8a880bbf1 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx @@ -19,7 +19,6 @@ import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { TokenInfo } from './TokenInfo' import { NoteBox } from '../common/NoteBox' import { useSelectedToken } from '../../hooks/useSelectedToken' -import { addressesEqual } from '../../util/AddressUtils' import { constants } from 'ethers' enum ImportStatus { @@ -67,7 +66,7 @@ export function TokenImportDialog({ arbTokenBridge: { bridgeTokens, token } } } = useAppState() - const [selectedToken, setSelectedToken] = useSelectedToken() + const [, setSelectedToken] = useSelectedToken() const [networks] = useNetworks() const { childChainProvider, parentChainProvider } = useNetworksRelationship(networks) @@ -173,7 +172,7 @@ export function TokenImportDialog({ ) useEffect(() => { - if (!isOpen) { + if (!isOpen || isImportingToken) { return } @@ -222,37 +221,8 @@ export function TokenImportDialog({ isL1AddressLoading, isOpen, l1Address, - searchForTokenInLists - ]) - - useEffect(() => { - if (!isOpen) { - return - } - - if (isL1AddressLoading && !l1Address) { - return - } - - const foundToken = tokensFromUser[l1Address || tokenAddress] - - if (typeof foundToken === 'undefined') { - return - } - - // Listen for the token to be added to the bridge so we can automatically select it - if (!addressesEqual(foundToken.address, selectedToken?.address)) { - onClose(true) - selectToken(foundToken) - } - }, [ - isL1AddressLoading, - tokenAddress, - isOpen, - l1Address, - onClose, - selectToken, - tokensFromUser + searchForTokenInLists, + isImportingToken ]) async function storeNewToken(newToken: string) { @@ -286,9 +256,15 @@ export function TokenImportDialog({ selectToken(tokenToImport!) } else { // Token is not added to the bridge, so we add it - storeNewToken(l1Address).catch(() => { - setStatus(ImportStatus.ERROR) - }) + storeNewToken(l1Address) + .then(() => { + if (tokenToImport) { + selectToken(tokenToImport) + } + }) + .catch(() => { + setStatus(ImportStatus.ERROR) + }) } } diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenInfo.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenInfo.tsx index 148d6efa4c..61c57eceed 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenInfo.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenInfo.tsx @@ -16,7 +16,7 @@ import { isTokenArbitrumSepoliaNativeUSDC } from '../../util/TokenUtils' import { SafeImage } from '../common/SafeImage' -import { getTokenOverride } from '../../pages/api/crosschain-transfers/utils' +import { getTokenOverride } from '../../app/api/crosschain-transfers/utils' export function TokenLogoFallback({ className }: { className?: string }) { return ( diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx index 4f079c9b75..44b22ef79b 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx @@ -34,7 +34,7 @@ import { BlockExplorerTokenLink } from './TokenInfoTooltip' import { addressesEqual } from '../../util/AddressUtils' import { constants } from 'ethers' import { ChainId } from '../../types/ChainId' -import { getTokenOverride } from '../../pages/api/crosschain-transfers/utils' +import { getTokenOverride } from '../../app/api/crosschain-transfers/utils' function tokenListIdsToNames(ids: string[]): string { return ids diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx index 40eb046455..33b32a4c0a 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx @@ -14,7 +14,7 @@ import { useSelectedTokenIsWithdrawOnly } from './hooks/useSelectedTokenIsWithdr import { isTransferDisabledToken } from '../../util/TokenTransferDisabledUtils' import { isTeleportEnabledToken } from '../../util/TokenTeleportEnabledUtils' import { addressesEqual } from '../../util/AddressUtils' -import { isValidLifiTransfer } from '../../pages/api/crosschain-transfers/utils' +import { isValidLifiTransfer } from '../../app/api/crosschain-transfers/utils' import { ERC20BridgeToken } from '../../hooks/arbTokenBridge.types' import { isLifiEnabled } from '../../util/featureFlag' import { CommonAddress } from '../../util/CommonAddressUtils' diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx index e249730ed4..8264587be1 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx @@ -101,7 +101,7 @@ import { useMode } from '../../hooks/useMode' import { getTokenOverride, isValidLifiTransfer -} from '../../pages/api/crosschain-transfers/utils' +} from '../../app/api/crosschain-transfers/utils' import { NoteBox } from '../common/NoteBox' const signerUndefinedError = 'Signer is undefined' diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx index 1e07533136..880df3d36c 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx @@ -26,7 +26,7 @@ import { useArbQueryParams } from '../../../hooks/useArbQueryParams' import { useIsCctpTransfer } from '../hooks/useIsCctpTransfer' import { sanitizeTokenSymbol } from '../../../util/TokenUtils' import { useRouteStore } from '../hooks/useRouteStore' -import { getTokenOverride } from '../../../pages/api/crosschain-transfers/utils' +import { getTokenOverride } from '../../../app/api/crosschain-transfers/utils' function BalanceRow({ parentErc20Address, diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx index 5a9595f9bd..73cc6cbf4b 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx @@ -39,7 +39,7 @@ import { useIsCctpTransfer } from '../hooks/useIsCctpTransfer' import { useSourceChainNativeCurrencyDecimals } from '../../../hooks/useSourceChainNativeCurrencyDecimals' import { useIsOftV2Transfer } from '../hooks/useIsOftV2Transfer' import { useBalances } from '../../../hooks/useBalances' -import { getTokenOverride } from '../../../pages/api/crosschain-transfers/utils' +import { getTokenOverride } from '../../../app/api/crosschain-transfers/utils' function Amount2ToggleButton() { const [networks] = useNetworks() diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.test.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.test.tsx index c9d9ecfac2..3eddd4d1b3 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.test.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.test.tsx @@ -2,17 +2,23 @@ import { act, renderHook } from '@testing-library/react' import { vi, it, expect } from 'vitest' import { useAmountBigNumber } from './useAmountBigNumber' import { - EncodedQuery, + DecodedValueMap, QueryParamAdapter, - QueryParamOptions, + QueryParamConfigMap, QueryParamProvider } from 'use-query-params' import React, { PropsWithChildren } from 'react' import { makeMockAdapter } from '../../../hooks/__tests__/helpers' +import { + queryParamProviderOptions, + SetQueryParamsParameters +} from '../../../hooks/useArbQueryParams' const mocks = vi.hoisted(() => { return { - useSelectedTokenDecimals: vi.fn() + useSelectedTokenDecimals: vi.fn(), + mockSetQueryParams: vi.fn(), + mockQueryParams: {} as Partial> } }) @@ -22,14 +28,44 @@ vi.mock('../../../hooks/TransferPanel/useSelectedTokenDecimals', () => { } }) -export function setupWrapper(query: EncodedQuery, options?: QueryParamOptions) { +vi.mock('use-query-params', async () => { + const actual = await vi.importActual('use-query-params') + return { + ...actual, + useQueryParams: vi.fn(() => [ + mocks.mockQueryParams, + mocks.mockSetQueryParams + ]) + } +}) + +export function setupWrapper( + query: Partial> +) { + mocks.mockQueryParams = Object.fromEntries( + new URLSearchParams(query as Record) + ) + const Adapter = makeMockAdapter({ search: new URLSearchParams(query as Record).toString() }) const adapter = Adapter.adapter as QueryParamAdapter + + mocks.mockSetQueryParams.mockImplementation( + (updates: SetQueryParamsParameters) => { + const searchParams = new URLSearchParams() + Object.entries(updates).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + searchParams.set(key, String(value)) + } + }) + adapter.push({ search: `?${searchParams.toString()}` }) + } + ) + const wrapper = ({ children }: PropsWithChildren) => ( - + {children} ) @@ -59,6 +95,7 @@ it('Does not truncate if amount has more digits than number of decimals', () => }) it('Update amount if selectedToken changes', async () => { + vi.useFakeTimers() mocks.useSelectedTokenDecimals.mockReturnValue(18) const { wrapper, adapter } = setupWrapper({ amount: '1.23456789' }) const { result, rerender } = renderHook(() => useAmountBigNumber(), { @@ -72,8 +109,14 @@ it('Update amount if selectedToken changes', async () => { rerender() }) + await act(async () => { + vi.runOnlyPendingTimers() + }) + expect(adapter.push).toHaveBeenCalledExactlyOnceWith({ search: '?amount=1.234567' }) expect(result.current.toString()).toEqual('1234567') + + vi.useRealTimers() }) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.ts index 6937d84c72..639e1e4fab 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.ts @@ -25,7 +25,7 @@ export function useAmountBigNumber() { ) if (amount !== sanitizedAmount) { - setQueryParams({ amount: sanitizedAmount }) + setQueryParams({ amount: sanitizedAmount }, { debounce: true }) } return utils.parseUnits(sanitizedAmount, selectedTokenDecimals) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useRouteStore.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useRouteStore.ts index ba4126084d..c3b85fbddc 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useRouteStore.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useRouteStore.ts @@ -3,7 +3,7 @@ import { create } from 'zustand' import { MergedTransactionLifiData } from '../../../state/app/state' import { LiFiStep } from '@lifi/sdk' import { Address } from 'viem' -import { LifiCrosschainTransfersRoute } from '../../../pages/api/crosschain-transfers/lifi' +import { LifiCrosschainTransfersRoute } from '@/bridge/app/api/crosschain-transfers/lifi' import { BigNumber } from 'ethers' export type RouteType = diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts index 91cc85c8a4..da3885c831 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts @@ -41,8 +41,8 @@ import { } from './hooks/useRouteStore' import { shallow } from 'zustand/shallow' import { isLifiEnabled } from '../../util/featureFlag' -import { isValidLifiTransfer } from '../../pages/api/crosschain-transfers/utils' -import { Token } from '../../pages/api/crosschain-transfers/types' +import { isValidLifiTransfer } from '../../app/api/crosschain-transfers/utils' +import { Token } from '../../app/api/crosschain-transfers/types' // Add chains IDs that are currently down or disabled // It will block transfers (both deposits and withdrawals) and display an info box in the transfer panel diff --git a/packages/arb-token-bridge-ui/src/components/common/Layout.tsx b/packages/arb-token-bridge-ui/src/components/common/Layout.tsx index c7f0f970ac..a9c8a44f73 100644 --- a/packages/arb-token-bridge-ui/src/components/common/Layout.tsx +++ b/packages/arb-token-bridge-ui/src/components/common/Layout.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { PropsWithChildren } from 'react' import { twMerge } from 'tailwind-merge' import Image from 'next/image' import EclipseBottom from '@/images/eclipse_bottom.png' @@ -7,28 +7,23 @@ import { SiteBanner } from './SiteBanner' import { AppSidebar } from '../Sidebar/AppSidebar' import { Toast } from './atoms/Toast' -import 'react-toastify/dist/ReactToastify.css' import { useMode } from '../../hooks/useMode' import { unica } from './Font' -export type LayoutProps = { - children: React.ReactNode -} - -export function Layout(props: LayoutProps) { +export function Layout(props: PropsWithChildren) { const { embedMode } = useMode() if (embedMode) { return ( - +
{props.children} - +
) } return ( - +
grains
- +
) } diff --git a/packages/arb-token-bridge-ui/src/components/common/SiteBanner.tsx b/packages/arb-token-bridge-ui/src/components/common/SiteBanner.tsx index b8593827ee..43467b0492 100644 --- a/packages/arb-token-bridge-ui/src/components/common/SiteBanner.tsx +++ b/packages/arb-token-bridge-ui/src/components/common/SiteBanner.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import dayjs from 'dayjs' import { ExternalLink } from './ExternalLink' -import { ArbitrumStatusResponse } from '../../pages/api/status' +import { ArbitrumStatusResponse } from '@/bridge/app/api/status' import { getAPIBaseUrl } from '../../util' const SiteBannerArbiscanIncident = ({ diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts index 3b4b1562da..bc808a7095 100644 --- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts @@ -21,7 +21,7 @@ import { useLifiCrossTransfersRoute, UseLifiCrossTransfersRouteParams } from '../useLifiCrossTransferRoute' -import { getTokenOverride } from '../../pages/api/crosschain-transfers/utils' +import { getTokenOverride } from '../../app/api/crosschain-transfers/utils' import { Address } from 'viem' import { useLifiSettingsStore } from '../../components/TransferPanel/hooks/useLifiSettingsStore' import { shallow } from 'zustand/shallow' diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSetInputAmount.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSetInputAmount.ts index a9f1ba6b23..043216a515 100644 --- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSetInputAmount.ts +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSetInputAmount.ts @@ -12,7 +12,7 @@ export function useSetInputAmount() { (newAmount: string) => { const correctDecimalsAmount = truncateExtraDecimals(newAmount, decimals) - setQueryParams({ amount: correctDecimalsAmount }) + setQueryParams({ amount: correctDecimalsAmount }, { debounce: true }) }, [decimals, setQueryParams] ) @@ -21,7 +21,7 @@ export function useSetInputAmount() { (newAmount: string) => { const correctDecimalsAmount = truncateExtraDecimals(newAmount, 18) - setQueryParams({ amount2: correctDecimalsAmount }) + setQueryParams({ amount2: correctDecimalsAmount }, { debounce: true }) }, [setQueryParams] ) diff --git a/packages/arb-token-bridge-ui/src/hooks/__tests__/useArbQueryParams.test.ts b/packages/arb-token-bridge-ui/src/hooks/__tests__/useArbQueryParams.test.ts index 20a74dea03..bdb842e50b 100644 --- a/packages/arb-token-bridge-ui/src/hooks/__tests__/useArbQueryParams.test.ts +++ b/packages/arb-token-bridge-ui/src/hooks/__tests__/useArbQueryParams.test.ts @@ -10,7 +10,10 @@ import { DisabledFeaturesParam } from '../useArbQueryParams' import { createMockOrbitChain } from './helpers' -import { sanitizeTabQueryParam, sanitizeTokenQueryParam } from '../../pages' +import { + sanitizeTabQueryParam, + sanitizeTokenQueryParam +} from '../../util/queryParamUtils' describe('AmountQueryParam custom encoder and decoder', () => { describe('encode input field value to query param', () => { diff --git a/packages/arb-token-bridge-ui/src/hooks/__tests__/useArbQueryParamsDebouncing.test.tsx b/packages/arb-token-bridge-ui/src/hooks/__tests__/useArbQueryParamsDebouncing.test.tsx new file mode 100644 index 0000000000..577881d99a --- /dev/null +++ b/packages/arb-token-bridge-ui/src/hooks/__tests__/useArbQueryParamsDebouncing.test.tsx @@ -0,0 +1,194 @@ +import React from 'react' +import { vi, beforeEach, describe, it, expect, afterEach } from 'vitest' +import { ChainId } from '../../types/ChainId' +import { PropsWithChildren } from 'react' +import { act, renderHook } from '@testing-library/react' +import { useArbQueryParams } from '../useArbQueryParams' + +const mockSetQueryParams = vi.fn() +let currentQueryParams = { + amount: '', + amount2: '', + sourceChain: ChainId.Ethereum, + destinationChain: ChainId.ArbitrumOne, + token: null +} + +vi.mock('use-query-params', () => ({ + useQueryParams: () => { + return [currentQueryParams, mockSetQueryParams] + }, + QueryParamProvider: ({ children }: PropsWithChildren) => children, + BooleanParam: { encode: vi.fn(), decode: vi.fn() }, + StringParam: { encode: vi.fn(), decode: vi.fn() }, + withDefault: vi.fn() +})) + +const TestWrapper = ({ children }: PropsWithChildren) => <>{children} + +describe.sequential('useArbQueryParams debouncing', () => { + beforeEach(() => { + vi.useFakeTimers() + + currentQueryParams = { + amount: '', + amount2: '', + sourceChain: ChainId.Ethereum, + destinationChain: ChainId.ArbitrumOne, + token: null + } + + mockSetQueryParams.mockClear() + }) + + afterEach(() => { + vi.useRealTimers() + vi.clearAllMocks() + }) + + it('should be batched with debounce: true', async () => { + const { result } = renderHook(() => useArbQueryParams(), { + wrapper: TestWrapper + }) + + await act(async () => { + const [, setQueryParams] = result.current + setQueryParams({ amount: '10.5' }, { debounce: true }) + setQueryParams({ sourceChain: ChainId.ArbitrumOne }, { debounce: true }) + setQueryParams({ destinationChain: ChainId.Ethereum }, { debounce: true }) + setQueryParams( + { token: '0xaf88d065e77c8cc2239327c5edb3a432268e5831' }, + { debounce: true } + ) + vi.runOnlyPendingTimers() + }) + + expect(mockSetQueryParams).toHaveBeenCalledExactlyOnceWith({ + amount: '10.5', + sourceChain: ChainId.ArbitrumOne, + destinationChain: ChainId.Ethereum, + token: '0xaf88d065e77c8cc2239327c5edb3a432268e5831' + }) + }) + + it('should flush pending updates when receiving a call with debounce: false', async () => { + const { result } = renderHook(() => useArbQueryParams(), { + wrapper: TestWrapper + }) + + await act(async () => { + const [, setQueryParams] = result.current + setQueryParams({ amount: '10.5' }, { debounce: true }) + setQueryParams({ sourceChain: ChainId.ArbitrumOne }, { debounce: true }) + setQueryParams( + { destinationChain: ChainId.Ethereum }, + { debounce: false } + ) + setQueryParams( + { token: '0xaf88d065e77c8cc2239327c5edb3a432268e5831' }, + { debounce: true } + ) + vi.runOnlyPendingTimers() + }) + + expect(mockSetQueryParams).toHaveBeenCalledTimes(2) + // The first 2 debounced updates are merged with the first non-debounced update + expect(mockSetQueryParams).toHaveBeenNthCalledWith(1, { + amount: '10.5', + sourceChain: ChainId.ArbitrumOne, + destinationChain: ChainId.Ethereum + }) + + expect(mockSetQueryParams).toHaveBeenNthCalledWith(2, { + token: '0xaf88d065e77c8cc2239327c5edb3a432268e5831' + }) + }) + + it('should not be batched with debounce: false', async () => { + const { result } = renderHook(() => useArbQueryParams(), { + wrapper: TestWrapper + }) + + await act(async () => { + const [, setQueryParams] = result.current + setQueryParams({ amount: '10.5' }) + setQueryParams({ sourceChain: ChainId.ArbitrumOne }) + setQueryParams({ destinationChain: ChainId.Ethereum }) + setQueryParams({ token: '0xaf88d065e77c8cc2239327c5edb3a432268e5831' }) + vi.runOnlyPendingTimers() + }) + + expect(mockSetQueryParams).toHaveBeenCalledTimes(4) + expect(mockSetQueryParams).toHaveBeenNthCalledWith(1, { + amount: '10.5' + }) + expect(mockSetQueryParams).toHaveBeenNthCalledWith(2, { + sourceChain: ChainId.ArbitrumOne + }) + expect(mockSetQueryParams).toHaveBeenNthCalledWith(3, { + destinationChain: ChainId.Ethereum + }) + expect(mockSetQueryParams).toHaveBeenNthCalledWith(4, { + token: '0xaf88d065e77c8cc2239327c5edb3a432268e5831' + }) + }) + + it('should handle mixed object and function updates correctly', async () => { + const { result } = renderHook(() => useArbQueryParams(), { + wrapper: TestWrapper + }) + + await act(async () => { + const [, setQueryParams] = result.current + setQueryParams({ amount: '100' }, { debounce: true }) + setQueryParams({ sourceChain: ChainId.Base }, { debounce: true }) + setQueryParams( + { destinationChain: ChainId.ArbitrumOne }, + { debounce: true } + ) + setQueryParams( + { token: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' }, + { debounce: true } + ) + setQueryParams(prevState => ({ + ...prevState, + amount: (Number(prevState.amount) * 2).toString(), // 200 + amount2: '300' + })) + setQueryParams(prevState => ({ + ...prevState, + amount2: (Number(prevState.amount2) * 3).toString() // 900 + })) + vi.runOnlyPendingTimers() + }) + + expect(mockSetQueryParams).toHaveBeenCalledTimes(2) + expect(mockSetQueryParams).toHaveBeenCalledWith(expect.any(Function)) + + const setQueryParams = mockSetQueryParams.mock.calls[0]?.[0] + const secondSetQueryParams = mockSetQueryParams.mock.calls[1]?.[0] + + const expectedInputs = { + ...currentQueryParams, + amount: '100', + sourceChain: ChainId.Base, + destinationChain: ChainId.ArbitrumOne, + token: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' + } + + const setQueryParamsResult = setQueryParams(expectedInputs) + expect(setQueryParamsResult).toEqual({ + ...expectedInputs, + amount: '200', // 100 * 2 + amount2: '300' + }) + + const secondSetQueryParamsResult = + secondSetQueryParams(setQueryParamsResult) + expect(secondSetQueryParamsResult).toEqual({ + ...setQueryParamsResult, + amount: '200', + amount2: '900' + }) + }) +}) diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx b/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx index ec22f28565..5628ef99be 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx +++ b/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx @@ -13,138 +13,112 @@ `setQueryParams(newAmount)` */ +import { useCallback } from 'react' import queryString from 'query-string' -import NextAdapterPages from 'next-query-params/pages' +import NextAdapterApp from 'next-query-params/app' import { BooleanParam, + DecodedValueMap, + QueryParamConfigMap, + QueryParamOptions, QueryParamProvider, + SetQuery, StringParam, - decodeNumber, - decodeString, useQueryParams, withDefault } from 'use-query-params' +import { defaultTheme } from './useTheme' import { - ChainKeyQueryParam, - getChainForChainKeyQueryParam, - getChainQueryParamForChain, - isValidChainQueryParam -} from '../types/ChainQueryParam' -import { ChainId } from '../types/ChainId' -import { defaultTheme, ThemeConfig } from './useTheme' - -export enum TabParamEnum { - BRIDGE = 'bridge', - TX_HISTORY = 'tx_history' -} - -export enum DisabledFeatures { - BATCH_TRANSFERS = 'batch-transfers', - TX_HISTORY = 'tx-history', - NETWORK_SELECTION = 'network-selection', - TRANSFERS_TO_NON_ARBITRUM_CHAINS = 'transfers-to-non-arbitrum-chains' -} - -export enum AmountQueryParamEnum { - MAX = 'max' -} - -export enum ModeParamEnum { - EMBED = 'embed' - // add other modes when we have a use case for it -} - -export const tabToIndex = { - [TabParamEnum.BRIDGE]: 0, - [TabParamEnum.TX_HISTORY]: 1 -} as const satisfies Record - -export const indexToTab = { - 0: TabParamEnum.BRIDGE, - 1: TabParamEnum.TX_HISTORY -} as const satisfies Record - -export const isValidDisabledFeature = (feature: string) => { - return Object.values(DisabledFeatures).includes( - feature.toLowerCase() as DisabledFeatures - ) + TabParamEnum, + DisabledFeatures, + ModeParamEnum, + AmountQueryParamEnum, + tabToIndex, + indexToTab, + isValidDisabledFeature, + DisabledFeaturesParam, + encodeChainQueryParam, + decodeChainQueryParam, + encodeTabQueryParam, + decodeTabQueryParam, + ModeParam, + ThemeParam, + AmountQueryParam, + sanitizeAmountQueryParam, + TokenQueryParam, + ChainParam, + TabParam +} from '../util/queryParamUtils' + +export { + TabParamEnum, + DisabledFeatures, + ModeParamEnum, + AmountQueryParamEnum, + tabToIndex, + indexToTab, + isValidDisabledFeature, + DisabledFeaturesParam, + encodeChainQueryParam, + decodeChainQueryParam, + encodeTabQueryParam, + decodeTabQueryParam, + ThemeParam, + AmountQueryParam, + sanitizeAmountQueryParam, + TokenQueryParam, + ChainParam, + TabParam } -export const DisabledFeaturesParam = { - encode: (disabledFeatures: string[] | undefined) => { - if (!disabledFeatures?.length) { - return undefined +/** + * We use variables outside of the hook to share the accumulator accross multiple calls of useArbQueryParams + */ +let pendingUpdates: QueryParamConfigMap = {} +let debounceTimeout: NodeJS.Timeout | null = null +export type SetQueryParamsParameters = + | Partial> + | (( + latestValues: DecodedValueMap + ) => Partial>) + +const debouncedUpdateQueryParams = ( + updates: SetQueryParamsParameters, + originalSetQueryParams: SetQuery, + /** debounce only applies to object update, for function updates it will be called immediately */ + debounce: boolean = false +) => { + // Handle function update: setQueryParams((prevState) => ({ ...prevState, ...newUpdate })) + if (typeof updates === 'function') { + if (debounceTimeout) { + clearTimeout(debounceTimeout) + debounceTimeout = null } - const url = new URLSearchParams() - const dedupedFeatures = new Set( - disabledFeatures - .map(feature => feature.toLowerCase()) - .filter(feature => isValidDisabledFeature(feature)) + originalSetQueryParams(prevState => + updates({ ...prevState, ...pendingUpdates }) ) + pendingUpdates = {} + } else { + // Handle classic object updates: setQueryParams({ amount: "0.1" }) + pendingUpdates = { ...pendingUpdates, ...updates } - for (const feature of dedupedFeatures) { - url.append('disabledFeatures', feature) + if (debounceTimeout) { + clearTimeout(debounceTimeout) } - return url.toString() - }, - decode: (value: string | (string | null)[] | null | undefined) => { - if (!value) return [] - - // Handle both string and array inputs - const features = - typeof value === 'string' - ? [value] - : value.filter((val): val is string => val !== null) - - // Normalize, validate and deduplicate in one pass - const dedupedFeatures = new Set() - for (const feature of features) { - const normalized = feature.toLowerCase() - if (isValidDisabledFeature(normalized)) { - dedupedFeatures.add(normalized) - } + if (debounce) { + debounceTimeout = setTimeout(() => { + originalSetQueryParams(pendingUpdates) + pendingUpdates = {} + debounceTimeout = null + }, 400) + } else { + originalSetQueryParams(pendingUpdates) + pendingUpdates = {} + debounceTimeout = null } - - return Array.from(dedupedFeatures) - } -} - -export const ThemeParam = { - encode: (config: ThemeConfig | undefined) => { - if (!config) return undefined - try { - return encodeURIComponent(JSON.stringify(config)) // Encode the JSON string to handle special characters like # in hex colors - } catch { - return undefined - } - }, - decode: ( - configStr: string | (string | null)[] | null | undefined - ): ThemeConfig => { - if (!configStr || Array.isArray(configStr)) return defaultTheme - try { - const decodedTheme = JSON.parse(decodeURIComponent(configStr)) - return { ...defaultTheme, ...decodedTheme } - } catch { - return defaultTheme - } - } -} - -const ModeParam = { - encode: (mode: ModeParamEnum) => { - if (!mode) return undefined - return mode - }, - decode: (value: string | (string | null)[] | null | undefined) => { - const modeStr = value?.toString()?.toLowerCase() - if (modeStr === ModeParamEnum.EMBED) { - return modeStr - } - return undefined } } @@ -152,10 +126,29 @@ export const useArbQueryParams = () => { /* returns [ queryParams (getter for all query state variables), - setQueryParams (setter for all query state variables) + setQueryParams (setter for all query state variables with debounced accumulator) ] */ - return useQueryParams({ + const [queryParams, setQueryParams] = useQueryParams() + + const debouncedSetQueryParams = useCallback( + ( + updates: SetQueryParamsParameters, + { debounce }: { debounce?: boolean } = {} + ) => debouncedUpdateQueryParams(updates, setQueryParams, debounce), + [setQueryParams] + ) + + return [queryParams, debouncedSetQueryParams] as const +} + +export const queryParamProviderOptions: QueryParamOptions = { + searchStringToObject: queryString.parse, + objectToSearchString: queryString.stringify, + updateType: 'replaceIn', // replace just a single parameter when updating query-state, leaving the rest as is + removeDefaultsFromUrl: true, + enableBatching: true, + params: { sourceChain: ChainParam, destinationChain: ChainParam, amount: withDefault(AmountQueryParam, ''), // amount which is filled in Transfer panel @@ -163,158 +156,12 @@ export const useArbQueryParams = () => { destinationAddress: withDefault(StringParam, undefined), token: TokenQueryParam, // import a new token using a Dialog Box settingsOpen: withDefault(BooleanParam, false), - tab: withDefault(TabParam, tabToIndex[TabParamEnum.BRIDGE]), // which tab is active + tab: TabParam, // which tab is active disabledFeatures: withDefault(DisabledFeaturesParam, []), // disabled features in the bridge mode: withDefault(ModeParam, undefined), // mode: 'embed', or undefined for normal mode theme: withDefault(ThemeParam, defaultTheme) // theme customization - }) -} - -const isMax = (amount: string | undefined) => - amount?.toLowerCase() === AmountQueryParamEnum.MAX - -/** - * Sanitise amount value - * @param amount - transfer amount value from the input field or from the URL - * @returns sanitised value - */ -export const sanitizeAmountQueryParam = (amount: string) => { - // no need to process empty string - if (amount.length === 0) { - return amount } - - const parsedAmount = amount.replace(/[,]/g, '.').toLowerCase() - - // add 0 to values starting with . - if (parsedAmount.startsWith('.')) { - return `0${parsedAmount}` - } - - // to catch strings like `amount=asdf` from the URL - if (isNaN(Number(parsedAmount))) { - // return original string if the string is `max` (case-insensitive) - // it doesn't show on the input[type=number] field because it isn't in the allowed chars - return isMax(parsedAmount) ? parsedAmount : '' - } - - // to reach here they must be a number - // check for negative sign at first char - if (parsedAmount.startsWith('-')) { - return String(Math.abs(Number(parsedAmount))) - } - - // replace leading zeros and spaces - // this regex finds 1 or more 0s before any digits including 0 - // but the digits are not captured into the result string - return parsedAmount.replace(/(^0+(?=\d))| /g, '') -} - -// Our custom query param type for Amount field - will be parsed and returned as a string, -// but we need to make sure that only valid numeric-string values are considered, else return '0' -// Defined here so that components can directly rely on this for clean amount values and not rewrite parsing logic everywhere it gets used -export const AmountQueryParam = { - // type of amount is always string | undefined coming from the input element onChange event `e.target.value` - encode: (amount: string | undefined = '') => sanitizeAmountQueryParam(amount), - decode: (amount: string | (string | null)[] | null | undefined) => { - // toString() casts the potential string array into a string - const amountStr = amount?.toString() ?? '' - return sanitizeAmountQueryParam(amountStr) - } -} - -const TokenQueryParam = { - encode: (token: string | undefined) => { - return token?.toLowerCase() - }, - decode: (token: string | (string | null)[] | null | undefined) => { - const tokenStr = token?.toString() - // We are not checking for a valid address because we handle it in the UI - // by showing an invalid token dialog - return tokenStr?.toLowerCase() - } -} - -// Parse chainId to ChainQueryParam or ChainId for orbit chain -export function encodeChainQueryParam( - chainId: number | null | undefined -): string | undefined { - if (!chainId) { - return undefined - } - - try { - const chain = getChainQueryParamForChain(chainId) - return chain.toString() - } catch (e) { - return undefined - } -} - -function isValidNumber(value: number | null | undefined): value is number { - if (typeof value === 'undefined' || value === null) { - return false - } - - return !Number.isNaN(value) } - -// Parse ChainQueryParam/ChainId to ChainId -// URL accept both chainId and chainQueryParam (string) -export function decodeChainQueryParam( - value: string | (string | null)[] | null | undefined - // ChainId type doesn't include custom orbit chain, we need to add number type -): ChainId | number | undefined { - const valueString = decodeString(value) - if (!valueString) { - return undefined - } - - const valueNumber = decodeNumber(value) - if ( - isValidNumber(valueNumber) && - isValidChainQueryParam(valueNumber as ChainId) - ) { - return valueNumber - } - - if (isValidChainQueryParam(valueString)) { - return getChainForChainKeyQueryParam(valueString as ChainKeyQueryParam).id - } - - return undefined -} - -export const ChainParam = { - encode: encodeChainQueryParam, - decode: decodeChainQueryParam -} - -export function encodeTabQueryParam( - tabIndex: number | null | undefined -): string { - if (typeof tabIndex === 'number' && tabIndex in indexToTab) { - return indexToTab[tabIndex as keyof typeof indexToTab] - } - return TabParamEnum.BRIDGE -} - -// Parse string to number -// URL accepts string only -export function decodeTabQueryParam( - tab: string | (string | null)[] | null | undefined -): number { - if (typeof tab === 'string' && tab in tabToIndex) { - return tabToIndex[tab as TabParamEnum] - } - return tabToIndex[TabParamEnum.BRIDGE] -} - -export const TabParam = { - encode: encodeTabQueryParam, - decode: decodeTabQueryParam -} - export function ArbQueryParamProvider({ children }: { @@ -322,14 +169,8 @@ export function ArbQueryParamProvider({ }) { return ( {children} diff --git a/packages/arb-token-bridge-ui/src/hooks/useLifiCrossTransferRoute.ts b/packages/arb-token-bridge-ui/src/hooks/useLifiCrossTransferRoute.ts index 8aa2dcb97f..5c58664d35 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useLifiCrossTransferRoute.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useLifiCrossTransferRoute.ts @@ -3,7 +3,7 @@ import { getAPIBaseUrl } from '../util' import { LifiCrosschainTransfersRoute, LifiParams -} from '../pages/api/crosschain-transfers/lifi' +} from '@/bridge/app/api/crosschain-transfers/lifi' import { Address } from 'viem' import { useDebounce } from '@uidotdev/usehooks' diff --git a/packages/arb-token-bridge-ui/src/hooks/useNetworks.ts b/packages/arb-token-bridge-ui/src/hooks/useNetworks.ts index 4ec6dcdcf4..6a952eb6e7 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useNetworks.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useNetworks.ts @@ -1,182 +1,17 @@ import { StaticJsonRpcProvider } from '@ethersproject/providers' import { useCallback, useMemo } from 'react' -import { mainnet, arbitrum } from '@wagmi/core/chains' import { Chain } from 'wagmi/chains' import useSWRImmutable from 'swr/immutable' import { DisabledFeatures, useArbQueryParams } from './useArbQueryParams' -import { getCustomChainsFromLocalStorage, isNetwork } from '../util/networks' import { ChainId } from '../types/ChainId' -import { - sepolia, - arbitrumNova, - arbitrumSepolia, - localL1Network as local, - localL2Network as arbitrumLocal, - localL3Network as l3Local, - base, - baseSepolia -} from '../util/wagmi/wagmiAdditionalNetworks' - -import { getDestinationChainIds } from '../util/networks' import { getWagmiChain } from '../util/wagmi/getWagmiChain' -import { getOrbitChains } from '../util/orbitChainsList' import { getProviderForChainId } from '@/token-bridge-sdk/utils' import { useDisabledFeatures } from './useDisabledFeatures' -import { isLifiEnabled } from '../util/featureFlag' - -export function isSupportedChainId( - chainId: ChainId | undefined -): chainId is ChainId { - if (!chainId) { - return false - } - - const customChainIds = getCustomChainsFromLocalStorage().map( - chain => chain.chainId - ) - - return [ - mainnet.id, - sepolia.id, - arbitrum.id, - arbitrumNova.id, - base.id, - arbitrumSepolia.id, - baseSepolia.id, - arbitrumLocal.id, - l3Local.id, - local.id, - ...getOrbitChains().map(chain => chain.chainId), - ...customChainIds - ].includes(chainId) -} - -const cache: Record< - string, - { - sourceChainId: number - destinationChainId: number - } -> = {} -export function sanitizeQueryParams({ - sourceChainId, - destinationChainId, - disableTransfersToNonArbitrumChains = false, - includeLifiEnabledChainPairs = isLifiEnabled() -}: { - sourceChainId: ChainId | number | undefined - destinationChainId: ChainId | number | undefined - disableTransfersToNonArbitrumChains?: boolean - includeLifiEnabledChainPairs?: boolean -}): { - sourceChainId: ChainId | number - destinationChainId: ChainId | number -} { - const key = `${sourceChainId}-${destinationChainId}-${disableTransfersToNonArbitrumChains}-${includeLifiEnabledChainPairs}` - const cacheHit = cache[key] - if (cacheHit) { - return cacheHit - } - - if ( - (!sourceChainId && !destinationChainId) || - (!isSupportedChainId(sourceChainId) && - !isSupportedChainId(destinationChainId)) - ) { - // when both `sourceChain` and `destinationChain` are undefined or invalid, default to Ethereum and Arbitrum One - return (cache[key] = { - sourceChainId: ChainId.Ethereum, - destinationChainId: ChainId.ArbitrumOne - }) - } - - // destinationChainId is supported and sourceChainId is undefined - if ( - !isSupportedChainId(sourceChainId) && - isSupportedChainId(destinationChainId) - ) { - // case 1: the destination chain id is supported, but invalid in the context of the feature flag - const isInvalidDestinationChainId = - disableTransfersToNonArbitrumChains && - isNetwork(destinationChainId).isNonArbitrumNetwork - - // case 2: the destination chain id is supported and valid, but it doesn't have a source chain partner, eg. sourceChain=undefined and destinationChain=base - const [defaultSourceChainId] = getDestinationChainIds(destinationChainId, { - disableTransfersToNonArbitrumChains, - includeLifiEnabledChainPairs - }) - - // in both cases, we default to eth<>arbitrum-one pair - if ( - typeof defaultSourceChainId === 'undefined' || - isInvalidDestinationChainId - ) { - return (cache[key] = { - sourceChainId: ChainId.Ethereum, - destinationChainId: ChainId.ArbitrumOne - }) - } +import { isSupportedChainId } from '../util/chainUtils' +import { sanitizeQueryParams } from '../util/queryParamUtils' - return (cache[key] = { - sourceChainId: defaultSourceChainId, - destinationChainId - }) - } - - // sourceChainId is valid and destinationChainId is undefined - if ( - isSupportedChainId(sourceChainId) && - !isSupportedChainId(destinationChainId) - ) { - const [defaultDestinationChainId] = getDestinationChainIds(sourceChainId, { - includeLifiEnabledChainPairs, - disableTransfersToNonArbitrumChains - }) - - if (typeof defaultDestinationChainId === 'undefined') { - return (cache[key] = { - sourceChainId: ChainId.Ethereum, - destinationChainId: ChainId.ArbitrumOne - }) - } - - return (cache[key] = { - sourceChainId: sourceChainId, - destinationChainId: defaultDestinationChainId - }) - } - - // destinationChainId is not a partner of sourceChainId - if ( - !getDestinationChainIds(sourceChainId!, { - disableTransfersToNonArbitrumChains, - includeLifiEnabledChainPairs - }).includes(destinationChainId!) - ) { - const [defaultDestinationChainId] = getDestinationChainIds(sourceChainId!, { - disableTransfersToNonArbitrumChains, - includeLifiEnabledChainPairs - }) - - if (!defaultDestinationChainId) { - return (cache[key] = { - sourceChainId: ChainId.Ethereum, - destinationChainId: ChainId.ArbitrumOne - }) - } - - return (cache[key] = { - sourceChainId: sourceChainId!, - destinationChainId: defaultDestinationChainId! - }) - } - - return (cache[key] = { - sourceChainId: sourceChainId!, - destinationChainId: destinationChainId! - }) -} +export { isSupportedChainId, sanitizeQueryParams } export type UseNetworksState = { sourceChain: Chain diff --git a/packages/arb-token-bridge-ui/src/hooks/useNetworksRelationship.ts b/packages/arb-token-bridge-ui/src/hooks/useNetworksRelationship.ts index d7935f55cc..724c34d8c6 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useNetworksRelationship.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useNetworksRelationship.ts @@ -5,7 +5,7 @@ import { Chain } from 'wagmi/chains' import { UseNetworksState } from './useNetworks' import { isDepositMode } from '../util/isDepositMode' import { isValidTeleportChainPair } from '@/token-bridge-sdk/teleport' -import { isLifiTransfer } from '../pages/api/crosschain-transfers/utils' +import { isLifiTransfer } from '../app/api/crosschain-transfers/utils' import { ChainId } from '../types/ChainId' type UseNetworksRelationshipState = { diff --git a/packages/arb-token-bridge-ui/src/hooks/useSelectedToken.ts b/packages/arb-token-bridge-ui/src/hooks/useSelectedToken.ts index d95fd6ff09..8bfd937908 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useSelectedToken.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useSelectedToken.ts @@ -1,5 +1,5 @@ import { useCallback } from 'react' -import { constants, utils } from 'ethers' +import { utils } from 'ethers' import useSWRImmutable from 'swr/immutable' import { Provider } from '@ethersproject/providers' import { @@ -26,7 +26,9 @@ import { } from '../components/TransferPanel/TokenSearchUtils' import { useArbQueryParams } from './useArbQueryParams' import { ChainId } from '../types/ChainId' -import { getArbitrumNetwork } from '@arbitrum/sdk' +import { sanitizeNullSelectedToken } from '../util/queryParamUtils' + +export { sanitizeNullSelectedToken } from '../util/queryParamUtils' const commonUSDC: ERC20BridgeToken = { name: 'USD Coin', @@ -39,45 +41,6 @@ const commonUSDC: ERC20BridgeToken = { 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png' } -/** - * On orbit chains with custom fee token, if selectedToken is null, we default to the native token of the chain - * for transfer from and to the parent chain. - * And constants.Zero (ETH) otherwise - */ -export function sanitizeNullSelectedToken({ - sourceChainId, - destinationChainId, - erc20ParentAddress -}: { - sourceChainId: number | undefined - destinationChainId: number | undefined - erc20ParentAddress: string | null -}) { - if (!sourceChainId || !destinationChainId) { - return undefined - } - - try { - const destinationChain = getArbitrumNetwork(destinationChainId) - - // If the destination chain has a custom fee token, and selectedToken is null, - // return native token for deposit from the parent chain, ETH otherwise - if (destinationChain.nativeToken && !erc20ParentAddress) { - if (sourceChainId === destinationChain.parentChainId) { - return erc20ParentAddress - } - return constants.AddressZero - } - } catch (error) { - // Withdrawing to non Arbitrum chains (Base, Ethereum) - const sourceChain = getArbitrumNetwork(sourceChainId) - if (sourceChain.parentChainId === destinationChainId) { - return erc20ParentAddress - } - return constants.AddressZero - } -} - export const useSelectedToken = (): [ ERC20BridgeToken | null, (erc20ParentAddress: string | null) => void diff --git a/packages/arb-token-bridge-ui/src/hooks/useTheme.ts b/packages/arb-token-bridge-ui/src/hooks/useTheme.ts index c25653a43b..f8dafc11ba 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useTheme.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useTheme.ts @@ -1,16 +1,7 @@ import useSWRImmutable from 'swr/immutable' import { useArbQueryParams } from './useArbQueryParams' import { unica } from '../components/common/Font' - -// Theme configuration types -export interface ThemeConfig { - borderRadius?: string - widgetBackgroundColor?: string - borderWidth?: string - networkThemeOverrideColor?: string - primaryCtaColor?: string - fontFamily?: string -} +import { ThemeConfig } from '../util/queryParamUtils' export const defaultTheme: ThemeConfig = { borderRadius: '5px', diff --git a/packages/arb-token-bridge-ui/src/pages/_app.tsx b/packages/arb-token-bridge-ui/src/pages/_app.tsx deleted file mode 100644 index 7302b48e64..0000000000 --- a/packages/arb-token-bridge-ui/src/pages/_app.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import type { AppProps } from 'next/app' -import Head from 'next/head' -import posthog from 'posthog-js' -import dayjs from 'dayjs' -import relativeTime from 'dayjs/plugin/relativeTime' -import advancedFormat from 'dayjs/plugin/advancedFormat' -import timeZone from 'dayjs/plugin/timezone' -import utc from 'dayjs/plugin/utc' -import type { Chain } from 'wagmi/chains' - -import 'tippy.js/dist/tippy.css' -import 'tippy.js/themes/light.css' - -import '@rainbow-me/rainbowkit/styles.css' - -import '../styles/tailwind.css' -import { - ChainKeyQueryParam, - getChainForChainKeyQueryParam -} from '../types/ChainQueryParam' -import { isNetwork } from '../util/networks' -import { initializeSentry } from '../util/SentryUtils' -import { isProductionEnvironment } from '../util/CommonUtils' - -dayjs.extend(utc) -dayjs.extend(relativeTime) -dayjs.extend(timeZone) -dayjs.extend(advancedFormat) - -/** - * Initialize Sentry for error tracking - */ -initializeSentry(process.env.NEXT_PUBLIC_SENTRY_DSN) - -if ( - typeof window !== 'undefined' && - typeof process.env.NEXT_PUBLIC_POSTHOG_KEY === 'string' -) { - posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { - api_host: 'https://app.posthog.com', - loaded: posthog => { - if (!isProductionEnvironment) { - // when in dev, you can see data that would be sent in prod (in devtools) - posthog.debug() - } - }, - // store data in temporary memory that expires with each session - persistence: 'memory', - // by default posthog autocaptures (sends) events such as onClick, etc - // we set up our own events instead - autocapture: false, - disable_session_recording: true - }) -} - -function DynamicMetaData({ - sourceChainInfo, - destinationChainInfo -}: { - sourceChainInfo: Chain - destinationChainInfo: Chain -}) { - const { isOrbitChain: isSourceOrbitChain } = isNetwork(sourceChainInfo.id) - const { isOrbitChain: isDestinationOrbitChain } = isNetwork( - destinationChainInfo.id - ) - - const siteTitle = `Bridge to ${destinationChainInfo.name}` - - const siteDescription = `Bridge from ${sourceChainInfo.name} to ${destinationChainInfo.name} using the Arbitrum Bridge. Built to scale Ethereum, Arbitrum brings you 10x lower costs while inheriting Ethereum's security model. Arbitrum is a Layer 2 Optimistic Rollup.` - const siteDomain = 'https://bridge.arbitrum.io' - - let metaImagePath = `${sourceChainInfo.id}-to-${destinationChainInfo.id}.jpg` - - if (isSourceOrbitChain) { - metaImagePath = `${sourceChainInfo.id}.jpg` - } - - if (isDestinationOrbitChain) { - metaImagePath = `${destinationChainInfo.id}.jpg` - } - - return ( - <> - - - {/* */} - - - - - - - {/* */} - - - - - - - - ) -} - -export default function App({ Component, pageProps, router }: AppProps) { - const sourceChainSlug = (router.query.sourceChain?.toString() ?? - 'ethereum') as ChainKeyQueryParam - const destinationChainSlug = (router.query.destinationChain?.toString() ?? - 'arbitrum-one') as ChainKeyQueryParam - - let sourceChainInfo - let destinationChainInfo - - try { - sourceChainInfo = getChainForChainKeyQueryParam(sourceChainSlug) - destinationChainInfo = getChainForChainKeyQueryParam(destinationChainSlug) - } catch (error) { - // 1. slug misspelling can enter this flow - // 2. when user selects a custom orbit chain, it will also go to this flow (they are only available in local storage and not on the server) - console.warn( - `Could not resolve chain slugs: ${sourceChainSlug} / ${destinationChainSlug}. Defaulting.` - ) - sourceChainInfo = getChainForChainKeyQueryParam('ethereum') - destinationChainInfo = getChainForChainKeyQueryParam('arbitrum-one') - } - - const siteTitle = `Bridge to ${destinationChainInfo.name}` - - return ( - <> - - - - {/* title must be here because it doesn't render if it's in DynamicMetaData */} - {siteTitle} - - - - ) -} diff --git a/packages/arb-token-bridge-ui/src/pages/_document.tsx b/packages/arb-token-bridge-ui/src/pages/_document.tsx deleted file mode 100644 index cae930c3c6..0000000000 --- a/packages/arb-token-bridge-ui/src/pages/_document.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Html, Head, Main, NextScript } from 'next/document' - -export default function Document() { - return ( - - - - - - - - -
- - - - ) -} diff --git a/packages/arb-token-bridge-ui/src/pages/api/denylist.ts b/packages/arb-token-bridge-ui/src/pages/api/denylist.ts deleted file mode 100644 index da5c2e4c9a..0000000000 --- a/packages/arb-token-bridge-ui/src/pages/api/denylist.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next' -import denylist from '../../../public/__auto-generated-denylist.json' - -const ONE_WEEK_IN_SECONDS = 60 * 60 * 24 * 7 - -type Request = NextApiRequest & { query: { address: string } } - -export default async function handler( - req: Request, - res: NextApiResponse<{ data: boolean | undefined; message?: string }> -) { - try { - // validate method - if (req.method !== 'GET') { - res - .status(400) - .send({ message: `invalid_method: ${req.method}`, data: undefined }) - return - } - - const { address } = req.query - - if (typeof address !== 'string') { - res.status(400).send({ - message: `invalid_parameter: expected 'address' to be a string but got ${typeof address}`, - data: undefined - }) - return - } - - const isDenylisted = new Set(denylist.content).has(address.toLowerCase()) - - // https://vercel.com/docs/concepts/functions/serverless-functions/edge-caching#cache-control - // https://vercel.com/docs/concepts/functions/serverless-functions/edge-caching#recommended-cache-control - res.setHeader('Cache-Control', `max-age=0, s-maxage=${ONE_WEEK_IN_SECONDS}`) - - res.status(200).json({ data: isDenylisted }) - } catch (error: any) { - res.status(500).json({ - message: error?.message ?? 'Something went wrong', - data: undefined - }) - } -} diff --git a/packages/arb-token-bridge-ui/src/pages/index.tsx b/packages/arb-token-bridge-ui/src/pages/index.tsx deleted file mode 100644 index 749719f060..0000000000 --- a/packages/arb-token-bridge-ui/src/pages/index.tsx +++ /dev/null @@ -1,291 +0,0 @@ -import { ComponentType, useEffect, useState } from 'react' -import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next' -import dynamic from 'next/dynamic' -import { decodeString, encodeString } from 'use-query-params' -import { registerCustomArbitrumNetwork } from '@arbitrum/sdk' -import { constants } from 'ethers' - -import { Loader } from '../components/common/atoms/Loader' -import { - getCustomChainsFromLocalStorage, - mapCustomChainToNetworkData, - registerLocalNetwork -} from '../util/networks' -import { getOrbitChains, orbitChains } from '../util/orbitChainsList' -import { sanitizeQueryParams } from '../hooks/useNetworks' -import { - decodeChainQueryParam, - encodeChainQueryParam, - TabParamEnum, - DisabledFeaturesParam, - ModeParamEnum -} from '../hooks/useArbQueryParams' -import { sanitizeExperimentalFeaturesQueryParam } from '../util' -import { - isE2eTestingEnvironment, - isProductionEnvironment -} from '../util/CommonUtils' -import { sanitizeNullSelectedToken } from '../hooks/useSelectedToken' - -const App = dynamic( - () => { - return new Promise<{ default: ComponentType }>(async resolve => { - if (!isProductionEnvironment || isE2eTestingEnvironment) { - await registerLocalNetwork() - } - - const AppComponent = await import('../components/App/App') - resolve(AppComponent) - }) - }, - { - ssr: false, - loading: () => ( - <> -
-
- -
- - ) - } -) - -export const sanitizeTokenQueryParam = ({ - token, - sourceChainId, - destinationChainId -}: { - token: string | null | undefined - sourceChainId: number | undefined - destinationChainId: number | undefined -}) => { - const tokenLowercased = token?.toLowerCase() - - if (!tokenLowercased) { - const sanitizedTokenAddress = sanitizeNullSelectedToken({ - sourceChainId, - destinationChainId, - erc20ParentAddress: tokenLowercased || null - }) - - if (sanitizedTokenAddress) { - return sanitizedTokenAddress - } - } - if (!destinationChainId) { - return tokenLowercased - } - - const orbitChain = orbitChains[destinationChainId] - - const isOrbitChainWithCustomGasToken = - typeof orbitChain !== 'undefined' && - typeof orbitChain.nativeToken !== 'undefined' && - orbitChain.nativeToken !== constants.AddressZero - - // token=eth doesn't need to be set if ETH is the native gas token - // we strip it for clarity - if (tokenLowercased === 'eth' && !isOrbitChainWithCustomGasToken) { - return undefined - } - - return tokenLowercased -} - -export const sanitizeTabQueryParam = ( - tab: string | string[] | null | undefined -): string => { - const enumEntryNames = Object.keys(TabParamEnum) - - if (typeof tab === 'string' && enumEntryNames.includes(tab.toUpperCase())) { - return tab.toLowerCase() - } - - return TabParamEnum.BRIDGE -} - -function getDestinationWithSanitizedQueryParams( - sanitized: { - sourceChainId: number - destinationChainId: number - experiments: string | undefined - token: string | undefined - tab: string - disabledFeatures: string[] | undefined - mode: string | undefined - }, - query: GetServerSidePropsContext['query'] -) { - const params = new URLSearchParams() - - for (const key in query) { - // don't copy "sourceChain" and "destinationChain" query params - if ( - key === 'sourceChain' || - key === 'destinationChain' || - key === 'experiments' || - key === 'token' || - key === 'tab' || - key === 'disabledFeatures' || - key === 'mode' - ) { - continue - } - - const value = query[key] - - // copy everything else - if (typeof value === 'string') { - params.set(key, value) - } - } - - const encodedSource = encodeChainQueryParam(sanitized.sourceChainId) - const encodedDestination = encodeChainQueryParam(sanitized.destinationChainId) - const encodedExperiments = encodeString(sanitized.experiments) - const encodedToken = encodeString(sanitized.token) - const encodedTab = encodeString(sanitized.tab) - const encodedMode = encodeString(sanitized.mode) - - if (encodedSource) { - params.set('sourceChain', encodedSource) - - if (encodedDestination) { - params.set('destinationChain', encodedDestination) - } - } - - if (encodedExperiments) { - params.set('experiments', encodedExperiments) - } - - if (encodedToken) { - params.set('token', encodedToken) - } - - if (encodedTab) { - params.set('tab', encodedTab) - } - - if (encodedMode) { - params.set('mode', encodedMode) - } - - if (sanitized.disabledFeatures) { - for (const disabledFeature of sanitized.disabledFeatures) { - params.append('disabledFeatures', disabledFeature) - } - } - - return `/?${params.toString()}` -} - -function addOrbitChainsToArbitrumSDK() { - ;[...getOrbitChains(), ...getCustomChainsFromLocalStorage()].forEach( - chain => { - try { - registerCustomArbitrumNetwork(chain) - mapCustomChainToNetworkData(chain) - } catch (_) { - // already added - } - } - ) -} - -export async function getServerSideProps({ - query -}: GetServerSidePropsContext): Promise< - GetServerSidePropsResult> -> { - const sourceChainId = decodeChainQueryParam(query.sourceChain) - const destinationChainId = decodeChainQueryParam(query.destinationChain) - const experiments = decodeString(query.experiments) - const token = decodeString(query.token) - const tab = decodeString(query.tab) - const mode = decodeString(query.mode) - - // Parse disabled features string/array to array - const disabledFeatures = - typeof query.disabledFeatures === 'string' - ? [query.disabledFeatures] - : query.disabledFeatures - - // If both sourceChain and destinationChain are not present, let the client sync with Metamask - if (!sourceChainId && !destinationChainId) { - return { - props: {} - } - } - - if (!isProductionEnvironment || isE2eTestingEnvironment) { - await registerLocalNetwork() - } - // it's necessary to call this before sanitization to make sure all chains are registered - addOrbitChainsToArbitrumSDK() - - // sanitize the query params - const sanitizedChainIds = sanitizeQueryParams({ - sourceChainId, - destinationChainId, - disableTransfersToNonArbitrumChains: mode === ModeParamEnum.EMBED - }) - - const sanitized = { - ...sanitizedChainIds, - experiments: sanitizeExperimentalFeaturesQueryParam(experiments), - token: sanitizeTokenQueryParam({ - token, - sourceChainId: sanitizedChainIds.sourceChainId, - destinationChainId: sanitizedChainIds.destinationChainId - }), - tab: sanitizeTabQueryParam(tab), - disabledFeatures: DisabledFeaturesParam.decode(disabledFeatures), - mode: mode ? mode : undefined - } - - // if the sanitized query params are different from the initial values, redirect to the url with sanitized query params - if ( - sourceChainId !== sanitized.sourceChainId || - destinationChainId !== sanitized.destinationChainId || - experiments !== sanitized.experiments || - token !== sanitized.token || - tab !== sanitized.tab || - (disabledFeatures?.length || 0) !== sanitized.disabledFeatures.length || - mode !== sanitized.mode - ) { - console.log(`[getServerSideProps] sanitizing query params`) - console.log( - `[getServerSideProps] sourceChain=${sourceChainId}&destinationChain=${destinationChainId}&experiments=${experiments}&token=${token}&tab=${tab}&disabledFeatures=${disabledFeatures}&mode=${mode} (before)` - ) - console.log( - `[getServerSideProps] sourceChain=${sanitized.sourceChainId}&destinationChain=${sanitized.destinationChainId}&experiments=${sanitized.experiments}&token=${sanitized.token}&tab=${sanitized.tab}&disabledFeatures=${sanitized.disabledFeatures}&mode=${sanitized.mode} (after)` - ) - return { - redirect: { - permanent: false, - destination: getDestinationWithSanitizedQueryParams(sanitized, query) - } - } - } - - return { - props: {} - } -} - -export default function Index() { - const [loaded, setLoaded] = useState(false) - - useEffect(() => { - addOrbitChainsToArbitrumSDK() - setLoaded(true) - }, []) - - if (!loaded) { - return null - } - - return -} diff --git a/packages/arb-token-bridge-ui/src/state/cctpState.ts b/packages/arb-token-bridge-ui/src/state/cctpState.ts index d16d47478f..71f3e0ff06 100644 --- a/packages/arb-token-bridge-ui/src/state/cctpState.ts +++ b/packages/arb-token-bridge-ui/src/state/cctpState.ts @@ -18,7 +18,7 @@ import { CompletedCCTPTransfer, PendingCCTPTransfer, Response -} from '../pages/api/cctp/[type]' +} from '../app/api/cctp/[type]' import { CommonAddress } from '../util/CommonAddressUtils' import { trackEvent } from '../util/AnalyticsUtils' import { useAccountType } from '../hooks/useAccountType' diff --git a/packages/arb-token-bridge-ui/src/token-bridge-sdk/LifiTransferStarter.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/LifiTransferStarter.ts index 6b8a749a2c..688c37c210 100644 --- a/packages/arb-token-bridge-ui/src/token-bridge-sdk/LifiTransferStarter.ts +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/LifiTransferStarter.ts @@ -12,8 +12,8 @@ import { } from './BridgeTransferStarter' import { fetchErc20Allowance } from '../util/TokenUtils' import { Address } from 'viem' -import { TransactionRequest } from '../pages/api/crosschain-transfers/lifi' -import { Token } from '../pages/api/crosschain-transfers/types' +import { TransactionRequest } from '@/bridge/app/api/crosschain-transfers/lifi' +import { Token } from '@/bridge/app/api/crosschain-transfers/types' export type AmountWithToken = { amount: BigNumber diff --git a/packages/arb-token-bridge-ui/src/token-bridge-sdk/cctp.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/cctp.ts index 456ad24d8c..42ed3d6bde 100644 --- a/packages/arb-token-bridge-ui/src/token-bridge-sdk/cctp.ts +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/cctp.ts @@ -5,7 +5,7 @@ import { writeContract } from '@wagmi/core' import { TokenMinterAbi } from '../util/cctp/TokenMinterAbi' -import { ChainDomain } from '../pages/api/cctp/[type]' +import { ChainDomain } from '../app/api/cctp/[type]' import { MessageTransmitterAbi } from '../util/cctp/MessageTransmitterAbi' import { CCTPSupportedChainId } from '../state/cctpState' diff --git a/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts index 521121d218..c31a462b8b 100644 --- a/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts @@ -14,7 +14,7 @@ import { } from '@arbitrum/sdk' import { isDepositMode } from '../util/isDepositMode' import { EnhancedProvider } from './EnhancedProvider' -import { isValidLifiTransfer } from '../pages/api/crosschain-transfers/utils' +import { isValidLifiTransfer } from '../app/api/crosschain-transfers/utils' export const getAddressFromSigner = async (signer: Signer) => { const address = await signer.getAddress() diff --git a/packages/arb-token-bridge-ui/src/util/cctp/fetchCCTP.ts b/packages/arb-token-bridge-ui/src/util/cctp/fetchCCTP.ts index ea7c4f5b6e..f9f147bc61 100644 --- a/packages/arb-token-bridge-ui/src/util/cctp/fetchCCTP.ts +++ b/packages/arb-token-bridge-ui/src/util/cctp/fetchCCTP.ts @@ -4,7 +4,7 @@ import { CompletedCCTPTransfer, PendingCCTPTransfer, Response -} from '../../pages/api/cctp/[type]' +} from '../../app/api/cctp/[type]' import { utils } from 'ethers' export type FetchParams = { diff --git a/packages/arb-token-bridge-ui/src/util/chainUtils.ts b/packages/arb-token-bridge-ui/src/util/chainUtils.ts new file mode 100644 index 0000000000..b3f88a7841 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/util/chainUtils.ts @@ -0,0 +1,102 @@ +import { mainnet, arbitrum } from '@wagmi/core/chains' +import { + getChainByChainId, + getChildChainIds, + getCustomChainsFromLocalStorage, + isNetwork, + sortChainIds, + isArbitrumChain +} from './networks' +import { ChainId } from '../types/ChainId' +import { + sepolia, + arbitrumNova, + arbitrumSepolia, + localL1Network as local, + localL2Network as arbitrumLocal, + localL3Network as l3Local, + base, + baseSepolia +} from './wagmi/wagmiAdditionalNetworks' +import { getOrbitChains } from './orbitChainsList' +import { lifiDestinationChainIds } from '../app/api/crosschain-transfers/constants' + +export function isSupportedChainId( + chainId: ChainId | undefined +): chainId is ChainId { + if (!chainId) { + return false + } + + const customChainIds = getCustomChainsFromLocalStorage().map( + chain => chain.chainId + ) + + return [ + mainnet.id, + sepolia.id, + arbitrum.id, + arbitrumNova.id, + base.id, + arbitrumSepolia.id, + baseSepolia.id, + arbitrumLocal.id, + l3Local.id, + local.id, + ...getOrbitChains().map(chain => chain.chainId), + ...customChainIds + ].includes(chainId) +} + +export function getDestinationChainIds( + chainId: ChainId | number, + { + includeLifiEnabledChainPairs = false, + disableTransfersToNonArbitrumChains = false + }: { + includeLifiEnabledChainPairs?: boolean + disableTransfersToNonArbitrumChains?: boolean + } = {} +): ChainId[] { + const chain = getChainByChainId(chainId, { + includeRootChainsWithoutDestination: includeLifiEnabledChainPairs + }) + + if (!chain) { + return [] + } + + const parentChainId = isArbitrumChain(chain) ? chain.parentChainId : undefined + const chainIds = getChildChainIds(chain) + + /** + * Add parent chain if: + * - parent is an arbitrum network + * - parent is a non-arbitrum network and transfers to non-arbitrum chains are not disabled + */ + if ( + parentChainId && + (!isNetwork(parentChainId).isNonArbitrumNetwork || + (isNetwork(parentChainId).isNonArbitrumNetwork && + !disableTransfersToNonArbitrumChains)) + ) { + chainIds.push(parentChainId) + } + + /** Include lifi chains, if flag is on */ + const lifiChainIds = lifiDestinationChainIds[chainId] + if (includeLifiEnabledChainPairs && lifiChainIds && lifiChainIds.length) { + chainIds.push(...lifiChainIds) + } + + /** Disabling transfers to non arbitrum chains, remove non-arbitrum chains */ + if (disableTransfersToNonArbitrumChains) { + return sortChainIds([ + ...new Set( + chainIds.filter(chainId => !isNetwork(chainId).isNonArbitrumNetwork) + ) + ]) + } + + return sortChainIds([...new Set(chainIds)]) +} diff --git a/packages/arb-token-bridge-ui/src/util/networks.ts b/packages/arb-token-bridge-ui/src/util/networks.ts index 6a7255d1b7..a139d071c3 100644 --- a/packages/arb-token-bridge-ui/src/util/networks.ts +++ b/packages/arb-token-bridge-ui/src/util/networks.ts @@ -19,7 +19,7 @@ import { defaultL3CustomGasTokenNetwork } from './networksNitroTestnode' import { isE2eTestingEnvironment, isProductionEnvironment } from './CommonUtils' -import { lifiDestinationChainIds } from '../pages/api/crosschain-transfers/constants' +import { lifiDestinationChainIds } from '../app/api/crosschain-transfers/constants' /** The network that you reference when calling `block.number` in solidity */ type BlockNumberReferenceNetwork = { @@ -86,7 +86,7 @@ export const getChains = ( }) } -function getChainByChainId( +export function getChainByChainId( chainId: number, { includeRootChainsWithoutDestination } = { includeRootChainsWithoutDestination: false @@ -511,7 +511,7 @@ export function mapCustomChainToNetworkData(chain: ChainWithRpcUrl) { explorerUrls[chain.chainId] = chain.explorerUrl } -function isArbitrumChain( +export function isArbitrumChain( chain: BlockNumberReferenceNetwork | ArbitrumNetwork ): chain is ArbitrumNetwork { return typeof (chain as ArbitrumNetwork).parentChainId !== 'undefined' diff --git a/packages/arb-token-bridge-ui/src/util/queryParamUtils.ts b/packages/arb-token-bridge-ui/src/util/queryParamUtils.ts new file mode 100644 index 0000000000..90edcea4a5 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/util/queryParamUtils.ts @@ -0,0 +1,541 @@ +import { + ChainKeyQueryParam, + getChainForChainKeyQueryParam, + getChainQueryParamForChain, + isValidChainQueryParam +} from '../types/ChainQueryParam' +import { ChainId } from '../types/ChainId' +import { isSupportedChainId, getDestinationChainIds } from './chainUtils' +import { isNetwork } from './networks' +import { isLifiEnabled } from './featureFlag' +import { orbitChains } from './orbitChainsList' +import { constants } from 'ethers' +import { getArbitrumNetwork } from '@arbitrum/sdk' + +export interface ThemeConfig { + borderRadius?: string + widgetBackgroundColor?: string + borderWidth?: string + networkThemeOverrideColor?: string + primaryCtaColor?: string + fontFamily?: string +} + +export const defaultTheme: ThemeConfig = {} + +export enum AmountQueryParamEnum { + MAX = 'max' +} + +export enum TabParamEnum { + BRIDGE = 'bridge', + TX_HISTORY = 'tx_history' +} + +export enum DisabledFeatures { + BATCH_TRANSFERS = 'batch-transfers', + TX_HISTORY = 'tx-history', + NETWORK_SELECTION = 'network-selection', + TRANSFERS_TO_NON_ARBITRUM_CHAINS = 'transfers-to-non-arbitrum-chains' +} + +export enum ModeParamEnum { + EMBED = 'embed' +} + +export const tabToIndex = { + [TabParamEnum.BRIDGE]: 0, + [TabParamEnum.TX_HISTORY]: 1 +} as const satisfies Record + +export const indexToTab = { + 0: TabParamEnum.BRIDGE, + 1: TabParamEnum.TX_HISTORY +} as const satisfies Record + +export const isValidDisabledFeature = (feature: string) => { + return Object.values(DisabledFeatures).includes( + feature.toLowerCase() as DisabledFeatures + ) +} + +function isValidNumber(value: number | null | undefined): value is number { + if (typeof value === 'undefined' || value === null) { + return false + } + return !Number.isNaN(value) +} + +export function encodeChainQueryParam( + chainId: number | null | undefined +): string | undefined { + if (!chainId) { + return undefined + } + + try { + const chain = getChainQueryParamForChain(chainId) + return chain.toString() + } catch (e) { + return undefined + } +} + +/** + * Decodes a number from a string. If the number is invalid, + * it returns undefined. + * + * If an array is provided, only the first entry is used. + */ +function decodeNumber( + value: string | (string | null)[] | null | undefined +): number | null | undefined { + if (typeof value === 'string') { + const parsed = Number(value) + return isNaN(parsed) ? null : parsed + } + + if (Array.isArray(value)) { + const parsed = Number(value[0]) + return isNaN(parsed) ? null : parsed + } + + return value +} + +/** + * Decodes a string while safely handling null and undefined values. + * + * If an array is provided, only the first entry is used. + */ +function decodeString( + value: string | (string | null)[] | null | undefined +): string | null | undefined { + if (typeof value === 'string') { + return value + } + + if (Array.isArray(value)) { + return value[0] + } +} + +/** + * Encodes a string while safely handling null and undefined values. + */ +export function encodeString( + str: string | (string | null)[] | null | undefined +): string | null | undefined { + if (str == null) { + return str + } + + return String(str) +} + +export function decodeChainQueryParam( + value: string | (string | null)[] | null | undefined +): ChainId | number | undefined { + const valueString = decodeString(value) + if (!valueString) { + return undefined + } + + const valueNumber = decodeNumber(value) + if ( + isValidNumber(valueNumber) && + isValidChainQueryParam(valueNumber as ChainId) + ) { + return valueNumber + } + + if (isValidChainQueryParam(valueString)) { + return getChainForChainKeyQueryParam(valueString as ChainKeyQueryParam).id + } + + return undefined +} + +export function encodeTabQueryParam( + tabIndex: number | null | undefined +): string { + if (typeof tabIndex === 'number' && tabIndex in indexToTab) { + return indexToTab[tabIndex as keyof typeof indexToTab] + } + return TabParamEnum.BRIDGE +} + +export function decodeTabQueryParam( + tab: string | (string | null)[] | null | undefined +): number { + if (typeof tab === 'string' && tab in tabToIndex) { + return tabToIndex[tab as TabParamEnum] + } + return tabToIndex[TabParamEnum.BRIDGE] +} + +export const DisabledFeaturesParam = { + encode: (disabledFeatures: string[] | undefined) => { + if (!disabledFeatures?.length) { + return undefined + } + + const url = new URLSearchParams() + const dedupedFeatures = new Set( + disabledFeatures + .map(feature => feature.toLowerCase()) + .filter(feature => isValidDisabledFeature(feature)) + ) + + for (const feature of dedupedFeatures) { + url.append('disabledFeatures', feature) + } + + return url.toString() + }, + decode: (value: string | (string | null)[] | null | undefined) => { + if (!value) return [] + + // Handle both string and array inputs + const features = + typeof value === 'string' + ? [value] + : value.filter((val): val is string => val !== null) + + // Normalize, validate and deduplicate in one pass + const dedupedFeatures = new Set() + for (const feature of features) { + const normalized = feature.toLowerCase() + if (isValidDisabledFeature(normalized)) { + dedupedFeatures.add(normalized) + } + } + + return Array.from(dedupedFeatures) + } +} + +export const ModeParam = { + encode: (mode: ModeParamEnum) => { + if (!mode) return undefined + return mode + }, + decode: (value: string | (string | null)[] | null | undefined) => { + const modeStr = value?.toString()?.toLowerCase() + if (modeStr === ModeParamEnum.EMBED) { + return modeStr + } + return undefined + } +} + +export const ThemeParam = { + encode: (config: ThemeConfig | undefined) => { + if (!config) return undefined + try { + return encodeURIComponent(JSON.stringify(config)) // Encode the JSON string to handle special characters like # in hex colors + } catch { + return undefined + } + }, + decode: ( + configStr: string | (string | null)[] | null | undefined + ): ThemeConfig => { + if (!configStr || Array.isArray(configStr)) return defaultTheme + try { + const decodedTheme = JSON.parse(decodeURIComponent(configStr)) + return { ...defaultTheme, ...decodedTheme } + } catch { + return defaultTheme + } + } +} + +const isMax = (amount: string | undefined) => + amount?.toLowerCase() === AmountQueryParamEnum.MAX + +/** + * Sanitise amount value + * @param amount - transfer amount value from the input field or from the URL + * @returns sanitised value + */ +export const sanitizeAmountQueryParam = (amount: string) => { + // no need to process empty string + if (amount.length === 0) { + return amount + } + + const parsedAmount = amount.replace(/[,]/g, '.').toLowerCase() + + // add 0 to values starting with . + if (parsedAmount.startsWith('.')) { + return `0${parsedAmount}` + } + + // to catch strings like `amount=asdf` from the URL + if (isNaN(Number(parsedAmount))) { + // return original string if the string is `max` (case-insensitive) + // it doesn't show on the input[type=number] field because it isn't in the allowed chars + return isMax(parsedAmount) ? parsedAmount : '' + } + + // to reach here they must be a number + // check for negative sign at first char + if (parsedAmount.startsWith('-')) { + return String(Math.abs(Number(parsedAmount))) + } + + // replace leading zeros and spaces + // this regex finds 1 or more 0s before any digits including 0 + // but the digits are not captured into the result string + return parsedAmount.replace(/(^0+(?=\d))| /g, '') +} + +export const AmountQueryParam = { + // type of amount is always string | undefined coming from the input element onChange event `e.target.value` + encode: (amount: string | undefined = '') => sanitizeAmountQueryParam(amount), + decode: (amount: string | (string | null)[] | null | undefined) => { + // toString() casts the potential string array into a string + const amountStr = amount?.toString() ?? '' + return sanitizeAmountQueryParam(amountStr) + } +} + +export const TokenQueryParam = { + encode: (token: string | undefined) => { + return token?.toLowerCase() + }, + decode: (token: string | (string | null)[] | null | undefined) => { + const tokenStr = token?.toString() + // We are not checking for a valid address because we handle it in the UI + // by showing an invalid token dialog + return tokenStr?.toLowerCase() + } +} + +export const ChainParam = { + encode: encodeChainQueryParam, + decode: decodeChainQueryParam +} + +export const TabParam = { + encode: encodeTabQueryParam, + decode: decodeTabQueryParam +} + +const cache: Record< + string, + { + sourceChainId: number + destinationChainId: number + } +> = {} + +export function sanitizeQueryParams({ + sourceChainId, + destinationChainId, + disableTransfersToNonArbitrumChains = false, + includeLifiEnabledChainPairs = isLifiEnabled() +}: { + sourceChainId: ChainId | number | undefined + destinationChainId: ChainId | number | undefined + disableTransfersToNonArbitrumChains?: boolean + includeLifiEnabledChainPairs?: boolean +}): { + sourceChainId: ChainId | number + destinationChainId: ChainId | number +} { + const key = `${sourceChainId}-${destinationChainId}-${disableTransfersToNonArbitrumChains}-${includeLifiEnabledChainPairs}` + const cacheHit = cache[key] + if (cacheHit) { + return cacheHit + } + + if ( + (!sourceChainId && !destinationChainId) || + (!isSupportedChainId(sourceChainId) && + !isSupportedChainId(destinationChainId)) + ) { + // when both `sourceChain` and `destinationChain` are undefined or invalid, default to Ethereum and Arbitrum One + return (cache[key] = { + sourceChainId: ChainId.Ethereum, + destinationChainId: ChainId.ArbitrumOne + }) + } + + // destinationChainId is supported and sourceChainId is undefined + if ( + !isSupportedChainId(sourceChainId) && + isSupportedChainId(destinationChainId) + ) { + // case 1: the destination chain id is supported, but invalid in the context of the feature flag + const isInvalidDestinationChainId = + disableTransfersToNonArbitrumChains && + isNetwork(destinationChainId).isNonArbitrumNetwork + + // case 2: the destination chain id is supported and valid, but it doesn't have a source chain partner, eg. sourceChain=undefined and destinationChain=base + const [defaultSourceChainId] = getDestinationChainIds(destinationChainId, { + disableTransfersToNonArbitrumChains, + includeLifiEnabledChainPairs + }) + + // in both cases, we default to eth<>arbitrum-one pair + if ( + typeof defaultSourceChainId === 'undefined' || + isInvalidDestinationChainId + ) { + return (cache[key] = { + sourceChainId: ChainId.Ethereum, + destinationChainId: ChainId.ArbitrumOne + }) + } + + return (cache[key] = { + sourceChainId: defaultSourceChainId, + destinationChainId + }) + } + + // sourceChainId is valid and destinationChainId is undefined + if ( + isSupportedChainId(sourceChainId) && + !isSupportedChainId(destinationChainId) + ) { + const [defaultDestinationChainId] = getDestinationChainIds(sourceChainId, { + includeLifiEnabledChainPairs, + disableTransfersToNonArbitrumChains + }) + + if (typeof defaultDestinationChainId === 'undefined') { + return (cache[key] = { + sourceChainId: ChainId.Ethereum, + destinationChainId: ChainId.ArbitrumOne + }) + } + + return (cache[key] = { + sourceChainId: sourceChainId, + destinationChainId: defaultDestinationChainId + }) + } + + // destinationChainId is not a partner of sourceChainId + if ( + !getDestinationChainIds(sourceChainId!, { + disableTransfersToNonArbitrumChains, + includeLifiEnabledChainPairs + }).includes(destinationChainId!) + ) { + const [defaultDestinationChainId] = getDestinationChainIds(sourceChainId!, { + disableTransfersToNonArbitrumChains, + includeLifiEnabledChainPairs + }) + + if (!defaultDestinationChainId) { + return (cache[key] = { + sourceChainId: ChainId.Ethereum, + destinationChainId: ChainId.ArbitrumOne + }) + } + + return (cache[key] = { + sourceChainId: sourceChainId!, + destinationChainId: defaultDestinationChainId! + }) + } + + return (cache[key] = { + sourceChainId: sourceChainId!, + destinationChainId: destinationChainId! + }) +} + +export function sanitizeNullSelectedToken({ + sourceChainId, + destinationChainId, + erc20ParentAddress +}: { + sourceChainId: number | undefined + destinationChainId: number | undefined + erc20ParentAddress: string | null +}) { + if (!sourceChainId || !destinationChainId) { + return undefined + } + + try { + const destinationChain = getArbitrumNetwork(destinationChainId) + + // If the destination chain has a custom fee token, and selectedToken is null, + // return native token for deposit from the parent chain, ETH otherwise + if (destinationChain.nativeToken && !erc20ParentAddress) { + if (sourceChainId === destinationChain.parentChainId) { + return erc20ParentAddress + } + + return constants.AddressZero + } + } catch (error) { + // Withdrawing to non Arbitrum chains (Base, Ethereum) + const sourceChain = getArbitrumNetwork(sourceChainId) + if (sourceChain.parentChainId === destinationChainId) { + return erc20ParentAddress + } + + return constants.AddressZero + } +} + +export const sanitizeTokenQueryParam = ({ + token, + sourceChainId, + destinationChainId +}: { + token: string | null | undefined + sourceChainId: number | undefined + destinationChainId: number | undefined +}) => { + const tokenLowercased = token?.toLowerCase() + + if (!tokenLowercased) { + const sanitizedTokenAddress = sanitizeNullSelectedToken({ + sourceChainId, + destinationChainId, + erc20ParentAddress: tokenLowercased || null + }) + + if (sanitizedTokenAddress) { + return sanitizedTokenAddress + } + } + if (!destinationChainId) { + return tokenLowercased + } + + const orbitChain = orbitChains[destinationChainId] + + const isOrbitChainWithCustomGasToken = + typeof orbitChain !== 'undefined' && + typeof orbitChain.nativeToken !== 'undefined' && + orbitChain.nativeToken !== constants.AddressZero + + // token=eth doesn't need to be set if ETH is the native gas token + // we strip it for clarity + if (tokenLowercased === 'eth' && !isOrbitChainWithCustomGasToken) { + return undefined + } + + return tokenLowercased +} + +export const sanitizeTabQueryParam = ( + tab: string | string[] | null | undefined +): string => { + const enumEntryNames = Object.keys(TabParamEnum) + + if (typeof tab === 'string' && enumEntryNames.includes(tab.toUpperCase())) { + return tab.toLowerCase() + } + + return TabParamEnum.BRIDGE +} diff --git a/packages/arb-token-bridge-ui/tests/support/commands.ts b/packages/arb-token-bridge-ui/tests/support/commands.ts index 3d3801e53c..dd2ddbf239 100644 --- a/packages/arb-token-bridge-ui/tests/support/commands.ts +++ b/packages/arb-token-bridge-ui/tests/support/commands.ts @@ -151,13 +151,19 @@ export function findAmount2Input(): Cypress.Chainable> { export function typeAmount( amount: string | number ): Cypress.Chainable> { - return cy.findAmountInput().scrollIntoView().type(String(amount)) + return cy + .findAmountInput() + .scrollIntoView() + .type(String(amount), { delay: 0 }) } export function typeAmount2( amount: string | number ): Cypress.Chainable> { - return cy.findAmount2Input().scrollIntoView().type(String(amount)) + return cy + .findAmount2Input() + .scrollIntoView() + .type(String(amount), { delay: 0 }) } export function findSourceChainButton( @@ -179,10 +185,17 @@ export function findDestinationChainButton( ) } -export function findGasFeeSummary( - amount: string | number | RegExp -): Cypress.Chainable> { - return cy.findByLabelText('Route gas').should('contain', amount) +export function findGasFeeSummary(amount: string | number | RegExp) { + return cy + .findByLabelText('Route gas') + .invoke('text') + .then(text => { + if (typeof amount === 'string' || typeof amount === 'number') { + expect(text).to.eq(amount) + } else { + expect(text).to.match(amount) + } + }) } export function findMoveFundsButton(): Cypress.Chainable> { @@ -217,7 +230,8 @@ export function findSelectTokenButton( .should('have.text', text) } -export function switchToTransferPanelTab() { +export async function switchToTransferPanelTab() { + await cy.wait(1_000) return cy.findByLabelText('Switch to Bridge Tab').click() } diff --git a/packages/arb-token-bridge-ui/tsconfig.json b/packages/arb-token-bridge-ui/tsconfig.json index d71387ca36..0dfa56f2d8 100644 --- a/packages/arb-token-bridge-ui/tsconfig.json +++ b/packages/arb-token-bridge-ui/tsconfig.json @@ -3,13 +3,8 @@ "include": [ "src", "additional.d.ts", - "postcss.config.js", - "prettier.config.js", - "tailwind.config.js", - "next.config.js", "next-env.d.ts", ".next/types/**/*.ts", - "build/types/**/*.ts", "vitest.config.ts", "vitest.mocks.ts", "src/generateOpenGraphImages.tsx" @@ -18,11 +13,6 @@ "noEmit": true, "incremental": true, "jsx": "preserve", - "paths": { - "@/images/*": ["./public/images/*"], - "@/icons/*": ["./public/icons/*"], - "@/token-bridge-sdk/*": ["./src/token-bridge-sdk/*"] - }, "plugins": [ { "name": "next" diff --git a/packages/arb-token-bridge-ui/vitest.config.ts b/packages/arb-token-bridge-ui/vitest.config.ts index 5680ab8d75..fbd3ad27cc 100644 --- a/packages/arb-token-bridge-ui/vitest.config.ts +++ b/packages/arb-token-bridge-ui/vitest.config.ts @@ -17,8 +17,8 @@ export default defineConfig({ }, resolve: { alias: { - '@/images': path.resolve(__dirname, './public/images'), - '@/icons': path.resolve(__dirname, './public/icons'), + '@/images': path.resolve(__dirname, '../app/public/images'), + '@/icons': path.resolve(__dirname, '../app/public/icons'), '@/token-bridge-sdk': path.resolve(__dirname, './src/token-bridge-sdk') } } diff --git a/packages/scripts/src/addOrbitChain/github.ts b/packages/scripts/src/addOrbitChain/github.ts index fc36c08008..3c6b958249 100644 --- a/packages/scripts/src/addOrbitChain/github.ts +++ b/packages/scripts/src/addOrbitChain/github.ts @@ -1,12 +1,12 @@ -import { context, getOctokit } from "@actions/github"; -import { Issue } from "./schemas"; +import { context, getOctokit } from '@actions/github' +import { Issue } from './schemas' -const github = getOctokit(process.env.GITHUB_TOKEN || ""); +const github = getOctokit(process.env.GITHUB_TOKEN || '') export const getIssue = async (issueNumber: string): Promise => { const response = await github.rest.issues.get({ ...context.repo, - issue_number: Number(issueNumber), - }); - return response.data as Issue; -}; + issue_number: Number(issueNumber) + }) + return response.data as Issue +} diff --git a/packages/scripts/src/addOrbitChain/index.ts b/packages/scripts/src/addOrbitChain/index.ts index cf210cdc47..92230d5c1c 100644 --- a/packages/scripts/src/addOrbitChain/index.ts +++ b/packages/scripts/src/addOrbitChain/index.ts @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import * as core from "@actions/core"; +import * as core from '@actions/core' import { initializeAndFetchData, processChainData, handleImages, createAndValidateOrbitChain, - updateOrbitChainsList, -} from "./transforms"; + updateOrbitChainsList +} from './transforms' /** * Main function to add an Orbit chain @@ -14,24 +14,24 @@ import { */ export async function addOrbitChain(targetJsonPath: string): Promise { try { - await initializeAndFetchData(); + await initializeAndFetchData() - const { branchName, validatedIncomingData } = await processChainData(); + const { branchName, validatedIncomingData } = await processChainData() const { chainLogoPath, nativeTokenLogoPath } = await handleImages( branchName, validatedIncomingData - ); + ) const orbitChain = await createAndValidateOrbitChain( validatedIncomingData, chainLogoPath, nativeTokenLogoPath - ); + ) - await updateOrbitChainsList(orbitChain, targetJsonPath); + await updateOrbitChainsList(orbitChain, targetJsonPath) } catch (error) { - core.setFailed(`Error in addOrbitChain: ${error}`); - throw error; + core.setFailed(`Error in addOrbitChain: ${error}`) + throw error } } diff --git a/packages/scripts/src/addOrbitChain/provider.ts b/packages/scripts/src/addOrbitChain/provider.ts index d7287dedb7..20f437bc4c 100644 --- a/packages/scripts/src/addOrbitChain/provider.ts +++ b/packages/scripts/src/addOrbitChain/provider.ts @@ -1,12 +1,12 @@ -import { StaticJsonRpcProvider } from "@ethersproject/providers"; -import { ConnectionInfo } from "ethers/lib/utils"; +import { StaticJsonRpcProvider } from '@ethersproject/providers' +import { ConnectionInfo } from 'ethers/lib/utils' export const getProvider = (chainInfo: { - rpcUrl: string; - name: string; - chainId: number; + rpcUrl: string + name: string + chainId: number }) => { - const THROTTLE_LIMIT = 10; + const THROTTLE_LIMIT = 10 const connection: ConnectionInfo = { url: chainInfo.rpcUrl, @@ -19,18 +19,18 @@ export const getProvider = (chainInfo: { // Always retry until we hit the THROTTLE_LIMIT // Otherwise, it only throttles for specific response codes // Return true to continue retrying, false to stop - return attempt <= THROTTLE_LIMIT; + return attempt <= THROTTLE_LIMIT }, headers: { - Accept: "*/*", - "Accept-Encoding": "gzip, deflate, br", - }, - }; + Accept: '*/*', + 'Accept-Encoding': 'gzip, deflate, br' + } + } const provider = new StaticJsonRpcProvider(connection, { name: chainInfo.name, - chainId: chainInfo.chainId, - }); + chainId: chainInfo.chainId + }) - return provider; -}; + return provider +} diff --git a/packages/scripts/src/addOrbitChain/schemas.ts b/packages/scripts/src/addOrbitChain/schemas.ts index 008c246fa4..aae93bff49 100644 --- a/packages/scripts/src/addOrbitChain/schemas.ts +++ b/packages/scripts/src/addOrbitChain/schemas.ts @@ -1,121 +1,121 @@ -import { z } from "zod"; -import { constants } from "ethers"; -import { warning } from "@actions/core"; -import { getOctokit } from "@actions/github"; -import path from "path"; -import * as dotenv from "dotenv"; -import { getProvider } from "./provider"; +import { z } from 'zod' +import { constants } from 'ethers' +import { warning } from '@actions/core' +import { getOctokit } from '@actions/github' +import path from 'path' +import * as dotenv from 'dotenv' +import { getProvider } from './provider' // Load .env from the UI project directory dotenv.config({ - path: path.resolve(__dirname, "../../../arb-token-bridge-ui/.env"), -}); -export const TESTNET_PARENT_CHAIN_IDS = [11155111, 421614, 84532]; -const ZERO_ADDRESS = constants.AddressZero; + path: path.resolve(__dirname, '../../../arb-token-bridge-ui/.env') +}) +export const TESTNET_PARENT_CHAIN_IDS = [11155111, 421614, 84532] +const ZERO_ADDRESS = constants.AddressZero export const getParentChainInfo = (parentChainId: number) => { - const INFURA_KEY = process.env.NEXT_PUBLIC_INFURA_KEY; + const INFURA_KEY = process.env.NEXT_PUBLIC_INFURA_KEY switch (parentChainId) { case 1: // Ethereum Mainnet return { rpcUrl: INFURA_KEY ? `https://mainnet.infura.io/v3/${INFURA_KEY}` - : "https://eth.llamarpc.com", - blockExplorer: "https://etherscan.io", + : 'https://eth.llamarpc.com', + blockExplorer: 'https://etherscan.io', chainId: 1, - name: "Ethereum", - }; + name: 'Ethereum' + } case 42161: // Arbitrum One return { rpcUrl: INFURA_KEY ? `https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}` - : "https://arb1.arbitrum.io/rpc", - blockExplorer: "https://arbiscan.io", + : 'https://arb1.arbitrum.io/rpc', + blockExplorer: 'https://arbiscan.io', chainId: 42161, - name: "Arbitrum One", - }; + name: 'Arbitrum One' + } case 42170: // Arbitrum Nova return { - rpcUrl: "https://nova.arbitrum.io/rpc", - blockExplorer: "https://nova.arbiscan.io", + rpcUrl: 'https://nova.arbitrum.io/rpc', + blockExplorer: 'https://nova.arbiscan.io', chainId: 42170, - name: "Arbitrum Nova", - }; + name: 'Arbitrum Nova' + } case 11155111: // Sepolia return { rpcUrl: INFURA_KEY ? `https://sepolia.infura.io/v3/${INFURA_KEY}` - : "https://ethereum-sepolia-rpc.publicnode.com", - blockExplorer: "https://sepolia.etherscan.io", + : 'https://ethereum-sepolia-rpc.publicnode.com', + blockExplorer: 'https://sepolia.etherscan.io', chainId: 11155111, - name: "Sepolia", - }; + name: 'Sepolia' + } case 421614: // Arbitrum Sepolia return { rpcUrl: INFURA_KEY ? `https://arbitrum-sepolia.infura.io/v3/${INFURA_KEY}` - : "https://sepolia-rollup.arbitrum.io/rpc", - blockExplorer: "https://sepolia.arbiscan.io", + : 'https://sepolia-rollup.arbitrum.io/rpc', + blockExplorer: 'https://sepolia.arbiscan.io', chainId: 421614, - name: "Arbitrum Sepolia", - }; + name: 'Arbitrum Sepolia' + } case 8453: // Base return { - rpcUrl: "https://mainnet.base.org", - blockExplorer: "https://basescan.io", + rpcUrl: 'https://mainnet.base.org', + blockExplorer: 'https://basescan.io', chainId: 8453, - name: "Base", - }; + name: 'Base' + } case 84532: // Base Sepolia return { - rpcUrl: "https://sepolia.base.org", - blockExplorer: "https://sepolia.basescan.io", + rpcUrl: 'https://sepolia.base.org', + blockExplorer: 'https://sepolia.basescan.io', chainId: 84532, - name: "Base Sepolia", - }; + name: 'Base Sepolia' + } default: - throw new Error(`Unsupported parent chain ID: ${parentChainId}`); + throw new Error(`Unsupported parent chain ID: ${parentChainId}`) } -}; +} export const isValidAddress = (address: string): boolean => { - return /^0x[a-fA-F0-9]{40}$/.test(address); -}; + return /^0x[a-fA-F0-9]{40}$/.test(address) +} export const addressSchema = z.string().refine(isValidAddress, { - message: "Invalid Ethereum address", -}); + message: 'Invalid Ethereum address' +}) export const urlSchema = z .string() - .url({ message: "Invalid URL format." }) - .refine((url) => url.startsWith("https://"), { - message: "URL must start with https://.", + .url({ message: 'Invalid URL format.' }) + .refine(url => url.startsWith('https://'), { + message: 'URL must start with https://.' }) - .transform((url) => (url.endsWith("/") ? url.slice(0, -1) : url)); + .transform(url => (url.endsWith('/') ? url.slice(0, -1) : url)) export const colorHexSchema = z .string() - .regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/, "Invalid color hex"); + .regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/, 'Invalid color hex') export const descriptionSchema = z .string() .optional() - .transform((desc) => { + .transform(desc => { if (!desc) { - return desc; + return desc } - return desc.endsWith(".") ? desc : `${desc}.`; - }); + return desc.endsWith('.') ? desc : `${desc}.` + }) export const ethBridgeSchema = z.object({ bridge: addressSchema, inbox: addressSchema, outbox: addressSchema, rollup: addressSchema, - sequencerInbox: addressSchema, -}); + sequencerInbox: addressSchema +}) export const tokenBridgeSchema = z.object({ parentCustomGateway: addressSchema, @@ -131,25 +131,25 @@ export const tokenBridgeSchema = z.object({ childMultiCall: addressSchema.optional(), childProxyAdmin: addressSchema.optional().default(ZERO_ADDRESS), childWeth: addressSchema, - childWethGateway: addressSchema, -}); + childWethGateway: addressSchema +}) export const bridgeUiConfigSchema = z.object({ color: colorHexSchema, network: z.object({ name: z.string().min(1), logo: z.string().optional(), - description: descriptionSchema, + description: descriptionSchema }), nativeTokenData: z .object({ name: z.string().min(1), symbol: z.string().min(1), - logoUrl: z.string().optional(), + logoUrl: z.string().optional() }) .optional(), - fastWithdrawalTime: z.number().int().positive().optional(), -}); + fastWithdrawalTime: z.number().int().positive().optional() +}) export const chainSchema = z .object({ @@ -165,10 +165,10 @@ export const chainSchema = z slug: z.string().min(1), parentChainId: z.number().int().positive(), tokenBridge: tokenBridgeSchema, - bridgeUiConfig: bridgeUiConfigSchema, + bridgeUiConfig: bridgeUiConfigSchema }) .superRefine(async (chain, ctx) => { - const parentChainInfo = getParentChainInfo(chain.parentChainId); + const parentChainInfo = getParentChainInfo(chain.parentChainId) const parentAddressesToCheck = [ chain.ethBridge.bridge, @@ -181,21 +181,21 @@ export const chainSchema = z chain.tokenBridge.parentGatewayRouter, chain.tokenBridge.parentMultiCall, chain.tokenBridge.parentWeth, - chain.tokenBridge.parentWethGateway, + chain.tokenBridge.parentWethGateway ].filter( (address): address is string => - typeof address === "string" && address !== ZERO_ADDRESS - ); + typeof address === 'string' && address !== ZERO_ADDRESS + ) const childAddressesToCheck = [ chain.tokenBridge.childCustomGateway, chain.tokenBridge.childErc20Gateway, chain.tokenBridge.childGatewayRouter, - chain.tokenBridge.childMultiCall, + chain.tokenBridge.childMultiCall ].filter( (address): address is string => - typeof address === "string" && address !== ZERO_ADDRESS - ); + typeof address === 'string' && address !== ZERO_ADDRESS + ) const checkAddresses = async ( addresses: string[], @@ -204,32 +204,32 @@ export const chainSchema = z chainId: number, chainName: string ) => { - const provider = getProvider({ rpcUrl, name: chainName, chainId }); + const provider = getProvider({ rpcUrl, name: chainName, chainId }) for (const address of addresses) { try { - const code = await provider.getCode(address); - if (code === "0x") { - throw new Error("Address is not a contract"); + const code = await provider.getCode(address) + if (code === '0x') { + throw new Error('Address is not a contract') } } catch (error) { - const explorerLink = `${blockExplorer}/address/${address}`; + const explorerLink = `${blockExplorer}/address/${address}` const warningMsg = ` Failed to verify contract at ${address} on ${chainName} -Please verify contract manually by visiting ${explorerLink}`; - console.error(warningMsg + `\n\n==================\n\n${error}`); - warning(warningMsg); +Please verify contract manually by visiting ${explorerLink}` + console.error(warningMsg + `\n\n==================\n\n${error}`) + warning(warningMsg) ctx.addIssue({ code: z.ZodIssueCode.custom, - message: warningMsg, - }); + message: warningMsg + }) } } - }; + } - chain.isTestnet = TESTNET_PARENT_CHAIN_IDS.includes(chain.parentChainId); + chain.isTestnet = TESTNET_PARENT_CHAIN_IDS.includes(chain.parentChainId) await checkAddresses( parentAddressesToCheck, @@ -237,26 +237,26 @@ Please verify contract manually by visiting ${explorerLink}`; parentChainInfo.blockExplorer, parentChainInfo.chainId, parentChainInfo.name - ); + ) await checkAddresses( childAddressesToCheck, chain.rpcUrl, chain.explorerUrl, chain.chainId, chain.name - ); - }); + ) + }) export type OrbitChainsList = { - mainnet: OrbitChain[]; - testnet: OrbitChain[]; -}; + mainnet: OrbitChain[] + testnet: OrbitChain[] +} // Update the orbitChainsListSchema export const orbitChainsListSchema = z.object({ mainnet: z.array(chainSchema), - testnet: z.array(chainSchema), -}); + testnet: z.array(chainSchema) +}) // Schema for incoming data from GitHub issue export const incomingChainDataSchema = z.object({ @@ -286,77 +286,77 @@ export const incomingChainDataSchema = z.object({ parentMultiCall: addressSchema, childMultiCall: addressSchema, fastWithdrawalActive: z.boolean(), - fastWithdrawalMinutes: z.string().regex(/^\d+$/).optional(), -}); + fastWithdrawalMinutes: z.string().regex(/^\d+$/).optional() +}) // Schema for the final OrbitChain structure -export const orbitChainSchema = chainSchema; +export const orbitChainSchema = chainSchema export const validateIncomingChainData = async ( rawData: unknown ): Promise => { try { - return await incomingChainDataSchema.parseAsync(rawData); + return await incomingChainDataSchema.parseAsync(rawData) } catch (error) { if (error instanceof z.ZodError) { - console.error("Validation errors:"); - error.errors.forEach((err) => { - console.error(`${err.path.join(".")}: ${err.message}`); - }); + console.error('Validation errors:') + error.errors.forEach(err => { + console.error(`${err.path.join('.')}: ${err.message}`) + }) } - throw error; + throw error } -}; +} export const validateOrbitChain = async (chainData: unknown) => { - return await chainSchema.parseAsync(chainData); -}; + return await chainSchema.parseAsync(chainData) +} export const chainDataLabelToKey: Record = { - "Chain ID": "chainId", - "Chain name": "name", - "Chain description": "description", - "Chain logo": "chainLogo", - "Brand color": "color", - "RPC URL": "rpcUrl", - "Explorer URL": "explorerUrl", - "Parent chain ID": "parentChainId", - "Native token address on Parent Chain": "nativeTokenAddress", - "Native token name": "nativeTokenName", - "Native token symbol": "nativeTokenSymbol", - "Native token logo": "nativeTokenLogo", - rollup: "rollup", - "Parent Gateway Router": "parentGatewayRouter", - "Child Gateway Router": "childGatewayRouter", - "Parent ERC20 Gateway": "parentErc20Gateway", - "Child ERC20 Gateway": "childErc20Gateway", - "Parent Custom Gateway": "parentCustomGateway", - "Child Custom Gateway": "childCustomGateway", - "Parent WETH Gateway": "parentWethGateway", - "Child WETH Gateway": "childWethGateway", - "Child WETH": "childWeth", - "Parent MultiCall": "parentMultiCall", - "Child MultiCall": "childMultiCall", - "Parent WETH": "parentWeth", - "Fast Withdrawals active": "fastWithdrawalActive", - "Fast Withdrawals time in minutes": "fastWithdrawalMinutes", -}; + 'Chain ID': 'chainId', + 'Chain name': 'name', + 'Chain description': 'description', + 'Chain logo': 'chainLogo', + 'Brand color': 'color', + 'RPC URL': 'rpcUrl', + 'Explorer URL': 'explorerUrl', + 'Parent chain ID': 'parentChainId', + 'Native token address on Parent Chain': 'nativeTokenAddress', + 'Native token name': 'nativeTokenName', + 'Native token symbol': 'nativeTokenSymbol', + 'Native token logo': 'nativeTokenLogo', + rollup: 'rollup', + 'Parent Gateway Router': 'parentGatewayRouter', + 'Child Gateway Router': 'childGatewayRouter', + 'Parent ERC20 Gateway': 'parentErc20Gateway', + 'Child ERC20 Gateway': 'childErc20Gateway', + 'Parent Custom Gateway': 'parentCustomGateway', + 'Child Custom Gateway': 'childCustomGateway', + 'Parent WETH Gateway': 'parentWethGateway', + 'Child WETH Gateway': 'childWethGateway', + 'Child WETH': 'childWeth', + 'Parent MultiCall': 'parentMultiCall', + 'Child MultiCall': 'childMultiCall', + 'Parent WETH': 'parentWeth', + 'Fast Withdrawals active': 'fastWithdrawalActive', + 'Fast Withdrawals time in minutes': 'fastWithdrawalMinutes' +} export interface Issue { - state: string; - body: string; - html_url: string; + state: string + body: string + html_url: string } -export type IncomingChainData = z.infer; -export type TokenBridgeAddresses = z.infer; -export type OrbitChain = z.infer; +export type IncomingChainData = z.infer +export type TokenBridgeAddresses = z.infer +export type OrbitChain = z.infer export interface FieldMetadata { - label: string; - required: boolean; - type: "string" | "number" | "address" | "url" | "color"; - validator?: (value: string) => boolean | string | number; + label: string + required: boolean + type: 'string' | 'number' | 'address' | 'url' | 'color' + validator?: (value: string) => boolean | string | number } -export type GithubClient = ReturnType; +export type GithubClient = ReturnType diff --git a/packages/scripts/src/addOrbitChain/tests/__mocks__/chainDataMocks.ts b/packages/scripts/src/addOrbitChain/tests/__mocks__/chainDataMocks.ts index 773391c30d..b61bdd833f 100644 --- a/packages/scripts/src/addOrbitChain/tests/__mocks__/chainDataMocks.ts +++ b/packages/scripts/src/addOrbitChain/tests/__mocks__/chainDataMocks.ts @@ -1,4 +1,4 @@ -import { Issue, IncomingChainData, OrbitChain } from "../../schemas"; +import { Issue, IncomingChainData, OrbitChain } from '../../schemas' export const mockIssue: Issue = { body: ` @@ -11,9 +11,9 @@ Test Chain ### Not required _No response_ `, - state: "open", - html_url: "https://github.com/example/repo/issues/1", -}; + state: 'open', + html_url: 'https://github.com/example/repo/issues/1' +} export const fullMockIssue: Issue = { body: ` @@ -92,153 +92,153 @@ TST ### Child WETH Gateway 0x0000000000000000000000000000000000000014 `, - state: "open", - html_url: "https://github.com/example/repo/issues/1", -}; + state: 'open', + html_url: 'https://github.com/example/repo/issues/1' +} export const mockRawData = { - chainId: "42161", - name: "Test Chain", - description: "A test chain.", + chainId: '42161', + name: 'Test Chain', + description: 'A test chain.', chainLogo: - "https://github.com/user-attachments/assets/bb30cba1-242f-49a3-95bd-57e5bdee61e5", - color: "#FF0000", - rpcUrl: "https://rpc.example.com", - explorerUrl: "https://explorer.example.com", - parentChainId: "1", - nativeTokenAddress: "0x0000000000000000000000000000000000000001", - nativeTokenName: "Test Token", - nativeTokenSymbol: "TST", + 'https://github.com/user-attachments/assets/bb30cba1-242f-49a3-95bd-57e5bdee61e5', + color: '#FF0000', + rpcUrl: 'https://rpc.example.com', + explorerUrl: 'https://explorer.example.com', + parentChainId: '1', + nativeTokenAddress: '0x0000000000000000000000000000000000000001', + nativeTokenName: 'Test Token', + nativeTokenSymbol: 'TST', nativeTokenLogo: - "https://github.com/user-attachments/assets/bb30cba1-242f-49a3-95bd-57e5bdee61e5", - rollup: "0x0000000000000000000000000000000000000005", - parentCustomGateway: "0x0000000000000000000000000000000000000007", - parentErc20Gateway: "0x0000000000000000000000000000000000000008", - parentGatewayRouter: "0x0000000000000000000000000000000000000009", - parentMultiCall: "0x000000000000000000000000000000000000000A", - parentWeth: "0x000000000000000000000000000000000000000C", - parentWethGateway: "0x000000000000000000000000000000000000000D", - childCustomGateway: "0x000000000000000000000000000000000000000E", - childErc20Gateway: "0x000000000000000000000000000000000000000F", - childGatewayRouter: "0x0000000000000000000000000000000000000010", - childMultiCall: "0x0000000000000000000000000000000000000011", - childWeth: "0x0000000000000000000000000000000000000013", - childWethGateway: "0x0000000000000000000000000000000000000014", -}; + 'https://github.com/user-attachments/assets/bb30cba1-242f-49a3-95bd-57e5bdee61e5', + rollup: '0x0000000000000000000000000000000000000005', + parentCustomGateway: '0x0000000000000000000000000000000000000007', + parentErc20Gateway: '0x0000000000000000000000000000000000000008', + parentGatewayRouter: '0x0000000000000000000000000000000000000009', + parentMultiCall: '0x000000000000000000000000000000000000000A', + parentWeth: '0x000000000000000000000000000000000000000C', + parentWethGateway: '0x000000000000000000000000000000000000000D', + childCustomGateway: '0x000000000000000000000000000000000000000E', + childErc20Gateway: '0x000000000000000000000000000000000000000F', + childGatewayRouter: '0x0000000000000000000000000000000000000010', + childMultiCall: '0x0000000000000000000000000000000000000011', + childWeth: '0x0000000000000000000000000000000000000013', + childWethGateway: '0x0000000000000000000000000000000000000014' +} export const mockAddresses = { - bridge: "0x1234", - inbox: "0x5678", - outbox: "0x9ABC", - rollup: "0xDEF0", - sequencerInbox: "0x1111", - parentCustomGateway: "0x2222", - parentErc20Gateway: "0x3333", - parentGatewayRouter: "0x4444", - parentMultiCall: "0x5555", - parentWeth: "0x7777", - parentWethGateway: "0x8888", - childCustomGateway: "0x9999", - childErc20Gateway: "0xAAAA", - childGatewayRouter: "0xBBBB", - childMultiCall: "0xCCCC", - childWeth: "0xEEEE", - childWethGateway: "0xFFFF", -}; + bridge: '0x1234', + inbox: '0x5678', + outbox: '0x9ABC', + rollup: '0xDEF0', + sequencerInbox: '0x1111', + parentCustomGateway: '0x2222', + parentErc20Gateway: '0x3333', + parentGatewayRouter: '0x4444', + parentMultiCall: '0x5555', + parentWeth: '0x7777', + parentWethGateway: '0x8888', + childCustomGateway: '0x9999', + childErc20Gateway: '0xAAAA', + childGatewayRouter: '0xBBBB', + childMultiCall: '0xCCCC', + childWeth: '0xEEEE', + childWethGateway: '0xFFFF' +} export const mockIncomingChainData: IncomingChainData = { - chainId: "1234567890", - name: "Test Chain", - description: "This is a test chain.", + chainId: '1234567890', + name: 'Test Chain', + description: 'This is a test chain.', chainLogo: - "https://github.com/user-attachments/assets/bb30cba1-242f-49a3-95bd-57e5bdee61e5", - color: "#FF0000", - rpcUrl: "https://sepolia-rollup.arbitrum.io/rpc", - explorerUrl: "https://testexplorer.com", - parentChainId: "421614", - nativeTokenAddress: "0x0000000000000000000000000000000000000006", - nativeTokenName: "Test Token", - nativeTokenSymbol: "TEST", + 'https://github.com/user-attachments/assets/bb30cba1-242f-49a3-95bd-57e5bdee61e5', + color: '#FF0000', + rpcUrl: 'https://sepolia-rollup.arbitrum.io/rpc', + explorerUrl: 'https://testexplorer.com', + parentChainId: '421614', + nativeTokenAddress: '0x0000000000000000000000000000000000000006', + nativeTokenName: 'Test Token', + nativeTokenSymbol: 'TEST', nativeTokenLogo: - "https://github.com/user-attachments/assets/bb30cba1-242f-49a3-95bd-57e5bdee61e5", - rollup: "0xeedE9367Df91913ab149e828BDd6bE336df2c892", - parentGatewayRouter: "0x0000000000000000000000000000000000000009", - childGatewayRouter: "0x0000000000000000000000000000000000000016", - parentErc20Gateway: "0x0000000000000000000000000000000000000008", - childErc20Gateway: "0x0000000000000000000000000000000000000015", - parentCustomGateway: "0x0000000000000000000000000000000000000007", - childCustomGateway: "0x0000000000000000000000000000000000000014", - parentWethGateway: "0x0000000000000000000000000000000000000013", - childWethGateway: "0x0000000000000000000000000000000000000020", - parentWeth: "0x0000000000000000000000000000000000000012", - childWeth: "0x0000000000000000000000000000000000000019", - parentMultiCall: "0x0000000000000000000000000000000000000010", - childMultiCall: "0x0000000000000000000000000000000000000017", - fastWithdrawalMinutes: "15", - fastWithdrawalActive: true, -}; + 'https://github.com/user-attachments/assets/bb30cba1-242f-49a3-95bd-57e5bdee61e5', + rollup: '0xeedE9367Df91913ab149e828BDd6bE336df2c892', + parentGatewayRouter: '0x0000000000000000000000000000000000000009', + childGatewayRouter: '0x0000000000000000000000000000000000000016', + parentErc20Gateway: '0x0000000000000000000000000000000000000008', + childErc20Gateway: '0x0000000000000000000000000000000000000015', + parentCustomGateway: '0x0000000000000000000000000000000000000007', + childCustomGateway: '0x0000000000000000000000000000000000000014', + parentWethGateway: '0x0000000000000000000000000000000000000013', + childWethGateway: '0x0000000000000000000000000000000000000020', + parentWeth: '0x0000000000000000000000000000000000000012', + childWeth: '0x0000000000000000000000000000000000000019', + parentMultiCall: '0x0000000000000000000000000000000000000010', + childMultiCall: '0x0000000000000000000000000000000000000017', + fastWithdrawalMinutes: '15', + fastWithdrawalActive: true +} export const mockOrbitChain: OrbitChain = { chainId: 660279, confirmPeriodBlocks: 45818, ethBridge: { - bridge: "0x7dd8A76bdAeBE3BBBaCD7Aa87f1D4FDa1E60f94f", - inbox: "0xaE21fDA3de92dE2FDAF606233b2863782Ba046F9", - outbox: "0x1E400568AD4840dbE50FB32f306B842e9ddeF726", - rollup: "0xC47DacFbAa80Bd9D8112F4e8069482c2A3221336", - sequencerInbox: "0x995a9d3ca121D48d21087eDE20bc8acb2398c8B1", + bridge: '0x7dd8A76bdAeBE3BBBaCD7Aa87f1D4FDa1E60f94f', + inbox: '0xaE21fDA3de92dE2FDAF606233b2863782Ba046F9', + outbox: '0x1E400568AD4840dbE50FB32f306B842e9ddeF726', + rollup: '0xC47DacFbAa80Bd9D8112F4e8069482c2A3221336', + sequencerInbox: '0x995a9d3ca121D48d21087eDE20bc8acb2398c8B1' }, - nativeToken: "0x4Cb9a7AE498CEDcBb5EAe9f25736aE7d428C9D66", - explorerUrl: "https://explorer.xai-chain.net", - rpcUrl: "https://xai-chain.net/rpc", + nativeToken: '0x4Cb9a7AE498CEDcBb5EAe9f25736aE7d428C9D66', + explorerUrl: 'https://explorer.xai-chain.net', + rpcUrl: 'https://xai-chain.net/rpc', isCustom: true, isTestnet: false, - name: "Xai", - slug: "xai", + name: 'Xai', + slug: 'xai', parentChainId: 42161, tokenBridge: { - parentGatewayRouter: "0x22CCA5Dc96a4Ac1EC32c9c7C5ad4D66254a24C35", - childGatewayRouter: "0xd096e8dE90D34de758B0E0bA4a796eA2e1e272cF", - parentErc20Gateway: "0xb591cE747CF19cF30e11d656EB94134F523A9e77", - childErc20Gateway: "0x0c71417917D24F4A6A6A55559B98c5cCEcb33F7a", - parentCustomGateway: "0xb15A0826d65bE4c2fDd961b72636168ee70Af030", - childCustomGateway: "0x96551194230725c72ACF8E9573B1382CCBC70635", - parentWethGateway: "0x0000000000000000000000000000000000000000", - childWethGateway: "0x0000000000000000000000000000000000000000", - parentWeth: "0x0000000000000000000000000000000000000000", - childWeth: "0x0000000000000000000000000000000000000000", - parentProxyAdmin: "0x0000000000000000000000000000000000000000", - childProxyAdmin: "0x0000000000000000000000000000000000000000", - parentMultiCall: "0x90B02D9F861017844F30dFbdF725b6aa84E63822", - childMultiCall: "0xEEC168551A85911Ec3A905e0561b656979f3ea67", + parentGatewayRouter: '0x22CCA5Dc96a4Ac1EC32c9c7C5ad4D66254a24C35', + childGatewayRouter: '0xd096e8dE90D34de758B0E0bA4a796eA2e1e272cF', + parentErc20Gateway: '0xb591cE747CF19cF30e11d656EB94134F523A9e77', + childErc20Gateway: '0x0c71417917D24F4A6A6A55559B98c5cCEcb33F7a', + parentCustomGateway: '0xb15A0826d65bE4c2fDd961b72636168ee70Af030', + childCustomGateway: '0x96551194230725c72ACF8E9573B1382CCBC70635', + parentWethGateway: '0x0000000000000000000000000000000000000000', + childWethGateway: '0x0000000000000000000000000000000000000000', + parentWeth: '0x0000000000000000000000000000000000000000', + childWeth: '0x0000000000000000000000000000000000000000', + parentProxyAdmin: '0x0000000000000000000000000000000000000000', + childProxyAdmin: '0x0000000000000000000000000000000000000000', + parentMultiCall: '0x90B02D9F861017844F30dFbdF725b6aa84E63822', + childMultiCall: '0xEEC168551A85911Ec3A905e0561b656979f3ea67' }, bridgeUiConfig: { - color: "#F30019", + color: '#F30019', network: { - name: "Xai", - logo: "/images/XaiLogo.svg", - description: "A chain for Web2 and Web3 gamers to play blockchain games.", + name: 'Xai', + logo: '/images/XaiLogo.svg', + description: 'A chain for Web2 and Web3 gamers to play blockchain games.' }, nativeTokenData: { - name: "Xai", - symbol: "XAI", - logoUrl: "/images/XaiLogo.svg", + name: 'Xai', + symbol: 'XAI', + logoUrl: '/images/XaiLogo.svg' }, - fastWithdrawalTime: 900000, - }, -}; + fastWithdrawalTime: 900000 + } +} export const mockValidTokenBridge = { - parentGatewayRouter: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - childGatewayRouter: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - parentErc20Gateway: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - childErc20Gateway: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - parentCustomGateway: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - childCustomGateway: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - parentWethGateway: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - childWethGateway: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - parentWeth: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - childWeth: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - parentMultiCall: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - childMultiCall: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", -}; + parentGatewayRouter: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + childGatewayRouter: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + parentErc20Gateway: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + childErc20Gateway: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + parentCustomGateway: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + childCustomGateway: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + parentWethGateway: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + childWethGateway: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + parentWeth: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + childWeth: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + parentMultiCall: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + childMultiCall: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' +} diff --git a/packages/scripts/src/addOrbitChain/tests/schemas.test.ts b/packages/scripts/src/addOrbitChain/tests/schemas.test.ts index 7a0651064b..9f0ecfcb11 100644 --- a/packages/scripts/src/addOrbitChain/tests/schemas.test.ts +++ b/packages/scripts/src/addOrbitChain/tests/schemas.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest' import { addressSchema, bridgeUiConfigSchema, @@ -8,185 +8,185 @@ import { ethBridgeSchema, isValidAddress, tokenBridgeSchema, - urlSchema, -} from "../schemas"; + urlSchema +} from '../schemas' import { mockOrbitChain, - mockValidTokenBridge, -} from "./__mocks__/chainDataMocks"; + mockValidTokenBridge +} from './__mocks__/chainDataMocks' -describe("Validation Functions", () => { - describe("isValidAddress", () => { - it("should return true for valid Ethereum addresses", () => { - expect(isValidAddress("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")).toBe( +describe('Validation Functions', () => { + describe('isValidAddress', () => { + it('should return true for valid Ethereum addresses', () => { + expect(isValidAddress('0x742d35Cc6634C0532925a3b844Bc454e4438f44e')).toBe( true - ); - expect(isValidAddress("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")).toBe( + ) + expect(isValidAddress('0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed')).toBe( true - ); - expect(isValidAddress("0x0000000000000000000000000000000000000000")).toBe( + ) + expect(isValidAddress('0x0000000000000000000000000000000000000000')).toBe( true - ); - }); + ) + }) - it("should return false for invalid Ethereum addresses", () => { - expect(isValidAddress("0x742d35Cc6634C0532925a3b844Bc454e4438f44")).toBe( + it('should return false for invalid Ethereum addresses', () => { + expect(isValidAddress('0x742d35Cc6634C0532925a3b844Bc454e4438f44')).toBe( false - ); // Too short - expect(isValidAddress("742d35Cc6634C0532925a3b844Bc454e4438f44e")).toBe( + ) // Too short + expect(isValidAddress('742d35Cc6634C0532925a3b844Bc454e4438f44e')).toBe( false - ); // Missing 0x + ) // Missing 0x expect( - isValidAddress("0x742d35Cc6634C0532925a3b844Bc454e4438f44e0") - ).toBe(false); // Too long - expect(isValidAddress("0x742d35Cc6634C0532925a3b844Bc454e4438f44G")).toBe( + isValidAddress('0x742d35Cc6634C0532925a3b844Bc454e4438f44e0') + ).toBe(false) // Too long + expect(isValidAddress('0x742d35Cc6634C0532925a3b844Bc454e4438f44G')).toBe( false - ); // Invalid character - expect(isValidAddress("not an address")).toBe(false); - }); - }); + ) // Invalid character + expect(isValidAddress('not an address')).toBe(false) + }) + }) - describe("addressSchema", () => { - it("should validate correct Ethereum addresses", () => { + describe('addressSchema', () => { + it('should validate correct Ethereum addresses', () => { expect(() => - addressSchema.parse("0x742d35Cc6634C0532925a3b844Bc454e4438f44e") - ).not.toThrow(); - }); + addressSchema.parse('0x742d35Cc6634C0532925a3b844Bc454e4438f44e') + ).not.toThrow() + }) - it("should throw for invalid Ethereum addresses", () => { + it('should throw for invalid Ethereum addresses', () => { expect(() => - addressSchema.parse("0x742d35Cc6634C0532925a3b844Bc454e4438f44") - ).toThrow(); - }); - }); - - describe("urlSchema", () => { - it("should validate correct URLs", () => { - expect(() => urlSchema.parse("https://example.com")).not.toThrow(); - expect(() => urlSchema.parse("https://example.com/")).not.toThrow(); - expect(urlSchema.parse("https://example.com/")).toBe( - "https://example.com" - ); - }); - - it("should throw for invalid URLs", () => { - expect(() => urlSchema.parse("http://example.com")).toThrow(); - expect(() => urlSchema.parse("https://")).toThrow(); - }); - }); - - describe("colorHexSchema", () => { - it("should validate correct color hex values", () => { - expect(() => colorHexSchema.parse("#FF0000")).not.toThrow(); - expect(() => colorHexSchema.parse("#f00")).not.toThrow(); - }); - - it("should throw for invalid color hex values", () => { - expect(() => colorHexSchema.parse("FF0000")).toThrow(); - expect(() => colorHexSchema.parse("#FF00")).toThrow(); - }); - }); - - describe("descriptionSchema", () => { - it("should validate and transform descriptions correctly", () => { - expect(descriptionSchema.parse("A description")).toBe("A description."); - expect(descriptionSchema.parse("A description.")).toBe("A description."); - }); - }); - - describe("ethBridgeSchema", () => { - it("should validate correct ethBridge objects", async () => { + addressSchema.parse('0x742d35Cc6634C0532925a3b844Bc454e4438f44') + ).toThrow() + }) + }) + + describe('urlSchema', () => { + it('should validate correct URLs', () => { + expect(() => urlSchema.parse('https://example.com')).not.toThrow() + expect(() => urlSchema.parse('https://example.com/')).not.toThrow() + expect(urlSchema.parse('https://example.com/')).toBe( + 'https://example.com' + ) + }) + + it('should throw for invalid URLs', () => { + expect(() => urlSchema.parse('http://example.com')).toThrow() + expect(() => urlSchema.parse('https://')).toThrow() + }) + }) + + describe('colorHexSchema', () => { + it('should validate correct color hex values', () => { + expect(() => colorHexSchema.parse('#FF0000')).not.toThrow() + expect(() => colorHexSchema.parse('#f00')).not.toThrow() + }) + + it('should throw for invalid color hex values', () => { + expect(() => colorHexSchema.parse('FF0000')).toThrow() + expect(() => colorHexSchema.parse('#FF00')).toThrow() + }) + }) + + describe('descriptionSchema', () => { + it('should validate and transform descriptions correctly', () => { + expect(descriptionSchema.parse('A description')).toBe('A description.') + expect(descriptionSchema.parse('A description.')).toBe('A description.') + }) + }) + + describe('ethBridgeSchema', () => { + it('should validate correct ethBridge objects', async () => { const validEthBridge = { - bridge: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - inbox: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - outbox: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - rollup: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - sequencerInbox: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - }; + bridge: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + inbox: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + outbox: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + rollup: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + sequencerInbox: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' + } await expect( ethBridgeSchema.parseAsync(validEthBridge) - ).resolves.not.toThrow(); - }); + ).resolves.not.toThrow() + }) - it("should throw for invalid ethBridge objects", async () => { + it('should throw for invalid ethBridge objects', async () => { const invalidEthBridge = { - bridge: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - inbox: "invalid", - outbox: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - rollup: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - sequencerInbox: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - }; + bridge: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + inbox: 'invalid', + outbox: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + rollup: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + sequencerInbox: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' + } await expect( ethBridgeSchema.parseAsync(invalidEthBridge) - ).rejects.toThrow(); - }); - }); + ).rejects.toThrow() + }) + }) - describe("tokenBridgeSchema", () => { - it("should validate correct tokenBridge objects", async () => { - const result = await tokenBridgeSchema.parseAsync(mockValidTokenBridge); - expect(result).toMatchSnapshot(); - }); + describe('tokenBridgeSchema', () => { + it('should validate correct tokenBridge objects', async () => { + const result = await tokenBridgeSchema.parseAsync(mockValidTokenBridge) + expect(result).toMatchSnapshot() + }) - it("should throw for invalid tokenBridge objects", async () => { + it('should throw for invalid tokenBridge objects', async () => { const invalidTokenBridge = { - parentGatewayRouter: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", - childGatewayRouter: "invalid", - }; + parentGatewayRouter: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + childGatewayRouter: 'invalid' + } await expect( tokenBridgeSchema.parseAsync(invalidTokenBridge) - ).rejects.toThrowErrorMatchingSnapshot(); - }); - }); + ).rejects.toThrowErrorMatchingSnapshot() + }) + }) - describe("bridgeUiConfigSchema", () => { - it("should validate correct bridgeUiConfig objects", () => { + describe('bridgeUiConfigSchema', () => { + it('should validate correct bridgeUiConfig objects', () => { const validBridgeUiConfig = { - color: "#FF0000", + color: '#FF0000', network: { - name: "Test Network", - logo: "https://example.com/logo.png", - description: "A test network.", + name: 'Test Network', + logo: 'https://example.com/logo.png', + description: 'A test network.' }, nativeTokenData: { - name: "Test Token", - symbol: "TST", + name: 'Test Token', + symbol: 'TST', decimals: 18, - logoUrl: "https://example.com/token-logo.png", + logoUrl: 'https://example.com/token-logo.png' }, - fastWithdrawalTime: 900000, - }; + fastWithdrawalTime: 900000 + } expect(() => bridgeUiConfigSchema.parse(validBridgeUiConfig) - ).not.toThrow(); - }); + ).not.toThrow() + }) - it("should throw for invalid bridgeUiConfig objects", () => { + it('should throw for invalid bridgeUiConfig objects', () => { const invalidBridgeUiConfig = { - color: "invalid", + color: 'invalid', network: { - name: "", - description: "A".repeat(251), - }, - }; - expect(() => bridgeUiConfigSchema.parse(invalidBridgeUiConfig)).toThrow(); - }); - }); - - describe("chainSchema", () => { - it("should validate correct chain objects", async () => { + name: '', + description: 'A'.repeat(251) + } + } + expect(() => bridgeUiConfigSchema.parse(invalidBridgeUiConfig)).toThrow() + }) + }) + + describe('chainSchema', () => { + it('should validate correct chain objects', async () => { await expect( chainSchema.parseAsync(mockOrbitChain) - ).resolves.not.toThrow(); - }, 1000000); + ).resolves.not.toThrow() + }, 1000000) - it("should throw for invalid chain objects", async () => { + it('should throw for invalid chain objects', async () => { const invalidChain = { // Missing required fields - chainId: "invalid", - name: "", - }; - await expect(chainSchema.parseAsync(invalidChain)).rejects.toThrow(); - }, 1000000); - }); -}); + chainId: 'invalid', + name: '' + } + await expect(chainSchema.parseAsync(invalidChain)).rejects.toThrow() + }, 1000000) + }) +}) diff --git a/packages/scripts/src/addOrbitChain/tests/setup.ts b/packages/scripts/src/addOrbitChain/tests/setup.ts index 85917d5c1a..a675dd23c6 100644 --- a/packages/scripts/src/addOrbitChain/tests/setup.ts +++ b/packages/scripts/src/addOrbitChain/tests/setup.ts @@ -1,8 +1,8 @@ -import dotenv from "dotenv"; +import dotenv from 'dotenv' // Load environment variables for tests -dotenv.config(); +dotenv.config() // Set GITHUB_TOKEN to "test" if it's not already set -const githubToken = process.env.GITHUB_TOKEN || "test"; -process.env.GITHUB_TOKEN = githubToken; +const githubToken = process.env.GITHUB_TOKEN || 'test' +process.env.GITHUB_TOKEN = githubToken diff --git a/packages/scripts/src/addOrbitChain/tests/transforms.test.ts b/packages/scripts/src/addOrbitChain/tests/transforms.test.ts index 59e88616f6..17d3e576be 100644 --- a/packages/scripts/src/addOrbitChain/tests/transforms.test.ts +++ b/packages/scripts/src/addOrbitChain/tests/transforms.test.ts @@ -1,6 +1,6 @@ -import fs from "fs"; -import path from "path"; -import sharp from "sharp"; +import fs from 'fs' +import path from 'path' +import sharp from 'sharp' import { afterAll, afterEach, @@ -8,9 +8,9 @@ import { describe, expect, it, - vi, -} from "vitest"; -import { IncomingChainData, Issue } from "../schemas"; + vi +} from 'vitest' +import { IncomingChainData, Issue } from '../schemas' import { extractImageUrlFromMarkdown, extractRawChainData, @@ -19,39 +19,39 @@ import { resizeImage, stripWhitespace, transformIncomingDataToOrbitChain, - updateOrbitChainsFile, -} from "../transforms"; + updateOrbitChainsFile +} from '../transforms' import { fullMockIssue, mockIncomingChainData, - mockOrbitChain, -} from "./__mocks__/chainDataMocks"; -import { warning } from "@actions/core"; -import axios from "axios"; - -describe("Transforms", () => { - describe("extractRawChainData", () => { - it("should extract raw chain data from the issue", () => { - expect(extractRawChainData(fullMockIssue)).toMatchSnapshot(); - }); - }); - - describe("transformIncomingDataToOrbitChain", () => { - it("should transform incoming chain data to OrbitChain format", async () => { - const chainLogoPath = "/images/mockChain_Logo.png"; - const nativeTokenLogoPath = "/images/mockChain_NativeTokenLogo.png"; + mockOrbitChain +} from './__mocks__/chainDataMocks' +import { warning } from '@actions/core' +import axios from 'axios' + +describe('Transforms', () => { + describe('extractRawChainData', () => { + it('should extract raw chain data from the issue', () => { + expect(extractRawChainData(fullMockIssue)).toMatchSnapshot() + }) + }) + + describe('transformIncomingDataToOrbitChain', () => { + it('should transform incoming chain data to OrbitChain format', async () => { + const chainLogoPath = '/images/mockChain_Logo.png' + const nativeTokenLogoPath = '/images/mockChain_NativeTokenLogo.png' const result = await transformIncomingDataToOrbitChain( mockIncomingChainData as IncomingChainData, chainLogoPath, nativeTokenLogoPath - ); + ) - expect(result).toMatchSnapshot(); - }); - }); + expect(result).toMatchSnapshot() + }) + }) - describe("updateOrbitChainsFile", () => { + describe('updateOrbitChainsFile', () => { const testData = `{ "mainnet": [ { "chainId": 2, "name": "Existing Chain 2" }, @@ -61,277 +61,275 @@ describe("Transforms", () => { "testnet": [ { "chainId": 3, "name": "Existing Testnet 1" } ] - }`; - const tempFilePath = path.join(__dirname, "tempMockChains.json"); + }` + const tempFilePath = path.join(__dirname, 'tempMockChains.json') beforeEach(() => { - fs.writeFileSync(tempFilePath, testData, "utf8"); - }); + fs.writeFileSync(tempFilePath, testData, 'utf8') + }) afterEach(() => { if (fs.existsSync(tempFilePath)) { - fs.unlinkSync(tempFilePath); + fs.unlinkSync(tempFilePath) } - }); + }) - it("should update the orbit chains file correctly while preserving order", () => { - const newChain = { ...mockOrbitChain, isTestnet: false, chainId: 5 }; - const result = updateOrbitChainsFile(newChain, tempFilePath); + it('should update the orbit chains file correctly while preserving order', () => { + const newChain = { ...mockOrbitChain, isTestnet: false, chainId: 5 } + const result = updateOrbitChainsFile(newChain, tempFilePath) expect(result.mainnet.map((chain: any) => chain.chainId)).toEqual([ - 2, 4, 1, 5, - ]); - expect(result.testnet.map((chain: any) => chain.chainId)).toEqual([3]); + 2, 4, 1, 5 + ]) + expect(result.testnet.map((chain: any) => chain.chainId)).toEqual([3]) expect(result.mainnet.find((chain: any) => chain.chainId === 5)).toEqual( newChain - ); + ) - const updatedContent = fs.readFileSync(tempFilePath, "utf8"); - expect(updatedContent).toMatchSnapshot(); - }); + const updatedContent = fs.readFileSync(tempFilePath, 'utf8') + expect(updatedContent).toMatchSnapshot() + }) - it("should add a new testnet chain while preserving order", () => { + it('should add a new testnet chain while preserving order', () => { const newTestnetChain = { ...mockOrbitChain, isTestnet: true, - chainId: 5, - }; - const result = updateOrbitChainsFile(newTestnetChain, tempFilePath); + chainId: 5 + } + const result = updateOrbitChainsFile(newTestnetChain, tempFilePath) expect(result.mainnet.map((chain: any) => chain.chainId)).toEqual([ - 2, 4, 1, - ]); - expect(result.testnet.map((chain: any) => chain.chainId)).toEqual([3, 5]); + 2, 4, 1 + ]) + expect(result.testnet.map((chain: any) => chain.chainId)).toEqual([3, 5]) expect(result.testnet.find((chain: any) => chain.chainId === 5)).toEqual( newTestnetChain - ); + ) - const updatedContent = fs.readFileSync(tempFilePath, "utf8"); - expect(updatedContent).toMatchSnapshot(); - }); + const updatedContent = fs.readFileSync(tempFilePath, 'utf8') + expect(updatedContent).toMatchSnapshot() + }) - it("should handle updating an existing chain while preserving order", () => { - const existingChainId = 2; + it('should handle updating an existing chain while preserving order', () => { + const existingChainId = 2 const updatedChain = { ...mockOrbitChain, isTestnet: false, chainId: existingChainId, - name: "Updated Chain", - }; - const result = updateOrbitChainsFile(updatedChain, tempFilePath); + name: 'Updated Chain' + } + const result = updateOrbitChainsFile(updatedChain, tempFilePath) expect(result.mainnet.map((chain: any) => chain.chainId)).toEqual([ - 2, 4, 1, - ]); - expect(result.testnet.map((chain: any) => chain.chainId)).toEqual([3]); + 2, 4, 1 + ]) + expect(result.testnet.map((chain: any) => chain.chainId)).toEqual([3]) expect(result.mainnet.find((chain: any) => chain.chainId === 2)).toEqual( updatedChain - ); - - const updatedContent = fs.readFileSync(tempFilePath, "utf8"); - expect(updatedContent).toMatchSnapshot(); - }); - }); - - describe("Utility Functions", () => { - describe("stripWhitespace", () => { - it("should remove all whitespace from a string", () => { - expect(stripWhitespace(" Hello World ")).toBe("HelloWorld"); - expect(stripWhitespace("No Spaces")).toBe("NoSpaces"); - expect(stripWhitespace(" ")).toBe(""); - expect(stripWhitespace("")).toBe(""); - }); - }); - - describe("nameToSlug", () => { - it("should convert a name to a slug", () => { - expect(nameToSlug("Hello World")).toBe("hello-world"); - expect(nameToSlug("Test Chain")).toBe("test-chain"); - expect(nameToSlug("Multiple Spaces")).toBe("multiple-spaces"); - expect(nameToSlug("")).toBe(""); - }); - }); - }); - - describe("resizeImage", () => { - const testImagePath = path.join(__dirname, "__mocks__", "test-image.jpg"); + ) + + const updatedContent = fs.readFileSync(tempFilePath, 'utf8') + expect(updatedContent).toMatchSnapshot() + }) + }) + + describe('Utility Functions', () => { + describe('stripWhitespace', () => { + it('should remove all whitespace from a string', () => { + expect(stripWhitespace(' Hello World ')).toBe('HelloWorld') + expect(stripWhitespace('No Spaces')).toBe('NoSpaces') + expect(stripWhitespace(' ')).toBe('') + expect(stripWhitespace('')).toBe('') + }) + }) + + describe('nameToSlug', () => { + it('should convert a name to a slug', () => { + expect(nameToSlug('Hello World')).toBe('hello-world') + expect(nameToSlug('Test Chain')).toBe('test-chain') + expect(nameToSlug('Multiple Spaces')).toBe('multiple-spaces') + expect(nameToSlug('')).toBe('') + }) + }) + }) + + describe('resizeImage', () => { + const testImagePath = path.join(__dirname, '__mocks__', 'test-image.jpg') const resizedImagePath = path.join( __dirname, - "__mocks__", - "resized-test-image.jpg" - ); + '__mocks__', + 'resized-test-image.jpg' + ) // COMMENT OUT TO KEEP RESIZED IMAGES afterEach(() => { // Clean up the resized image after each test if (fs.existsSync(resizedImagePath)) { - fs.unlinkSync(resizedImagePath); + fs.unlinkSync(resizedImagePath) } - }); + }) - it("should resize an image to under 100KB while maintaining aspect ratio", async () => { - const inputBuffer = fs.readFileSync(testImagePath); - const resizedBuffer = await resizeImage(inputBuffer); + it('should resize an image to under 100KB while maintaining aspect ratio', async () => { + const inputBuffer = fs.readFileSync(testImagePath) + const resizedBuffer = await resizeImage(inputBuffer) - expect(resizedBuffer.length).toBeLessThanOrEqual(100 * 1024); + expect(resizedBuffer.length).toBeLessThanOrEqual(100 * 1024) // Save the resized image - fs.writeFileSync(resizedImagePath, resizedBuffer); - console.log(`Resized image saved to: ${resizedImagePath}`); + fs.writeFileSync(resizedImagePath, resizedBuffer) + console.log(`Resized image saved to: ${resizedImagePath}`) // Additional check to ensure the file was actually saved - expect(fs.existsSync(resizedImagePath)).toBe(true); - const savedFileSize = fs.statSync(resizedImagePath).size; - expect(savedFileSize).toBeLessThanOrEqual(100 * 1024); + expect(fs.existsSync(resizedImagePath)).toBe(true) + const savedFileSize = fs.statSync(resizedImagePath).size + expect(savedFileSize).toBeLessThanOrEqual(100 * 1024) // Verify that the saved image can be decoded and aspect ratio is maintained - const originalMetadata = await sharp(testImagePath).metadata(); - const resizedMetadata = await sharp(resizedImagePath).metadata(); + const originalMetadata = await sharp(testImagePath).metadata() + const resizedMetadata = await sharp(resizedImagePath).metadata() - expect(resizedMetadata.format).toBe("jpeg"); - expect(resizedMetadata.width).toBeLessThanOrEqual( - originalMetadata.width! - ); + expect(resizedMetadata.format).toBe('jpeg') + expect(resizedMetadata.width).toBeLessThanOrEqual(originalMetadata.width!) expect(resizedMetadata.height).toBeLessThanOrEqual( originalMetadata.height! - ); + ) const originalAspectRatio = - originalMetadata.width! / originalMetadata.height!; + originalMetadata.width! / originalMetadata.height! const resizedAspectRatio = - resizedMetadata.width! / resizedMetadata.height!; - expect(resizedAspectRatio).toBeCloseTo(originalAspectRatio, 2); - }); - }); - describe("Image Download and Processing", () => { + resizedMetadata.width! / resizedMetadata.height! + expect(resizedAspectRatio).toBeCloseTo(originalAspectRatio, 2) + }) + }) + describe('Image Download and Processing', () => { const downloadedImagePath = path.join( process.cwd(), - "..", - "..", - "arb-token-bridge-ui", - "public", - "images", - "downloaded_chain_logo.png" - ); + '..', + '..', + 'arb-token-bridge-ui', + 'public', + 'images', + 'downloaded_chain_logo.png' + ) // Clean up downloaded image after tests // Comment out the following 'after' block if you want to inspect the downloaded image afterAll(() => { if (fs.existsSync(downloadedImagePath)) { - fs.unlinkSync(downloadedImagePath); - console.log("Cleaned up downloaded image"); + fs.unlinkSync(downloadedImagePath) + console.log('Cleaned up downloaded image') } - }); + }) - it("should download, process, and save the chain logo image from fullMockIssue", async () => { - const rawChainData = extractRawChainData(fullMockIssue); - const imageUrl = rawChainData.chainLogo as string; + it('should download, process, and save the chain logo image from fullMockIssue', async () => { + const rawChainData = extractRawChainData(fullMockIssue) + const imageUrl = rawChainData.chainLogo as string - expect(imageUrl).toBeTruthy(); - expect(imageUrl.startsWith("https://")).toBe(true); + expect(imageUrl).toBeTruthy() + expect(imageUrl.startsWith('https://')).toBe(true) - const { buffer, fileExtension } = await fetchAndProcessImage(imageUrl); + const { buffer, fileExtension } = await fetchAndProcessImage(imageUrl) - expect(buffer).toBeTruthy(); - expect(buffer.length).toBeGreaterThan(0); - expect(fileExtension).toBeTruthy(); + expect(buffer).toBeTruthy() + expect(buffer.length).toBeGreaterThan(0) + expect(fileExtension).toBeTruthy() - const fileName = "downloaded_chain_logo"; - const savedImagePath = saveImageLocally(buffer, fileName, fileExtension); + const fileName = 'downloaded_chain_logo' + const savedImagePath = saveImageLocally(buffer, fileName, fileExtension) - expect(savedImagePath).toBeTruthy(); - console.log(`Image downloaded and saved to: ${savedImagePath}`); + expect(savedImagePath).toBeTruthy() + console.log(`Image downloaded and saved to: ${savedImagePath}`) const fullSavePath = path.join( process.cwd(), - "..", - "..", - "arb-token-bridge-ui", - "public", + '..', + '..', + 'arb-token-bridge-ui', + 'public', savedImagePath - ); - expect(fs.existsSync(fullSavePath)).toBe(true); + ) + expect(fs.existsSync(fullSavePath)).toBe(true) - const stats = fs.statSync(fullSavePath); - expect(stats.size).toBeGreaterThan(0); - }); + const stats = fs.statSync(fullSavePath) + expect(stats.size).toBeGreaterThan(0) + }) - it("should throw an error if the image fetch fails", async () => { - const invalidUrl = "https://example.com/nonexistent-image.png"; - await expect(fetchAndProcessImage(invalidUrl)).rejects.toThrow(); - }); + it('should throw an error if the image fetch fails', async () => { + const invalidUrl = 'https://example.com/nonexistent-image.png' + await expect(fetchAndProcessImage(invalidUrl)).rejects.toThrow() + }) - it("should correctly identify and handle SVG images without extension", async () => { + it('should correctly identify and handle SVG images without extension', async () => { const svgContent = ` - `; + ` // Mock axios for this test - const mockAxios = vi.spyOn(axios, "get").mockResolvedValueOnce({ + const mockAxios = vi.spyOn(axios, 'get').mockResolvedValueOnce({ status: 200, data: svgContent, headers: { - "content-type": "application/octet-stream", // Intentionally wrong content-type - }, - }); + 'content-type': 'application/octet-stream' // Intentionally wrong content-type + } + }) const { buffer, fileExtension } = await fetchAndProcessImage( - "https://example.com/logo" - ); + 'https://example.com/logo' + ) // Verify it detected SVG correctly - expect(fileExtension).toBe(".svg"); + expect(fileExtension).toBe('.svg') // Verify the SVG content wasn't modified - expect(buffer.toString("utf8").trim()).toBe(svgContent.trim()); + expect(buffer.toString('utf8').trim()).toBe(svgContent.trim()) - mockAxios.mockRestore(); - }); - }); + mockAxios.mockRestore() + }) + }) - describe("Image URL Extraction", () => { - describe("extractImageUrlFromMarkdown", () => { - it("should extract URL from old GitHub markdown format", () => { + describe('Image URL Extraction', () => { + describe('extractImageUrlFromMarkdown', () => { + it('should extract URL from old GitHub markdown format', () => { const oldFormat = - "![Image](https://github.com/user-attachments/assets/0e5ddf1f-0847-457d-be07-f489d68630e8)"; - const result = extractImageUrlFromMarkdown(oldFormat); + '![Image](https://github.com/user-attachments/assets/0e5ddf1f-0847-457d-be07-f489d68630e8)' + const result = extractImageUrlFromMarkdown(oldFormat) expect(result).toBe( - "https://github.com/user-attachments/assets/0e5ddf1f-0847-457d-be07-f489d68630e8" - ); - }); + 'https://github.com/user-attachments/assets/0e5ddf1f-0847-457d-be07-f489d68630e8' + ) + }) - it("should extract URL from new GitHub HTML img tag format", () => { - const newFormat = `Image`; - const result = extractImageUrlFromMarkdown(newFormat); + it('should extract URL from new GitHub HTML img tag format', () => { + const newFormat = `Image` + const result = extractImageUrlFromMarkdown(newFormat) expect(result).toBe( - "https://github.com/user-attachments/assets/0e5ddf1f-0847-457d-be07-f489d68630e8" - ); - }); + 'https://github.com/user-attachments/assets/0e5ddf1f-0847-457d-be07-f489d68630e8' + ) + }) - it("should extract URL from HTML img tag with single quotes", () => { - const singleQuoteFormat = `Image`; - const result = extractImageUrlFromMarkdown(singleQuoteFormat); + it('should extract URL from HTML img tag with single quotes', () => { + const singleQuoteFormat = `Image` + const result = extractImageUrlFromMarkdown(singleQuoteFormat) expect(result).toBe( - "https://github.com/user-attachments/assets/0e5ddf1f-0847-457d-be07-f489d68630e8" - ); - }); + 'https://github.com/user-attachments/assets/0e5ddf1f-0847-457d-be07-f489d68630e8' + ) + }) - it("should extract URL from HTML img tag with different attribute order", () => { - const differentOrderFormat = `Image`; - const result = extractImageUrlFromMarkdown(differentOrderFormat); + it('should extract URL from HTML img tag with different attribute order', () => { + const differentOrderFormat = `Image` + const result = extractImageUrlFromMarkdown(differentOrderFormat) expect(result).toBe( - "https://github.com/user-attachments/assets/0e5ddf1f-0847-457d-be07-f489d68630e8" - ); - }); - - it("should return original string if no image format is detected", () => { - const plainUrl = "https://example.com/direct-url.png"; - const result = extractImageUrlFromMarkdown(plainUrl); - expect(result).toBe(plainUrl); - }); - }); - - it("should handle both markdown and direct URL for chain and token logos", () => { + 'https://github.com/user-attachments/assets/0e5ddf1f-0847-457d-be07-f489d68630e8' + ) + }) + + it('should return original string if no image format is detected', () => { + const plainUrl = 'https://example.com/direct-url.png' + const result = extractImageUrlFromMarkdown(plainUrl) + expect(result).toBe(plainUrl) + }) + }) + + it('should handle both markdown and direct URL for chain and token logos', () => { const issue: Issue = { body: ` ### Chain logo @@ -341,45 +339,45 @@ https://example.com/token-logo.png ### Chain name Test Chain `, - state: "open", - html_url: "https://github.com/example/repo/issues/1", - }; + state: 'open', + html_url: 'https://github.com/example/repo/issues/1' + } - const result = extractRawChainData(issue); - expect(result.chainLogo).toBe("https://example.com/chain-logo.png"); - expect(result.nativeTokenLogo).toBe("https://example.com/token-logo.png"); - }); - }); -}); + const result = extractRawChainData(issue) + expect(result.chainLogo).toBe('https://example.com/chain-logo.png') + expect(result.nativeTokenLogo).toBe('https://example.com/token-logo.png') + }) + }) +}) const saveImageLocally = ( imageBuffer: Buffer, fileName: string, fileExtension: string ): string => { - const imageSavePath = `images/${fileName}${fileExtension}`; + const imageSavePath = `images/${fileName}${fileExtension}` const fullSavePath = path.join( process.cwd(), - "..", - "..", - "arb-token-bridge-ui", - "public", + '..', + '..', + 'arb-token-bridge-ui', + 'public', imageSavePath - ); + ) // Create directories if they don't exist - const dirs = path.dirname(fullSavePath); + const dirs = path.dirname(fullSavePath) if (!fs.existsSync(dirs)) { - fs.mkdirSync(dirs, { recursive: true }); + fs.mkdirSync(dirs, { recursive: true }) } if (fs.existsSync(fullSavePath)) { warning( `${fileName} already exists at '${imageSavePath}'. Overwriting the existing image.` - ); + ) } - fs.writeFileSync(fullSavePath, imageBuffer); + fs.writeFileSync(fullSavePath, imageBuffer) - return `/${imageSavePath}`; -}; + return `/${imageSavePath}` +} diff --git a/packages/scripts/src/addOrbitChain/transforms.ts b/packages/scripts/src/addOrbitChain/transforms.ts index e384bb0ddc..b519e07b1b 100644 --- a/packages/scripts/src/addOrbitChain/transforms.ts +++ b/packages/scripts/src/addOrbitChain/transforms.ts @@ -1,15 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import * as core from "@actions/core"; -import { getArbitrumNetworkInformationFromRollup } from "@arbitrum/sdk"; -import axios from "axios"; -import { fileTypeFromBuffer } from "file-type"; -import * as fs from "fs"; -import path from "path"; -import sharp from "sharp"; -import { lookup } from "mime-types"; -import { getIssue } from "./github"; +import * as core from '@actions/core' +import { getArbitrumNetworkInformationFromRollup } from '@arbitrum/sdk' +import axios from 'axios' +import { fileTypeFromBuffer } from 'file-type' +import * as fs from 'fs' +import path from 'path' +import sharp from 'sharp' +import { lookup } from 'mime-types' +import { getIssue } from './github' import { chainDataLabelToKey, getParentChainInfo, @@ -19,340 +19,340 @@ import { OrbitChainsList, TESTNET_PARENT_CHAIN_IDS, validateIncomingChainData, - validateOrbitChain, -} from "./schemas"; -import { getProvider } from "./provider"; -import { ethers } from "ethers"; + validateOrbitChain +} from './schemas' +import { getProvider } from './provider' +import { ethers } from 'ethers' -const SUPPORTED_IMAGE_EXTENSIONS = ["png", "svg", "jpg", "jpeg", "webp"]; -const MAX_IMAGE_SIZE_KB = 100; -const ZERO_ADDRESS = ethers.constants.AddressZero; +const SUPPORTED_IMAGE_EXTENSIONS = ['png', 'svg', 'jpg', 'jpeg', 'webp'] +const MAX_IMAGE_SIZE_KB = 100 +const ZERO_ADDRESS = ethers.constants.AddressZero export const getFileExtension = (mimeType: string): string => { - const extension = lookup(mimeType); - return extension ? `.${extension}` : ""; -}; + const extension = lookup(mimeType) + return extension ? `.${extension}` : '' +} export const initializeAndFetchData = async (): Promise => { - core.startGroup("Initialization and Data Fetching"); - console.log("Starting initialization and data fetching..."); - const issueNumber = process.env.ISSUE_NUMBER; + core.startGroup('Initialization and Data Fetching') + console.log('Starting initialization and data fetching...') + const issueNumber = process.env.ISSUE_NUMBER if (!issueNumber) { - throw new Error("ISSUE_NUMBER environment variable is not set"); + throw new Error('ISSUE_NUMBER environment variable is not set') } - console.log(`Fetching issue #${issueNumber}`); - const issue = await getIssue(issueNumber); - console.log("Extracting raw chain data from issue"); - const rawChainData = extractRawChainData(issue); - console.log("Validating incoming chain data"); - await validateIncomingChainData(rawChainData); - console.log("Initialization and data fetching completed successfully"); - core.endGroup(); -}; + console.log(`Fetching issue #${issueNumber}`) + const issue = await getIssue(issueNumber) + console.log('Extracting raw chain data from issue') + const rawChainData = extractRawChainData(issue) + console.log('Validating incoming chain data') + await validateIncomingChainData(rawChainData) + console.log('Initialization and data fetching completed successfully') + core.endGroup() +} export const processChainData = async (): Promise<{ - branchName: string; - validatedIncomingData: IncomingChainData; + branchName: string + validatedIncomingData: IncomingChainData }> => { - core.startGroup("Chain Data Processing"); - console.log("Starting chain data processing..."); - const issue = await getIssue(process.env.ISSUE_NUMBER!); - console.log("Extracting raw chain data from issue"); - const rawChainData = extractRawChainData(issue); - console.log("Validating incoming chain data"); - const validatedIncomingData = await validateIncomingChainData(rawChainData); + core.startGroup('Chain Data Processing') + console.log('Starting chain data processing...') + const issue = await getIssue(process.env.ISSUE_NUMBER!) + console.log('Extracting raw chain data from issue') + const rawChainData = extractRawChainData(issue) + console.log('Validating incoming chain data') + const validatedIncomingData = await validateIncomingChainData(rawChainData) const branchName = `add-orbit-chain/${stripWhitespace( validatedIncomingData.name - )}`; - console.log(`Branch name generated: ${branchName}`); + )}` + console.log(`Branch name generated: ${branchName}`) - console.log("Chain data processing completed successfully"); - core.endGroup(); - return { branchName, validatedIncomingData }; -}; + console.log('Chain data processing completed successfully') + core.endGroup() + return { branchName, validatedIncomingData } +} export const handleImages = async ( branchName: string, validatedIncomingData: IncomingChainData ): Promise<{ chainLogoPath: string; nativeTokenLogoPath?: string }> => { - core.startGroup("Image Processing"); - console.log("Starting image processing..."); + core.startGroup('Image Processing') + console.log('Starting image processing...') console.log( `Fetching and saving chain logo: ${validatedIncomingData.chainLogo}` - ); + ) const chainLogoPath = await fetchAndSaveImage( validatedIncomingData.chainLogo, `${stripWhitespace(validatedIncomingData.name)}_Logo` - ); - console.log(`Chain logo saved at: ${chainLogoPath}`); + ) + console.log(`Chain logo saved at: ${chainLogoPath}`) - let nativeTokenLogoPath: string | undefined; + let nativeTokenLogoPath: string | undefined if ( validatedIncomingData.nativeTokenAddress && validatedIncomingData.nativeTokenLogo ) { console.log( `Fetching and saving native token logo: ${validatedIncomingData.nativeTokenLogo}` - ); + ) nativeTokenLogoPath = validatedIncomingData.chainLogo === validatedIncomingData.nativeTokenLogo ? chainLogoPath : await fetchAndSaveImage( validatedIncomingData.nativeTokenLogo, `${stripWhitespace(validatedIncomingData.name)}_NativeTokenLogo` - ); - console.log(`Native token logo saved at: ${nativeTokenLogoPath}`); + ) + console.log(`Native token logo saved at: ${nativeTokenLogoPath}`) } else { - console.log("No native token logo to process"); + console.log('No native token logo to process') } - console.log("Image processing completed successfully"); - core.endGroup(); - return { chainLogoPath, nativeTokenLogoPath }; -}; + console.log('Image processing completed successfully') + core.endGroup() + return { chainLogoPath, nativeTokenLogoPath } +} export const createAndValidateOrbitChain = async ( validatedIncomingData: IncomingChainData, chainLogoPath: string, nativeTokenLogoPath?: string ) => { - core.startGroup("Orbit Chain Creation and Validation"); - console.log("Creating OrbitChain object..."); + core.startGroup('Orbit Chain Creation and Validation') + console.log('Creating OrbitChain object...') const orbitChain = await transformIncomingDataToOrbitChain( validatedIncomingData, chainLogoPath, nativeTokenLogoPath - ); - console.log("Orbit Chain object created, validating..."); - await validateOrbitChain(orbitChain); - console.log("Orbit Chain validated successfully"); - core.endGroup(); - return orbitChain; -}; + ) + console.log('Orbit Chain object created, validating...') + await validateOrbitChain(orbitChain) + console.log('Orbit Chain validated successfully') + core.endGroup() + return orbitChain +} export const updateOrbitChainsList = async ( orbitChain: OrbitChain, targetJsonPath: string ) => { - core.startGroup("Orbit ChainsList Update and Validation"); - console.log("Updating OrbitChainsList..."); - const networkType = orbitChain.isTestnet ? "testnet" : "mainnet"; - console.log(`Network type determined: ${networkType}`); + core.startGroup('Orbit ChainsList Update and Validation') + console.log('Updating OrbitChainsList...') + const networkType = orbitChain.isTestnet ? 'testnet' : 'mainnet' + console.log(`Network type determined: ${networkType}`) const updatedOrbitChainsList = updateOrbitChainsFile( orbitChain, targetJsonPath - ); - console.log("Validating new Orbit chain..."); - await validateOrbitChain(orbitChain); - console.log("Orbit Chain validated successfully"); - core.endGroup(); - return updatedOrbitChainsList; -}; + ) + console.log('Validating new Orbit chain...') + await validateOrbitChain(orbitChain) + console.log('Orbit Chain validated successfully') + core.endGroup() + return updatedOrbitChainsList +} export const extractImageUrlFromMarkdown = ( markdown: string ): string | null => { // Match markdown image syntax: ![alt text](url) - const markdownMatch = markdown.match(/!\[.*?\]\((.*?)\)/); + const markdownMatch = markdown.match(/!\[.*?\]\((.*?)\)/) if (markdownMatch && markdownMatch[1]) { - return markdownMatch[1]; + return markdownMatch[1] } // Match HTML img tag syntax: const htmlMatch = markdown.match( /]+src\s*=\s*["']([^"']+)["'][^>]*\/?>/ - ); + ) if (htmlMatch && htmlMatch[1]) { - return htmlMatch[1]; + return htmlMatch[1] } - return markdown; -}; + return markdown +} export const extractRawChainData = ( issue: Issue ): Record => { - const pattern = /###\s*(.*?)\s*\n\s*([\s\S]*?)(?=###|$)/g; - const rawData: Record = {}; - let match; + const pattern = /###\s*(.*?)\s*\n\s*([\s\S]*?)(?=###|$)/g + const rawData: Record = {} + let match while ((match = pattern.exec(issue.body)) !== null) { - const [, label, value] = match; - const trimmedLabel = label.trim(); - const trimmedValue = value.trim(); - - const key = chainDataLabelToKey[trimmedLabel] || trimmedLabel; - - if (trimmedValue !== "_No response_") { - if (key === "chainLogo" || key === "nativeTokenLogo") { - const imageUrl = extractImageUrlFromMarkdown(trimmedValue); - rawData[key] = imageUrl || trimmedValue; - } else if (key === "fastWithdrawalActive") { - rawData[key] = trimmedValue === "Yes"; + const [, label, value] = match + const trimmedLabel = label.trim() + const trimmedValue = value.trim() + + const key = chainDataLabelToKey[trimmedLabel] || trimmedLabel + + if (trimmedValue !== '_No response_') { + if (key === 'chainLogo' || key === 'nativeTokenLogo') { + const imageUrl = extractImageUrlFromMarkdown(trimmedValue) + rawData[key] = imageUrl || trimmedValue + } else if (key === 'fastWithdrawalActive') { + rawData[key] = trimmedValue === 'Yes' } else { - rawData[key] = trimmedValue; + rawData[key] = trimmedValue } } } - return rawData; -}; + return rawData +} export const stripWhitespace = (text: string): string => - text.replace(/\s+/g, ""); + text.replace(/\s+/g, '') export const nameToSlug = (name: string): string => - name.toLowerCase().replace(/\s+/g, "-"); + name.toLowerCase().replace(/\s+/g, '-') export const resizeImage = async ( imageBuffer: Buffer, maxSizeKB = MAX_IMAGE_SIZE_KB ): Promise => { - console.log("Resizing image..."); - let resizedImage = imageBuffer; - let scale = 1; + console.log('Resizing image...') + let resizedImage = imageBuffer + let scale = 1 while (resizedImage.length > maxSizeKB * 1024 && scale > 0.1) { - const image = sharp(imageBuffer); - const metadata = await image.metadata(); + const image = sharp(imageBuffer) + const metadata = await image.metadata() if (!metadata.width || !metadata.height) { - throw new Error("Unable to get image dimensions"); + throw new Error('Unable to get image dimensions') } - const newWidth = Math.round(metadata.width * scale); - const newHeight = Math.round(metadata.height * scale); + const newWidth = Math.round(metadata.width * scale) + const newHeight = Math.round(metadata.height * scale) resizedImage = await image - .resize(newWidth, newHeight, { fit: "inside" }) - .toBuffer(); + .resize(newWidth, newHeight, { fit: 'inside' }) + .toBuffer() - scale -= 0.1; + scale -= 0.1 } if (resizedImage.length > maxSizeKB * 1024) { - throw new Error(`Unable to resize image to under ${maxSizeKB}KB`); + throw new Error(`Unable to resize image to under ${maxSizeKB}KB`) } - return resizedImage; -}; + return resizedImage +} export const fetchAndProcessImage = async ( urlOrPath: string ): Promise<{ buffer: Buffer; fileExtension: string }> => { - let imageBuffer: Buffer; - let fileExtension: string; + let imageBuffer: Buffer + let fileExtension: string - if (urlOrPath.startsWith("http")) { - console.log("Fetching image from:", urlOrPath); + if (urlOrPath.startsWith('http')) { + console.log('Fetching image from:', urlOrPath) const response = await axios.get(urlOrPath, { - responseType: "arraybuffer", - timeout: 10000, // 10 seconds timeout - }); + responseType: 'arraybuffer', + timeout: 10000 // 10 seconds timeout + }) if (response.status !== 200) { - throw new Error(`Failed to fetch image. Status: ${response.status}`); + throw new Error(`Failed to fetch image. Status: ${response.status}`) } - imageBuffer = Buffer.from(response.data); + imageBuffer = Buffer.from(response.data) const isSVG = - response.headers["content-type"]?.includes("svg") || - imageBuffer.toString("utf8").trim().toLowerCase().startsWith(" => { - const { buffer, fileExtension } = await fetchAndProcessImage(urlOrPath); - const imageSavePath = `images/${fileName}${fileExtension}`; + const { buffer, fileExtension } = await fetchAndProcessImage(urlOrPath) + const imageSavePath = `images/${fileName}${fileExtension}` const fullPath = path.join( process.cwd(), - "../../packages/arb-token-bridge-ui/public/images" - ); + '../../packages/arb-token-bridge-ui/public/images' + ) // Save the file locally - fs.writeFileSync(path.join(fullPath, `${fileName}${fileExtension}`), buffer); - console.log(`Successfully saved image to ${imageSavePath}`); + fs.writeFileSync(path.join(fullPath, `${fileName}${fileExtension}`), buffer) + console.log(`Successfully saved image to ${imageSavePath}`) - return `/${imageSavePath}`; -}; + return `/${imageSavePath}` +} export const transformIncomingDataToOrbitChain = async ( chainData: IncomingChainData, chainLogoPath: string, nativeTokenLogoPath?: string ): Promise => { - const parentChainId = parseInt(chainData.parentChainId, 10); - const isTestnet = TESTNET_PARENT_CHAIN_IDS.includes(parentChainId); - const parentChainInfo = getParentChainInfo(parentChainId); - const provider = getProvider(parentChainInfo); + const parentChainId = parseInt(chainData.parentChainId, 10) + const isTestnet = TESTNET_PARENT_CHAIN_IDS.includes(parentChainId) + const parentChainInfo = getParentChainInfo(parentChainId) + const provider = getProvider(parentChainInfo) const rollupData = await getArbitrumNetworkInformationFromRollup( chainData.rollup, provider - ); + ) return { chainId: parseInt(chainData.chainId, 10), @@ -362,7 +362,7 @@ export const transformIncomingDataToOrbitChain = async ( inbox: rollupData.ethBridge.inbox, outbox: rollupData.ethBridge.outbox, rollup: chainData.rollup, - sequencerInbox: rollupData.ethBridge.sequencerInbox, + sequencerInbox: rollupData.ethBridge.sequencerInbox }, nativeToken: chainData.nativeTokenAddress, explorerUrl: chainData.explorerUrl, @@ -386,61 +386,61 @@ export const transformIncomingDataToOrbitChain = async ( childMultiCall: chainData.childMultiCall, childProxyAdmin: ZERO_ADDRESS, childWeth: chainData.childWeth, - childWethGateway: chainData.childWethGateway, + childWethGateway: chainData.childWethGateway }, bridgeUiConfig: { color: chainData.color, network: { name: chainData.name, logo: chainLogoPath, - description: chainData.description, + description: chainData.description }, ...(chainData.nativeTokenAddress && { nativeTokenData: { - name: chainData.nativeTokenName || "", - symbol: chainData.nativeTokenSymbol || "", - logoUrl: nativeTokenLogoPath, - }, + name: chainData.nativeTokenName || '', + symbol: chainData.nativeTokenSymbol || '', + logoUrl: nativeTokenLogoPath + } }), ...(chainData.fastWithdrawalActive && { fastWithdrawalTime: chainData.fastWithdrawalMinutes ? Number(chainData.fastWithdrawalMinutes) * 60 * 1000 - : undefined, - }), - }, - }; -}; + : undefined + }) + } + } +} export const updateOrbitChainsFile = ( orbitChain: OrbitChain, targetJsonPath: string ): OrbitChainsList => { // Read the file contents - const fileContents = fs.readFileSync(targetJsonPath, "utf8"); + const fileContents = fs.readFileSync(targetJsonPath, 'utf8') // Parse the JSON const orbitChains: { mainnet: OrbitChain[]; testnet: OrbitChain[] } = - JSON.parse(fileContents); - const networkType = orbitChain.isTestnet ? "testnet" : "mainnet"; + JSON.parse(fileContents) + const networkType = orbitChain.isTestnet ? 'testnet' : 'mainnet' // Find the index of the chain if it already exists const existingIndex = orbitChains[networkType].findIndex( - (chain) => chain.chainId === orbitChain.chainId - ); + chain => chain.chainId === orbitChain.chainId + ) if (existingIndex !== -1) { // Update existing chain - orbitChains[networkType][existingIndex] = orbitChain; + orbitChains[networkType][existingIndex] = orbitChain } else { // Add new chain to the end of the array - orbitChains[networkType].push(orbitChain); + orbitChains[networkType].push(orbitChain) } // Convert the updated object back to a JSON string, preserving the original formatting - const updatedContents = JSON.stringify(orbitChains, null, 2); + const updatedContents = JSON.stringify(orbitChains, null, 2) // Write the updated contents back to the file - fs.writeFileSync(targetJsonPath, updatedContents); + fs.writeFileSync(targetJsonPath, updatedContents) - return orbitChains; -}; + return orbitChains +} diff --git a/packages/scripts/src/index.ts b/packages/scripts/src/index.ts index 11742ce657..11f5b6f97f 100644 --- a/packages/scripts/src/index.ts +++ b/packages/scripts/src/index.ts @@ -1,23 +1,23 @@ #!/usr/bin/env node -import { Command } from "commander"; -import { addOrbitChain } from "./addOrbitChain"; +import { Command } from 'commander' +import { addOrbitChain } from './addOrbitChain' -const program = new Command(); +const program = new Command() program - .name("orbit-scripts") - .description("CLI for Orbit chain management scripts") - .version("1.0.0"); + .name('orbit-scripts') + .description('CLI for Orbit chain management scripts') + .version('1.0.0') program - .command("add-orbit-chain ") - .description("Add a new Orbit chain") - .action((targetJsonPath) => { - addOrbitChain(targetJsonPath).catch((error) => { - console.error(`Error in addOrbitChain: ${error}`); - process.exit(1); - }); - }); + .command('add-orbit-chain ') + .description('Add a new Orbit chain') + .action(targetJsonPath => { + addOrbitChain(targetJsonPath).catch(error => { + console.error(`Error in addOrbitChain: ${error}`) + process.exit(1) + }) + }) // Add more commands here as needed, for example: // program @@ -27,4 +27,4 @@ program // // Call the function for the other script // }); -program.parse(process.argv); +program.parse(process.argv) diff --git a/packages/scripts/vite.config.ts b/packages/scripts/vite.config.ts index 3a0e811e39..4ef77990fe 100644 --- a/packages/scripts/vite.config.ts +++ b/packages/scripts/vite.config.ts @@ -1,31 +1,31 @@ -import { defineConfig } from "vite"; -import { resolve } from "path"; +import { defineConfig } from 'vite' +import { resolve } from 'path' export default defineConfig({ build: { lib: { - entry: resolve(__dirname, "src/index.ts"), - formats: ["cjs", "es"], - fileName: (format) => `scripts.${format}.js`, + entry: resolve(__dirname, 'src/index.ts'), + formats: ['cjs', 'es'], + fileName: format => `scripts.${format}.js` }, rollupOptions: { external: [ - "@actions/core", - "@actions/github", - "axios", - "fs", - "commander", - "sharp", - "path", - ], - }, + '@actions/core', + '@actions/github', + 'axios', + 'fs', + 'commander', + 'sharp', + 'path' + ] + } }, optimizeDeps: { - exclude: ["sharp"], + exclude: ['sharp'] }, resolve: { alias: { - path: "path", - }, - }, -}); + path: 'path' + } + } +}) diff --git a/packages/arb-token-bridge-ui/prettier.config.js b/prettier.config.js similarity index 100% rename from packages/arb-token-bridge-ui/prettier.config.js rename to prettier.config.js diff --git a/packages/arb-token-bridge-ui/tailwind.config.js b/tailwind.config.js similarity index 95% rename from packages/arb-token-bridge-ui/tailwind.config.js rename to tailwind.config.js index d06d6b4a17..df3926c2fe 100644 --- a/packages/arb-token-bridge-ui/tailwind.config.js +++ b/tailwind.config.js @@ -1,8 +1,9 @@ /** @type {import('tailwindcss').Config} */ module.exports = { content: [ - './src/**/*.{js,ts,jsx,tsx}', - '../../node_modules/@offchainlabs/cobalt/**/*.{js,ts,jsx,tsx}' + './packages/arb-token-bridge-ui/src/**/*.{js,ts,jsx,tsx}', + './packages/app/pages/**/*.{js,ts,jsx,tsx}', + './node_modules/@offchainlabs/cobalt/**/*.{js,ts,jsx,tsx}' ], // eslint-disable-next-line @typescript-eslint/no-require-imports plugins: [require('@headlessui/tailwindcss')], diff --git a/tsconfig.base.json b/tsconfig.base.json index 2d398742e4..0728c7ffb0 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -14,6 +14,17 @@ "resolveJsonModule": true, "isolatedModules": true, "noUncheckedIndexedAccess": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/app/*": ["./packages/app/src/*"], + "@/bridge/*": ["./packages/arb-token-bridge-ui/src/*"], + "@/token-bridge-sdk/*": [ + "./packages/arb-token-bridge-ui/src/token-bridge-sdk/*" + ], + "@/icons/*": ["./packages/app/public/icons/*"], + "@/images/*": ["./packages/app/public/images/*"], + "@/public/*": ["./packages/app/public/*"] + } } } diff --git a/vercel.json b/vercel.json index 298ef5d225..af5749a64a 100644 --- a/vercel.json +++ b/vercel.json @@ -1,4 +1,5 @@ { + "$schema": "https://openapi.vercel.sh/vercel.json", "routes": [ { "src": "/token-list-42161.json", diff --git a/yarn.lock b/yarn.lock index c493e2ca0a..0934b8a37a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1005,6 +1005,14 @@ resolved "https://registry.yarnpkg.com/@ecies/ciphers/-/ciphers-0.2.2.tgz#82a15b10a6e502b63fb30915d944b2eaf3ff17ff" integrity sha512-ylfGR7PyTd+Rm2PqQowG08BCKA22QuX8NzrL+LxAAvazN10DMwdJ2fWwAzRj05FI/M8vNFGm3cv9Wq/GFWCBLg== +"@emnapi/core@^1.4.3": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.5.tgz#bfbb0cbbbb9f96ec4e2c4fd917b7bbe5495ceccb" + integrity sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q== + dependencies: + "@emnapi/wasi-threads" "1.0.4" + tslib "^2.4.0" + "@emnapi/runtime@^1.4.0": version "1.4.3" resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.3.tgz#c0564665c80dc81c448adac23f9dfbed6c838f7d" @@ -1012,6 +1020,20 @@ dependencies: tslib "^2.4.0" +"@emnapi/runtime@^1.4.3": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.5.tgz#c67710d0661070f38418b6474584f159de38aba9" + integrity sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg== + dependencies: + tslib "^2.4.0" + +"@emnapi/wasi-threads@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz#703fc094d969e273b1b71c292523b2f792862bf4" + integrity sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g== + dependencies: + tslib "^2.4.0" + "@emotion/hash@^0.9.0": version "0.9.2" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" @@ -1312,6 +1334,13 @@ dependencies: "@types/json-schema" "^7.0.15" +"@eslint/core@^0.15.1": + version "0.15.2" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.2.tgz#59386327d7862cc3603ebc7c78159d2dcc4a868f" + integrity sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg== + dependencies: + "@types/json-schema" "^7.0.15" + "@eslint/eslintrc@^2.1.4": version "2.1.4" resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz" @@ -1357,12 +1386,12 @@ resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f" integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== -"@eslint/plugin-kit@^0.2.8": - version "0.2.8" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz#47488d8f8171b5d4613e833313f3ce708e3525f8" - integrity sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA== +"@eslint/plugin-kit@0.3.4", "@eslint/plugin-kit@^0.2.8": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz#c6b9f165e94bf4d9fdd493f1c028a94aaf5fc1cc" + integrity sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw== dependencies: - "@eslint/core" "^0.13.0" + "@eslint/core" "^0.15.1" levn "^0.4.1" "@ethereumjs/common@^3.2.0": @@ -2440,11 +2469,27 @@ "@mysten/sui" "1.29.1" "@wallet-standard/core" "1.1.0" +"@napi-rs/wasm-runtime@^0.2.11": + version "0.2.12" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" + integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ== + dependencies: + "@emnapi/core" "^1.4.3" + "@emnapi/runtime" "^1.4.3" + "@tybys/wasm-util" "^0.10.0" + "@next/env@14.2.32": version "14.2.32" resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.32.tgz#6d1107e2b7cc8649ff3730b8b46deb4e8a6d38fa" integrity sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng== +"@next/eslint-plugin-next@15.4.6": + version "15.4.6" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.4.6.tgz#d55ced8c7d2bb8f95496fa410331da1df31f3b9b" + integrity sha512-2NOu3ln+BTcpnbIDuxx6MNq+pRrCyey4WSXGaJIyt0D2TYicHeO9QrUENNjcf673n3B1s7hsiV5xBYRCK1Q8kA== + dependencies: + fast-glob "3.3.1" + "@next/eslint-plugin-next@^15.3.2": version "15.3.2" resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.2.tgz#6a371b022d6de47f2bafc868c7c2220f4e6a2903" @@ -2576,6 +2621,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@nolyfill/is-core-module@1.0.39": + version "1.0.39" + resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e" + integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== + "@octokit/auth-token@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-4.0.0.tgz#40d203ea827b9f17f42a29c6afb93b7745ef80c7" @@ -3057,6 +3107,11 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== +"@rushstack/eslint-patch@^1.10.3": + version "1.12.0" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz#326a7b46f6d4cfa54ae25bb888551697873069b4" + integrity sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw== + "@safe-global/safe-apps-provider@0.18.6": version "0.18.6" resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-provider/-/safe-apps-provider-0.18.6.tgz#b5756176549e35350b7e090824b71507a0d1f749" @@ -3641,6 +3696,13 @@ resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz" integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== +"@tybys/wasm-util@^0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.0.tgz#2fd3cd754b94b378734ce17058d0507c45c88369" + integrity sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ== + dependencies: + tslib "^2.4.0" + "@types/aria-query@^4.2.0": version "4.2.2" resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz" @@ -4085,6 +4147,21 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": + version "8.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz#c9afec1866ee1a6ea3d768b5f8e92201efbbba06" + integrity sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.39.0" + "@typescript-eslint/type-utils" "8.39.0" + "@typescript-eslint/utils" "8.39.0" + "@typescript-eslint/visitor-keys" "8.39.0" + graphemer "^1.4.0" + ignore "^7.0.0" + natural-compare "^1.4.0" + ts-api-utils "^2.1.0" + "@typescript-eslint/parser@8.32.1", "@typescript-eslint/parser@^8.32.1": version "8.32.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.32.1.tgz#18b0e53315e0bc22b2619d398ae49a968370935e" @@ -4106,6 +4183,26 @@ "@typescript-eslint/typescript-estree" "5.62.0" debug "^4.3.4" +"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": + version "8.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.39.0.tgz#c4b895d7a47f4cd5ee6ee77ea30e61d58b802008" + integrity sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg== + dependencies: + "@typescript-eslint/scope-manager" "8.39.0" + "@typescript-eslint/types" "8.39.0" + "@typescript-eslint/typescript-estree" "8.39.0" + "@typescript-eslint/visitor-keys" "8.39.0" + debug "^4.3.4" + +"@typescript-eslint/project-service@8.39.0": + version "8.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.39.0.tgz#71cb29c3f8139f99a905b8705127bffc2ae84759" + integrity sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew== + dependencies: + "@typescript-eslint/tsconfig-utils" "^8.39.0" + "@typescript-eslint/types" "^8.39.0" + debug "^4.3.4" + "@typescript-eslint/scope-manager@5.59.11": version "5.59.11" resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.11.tgz" @@ -4130,6 +4227,19 @@ "@typescript-eslint/types" "8.32.1" "@typescript-eslint/visitor-keys" "8.32.1" +"@typescript-eslint/scope-manager@8.39.0": + version "8.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz#ba4bf6d8257bbc172c298febf16bc22df4856570" + integrity sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A== + dependencies: + "@typescript-eslint/types" "8.39.0" + "@typescript-eslint/visitor-keys" "8.39.0" + +"@typescript-eslint/tsconfig-utils@8.39.0", "@typescript-eslint/tsconfig-utils@^8.39.0": + version "8.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz#b2e87fef41a3067c570533b722f6af47be213f13" + integrity sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ== + "@typescript-eslint/type-utils@5.59.11": version "5.59.11" resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.11.tgz" @@ -4150,6 +4260,17 @@ debug "^4.3.4" ts-api-utils "^2.1.0" +"@typescript-eslint/type-utils@8.39.0": + version "8.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz#310ec781ae5e7bb0f5940bfd652573587f22786b" + integrity sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q== + dependencies: + "@typescript-eslint/types" "8.39.0" + "@typescript-eslint/typescript-estree" "8.39.0" + "@typescript-eslint/utils" "8.39.0" + debug "^4.3.4" + ts-api-utils "^2.1.0" + "@typescript-eslint/types@5.59.11": version "5.59.11" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.11.tgz" @@ -4165,6 +4286,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.32.1.tgz#b19fe4ac0dc08317bae0ce9ec1168123576c1d4b" integrity sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg== +"@typescript-eslint/types@8.39.0", "@typescript-eslint/types@^8.39.0": + version "8.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.39.0.tgz#80f010b7169d434a91cd0529d70a528dbc9c99c6" + integrity sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg== + "@typescript-eslint/typescript-estree@5.59.11": version "5.59.11" resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.11.tgz" @@ -4205,6 +4331,22 @@ semver "^7.6.0" ts-api-utils "^2.1.0" +"@typescript-eslint/typescript-estree@8.39.0": + version "8.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz#b9477a5c47a0feceffe91adf553ad9a3cd4cb3d6" + integrity sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw== + dependencies: + "@typescript-eslint/project-service" "8.39.0" + "@typescript-eslint/tsconfig-utils" "8.39.0" + "@typescript-eslint/types" "8.39.0" + "@typescript-eslint/visitor-keys" "8.39.0" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.1.0" + "@typescript-eslint/utils@5.59.11": version "5.59.11" resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.11.tgz" @@ -4229,6 +4371,16 @@ "@typescript-eslint/types" "8.32.1" "@typescript-eslint/typescript-estree" "8.32.1" +"@typescript-eslint/utils@8.39.0": + version "8.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.39.0.tgz#dfea42f3c7ec85f9f3e994ff0bba8f3b2f09e220" + integrity sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ== + dependencies: + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/scope-manager" "8.39.0" + "@typescript-eslint/types" "8.39.0" + "@typescript-eslint/typescript-estree" "8.39.0" + "@typescript-eslint/visitor-keys@5.59.11": version "5.59.11" resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.11.tgz" @@ -4253,6 +4405,14 @@ "@typescript-eslint/types" "8.32.1" eslint-visitor-keys "^4.2.0" +"@typescript-eslint/visitor-keys@8.39.0": + version "8.39.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz#5d619a6e810cdd3fd1913632719cbccab08bf875" + integrity sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA== + dependencies: + "@typescript-eslint/types" "8.39.0" + eslint-visitor-keys "^4.2.1" + "@uidotdev/usehooks@^2.4.1": version "2.4.1" resolved "https://registry.npmjs.org/@uidotdev/usehooks/-/usehooks-2.4.1.tgz" @@ -4268,6 +4428,103 @@ resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.34.tgz#879461f5d4009327a24259bbab797e0f22db58c8" integrity sha512-Hc3TfrFaupg0M84e/Zv7BoF+fmMWDV15mZ5s8ZQt2qZxUcNw2GQW+L6L/2k74who31G+p1m3GRYbJpAo7d1pqA== +"@unrs/resolver-binding-android-arm-eabi@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz#9f5b04503088e6a354295e8ea8fe3cb99e43af81" + integrity sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw== + +"@unrs/resolver-binding-android-arm64@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz#7414885431bd7178b989aedc4d25cccb3865bc9f" + integrity sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g== + +"@unrs/resolver-binding-darwin-arm64@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz#b4a8556f42171fb9c9f7bac8235045e82aa0cbdf" + integrity sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g== + +"@unrs/resolver-binding-darwin-x64@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz#fd4d81257b13f4d1a083890a6a17c00de571f0dc" + integrity sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ== + +"@unrs/resolver-binding-freebsd-x64@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz#d2513084d0f37c407757e22f32bd924a78cfd99b" + integrity sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw== + +"@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz#844d2605d057488d77fab09705f2866b86164e0a" + integrity sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw== + +"@unrs/resolver-binding-linux-arm-musleabihf@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz#204892995cefb6bd1d017d52d097193bc61ddad3" + integrity sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw== + +"@unrs/resolver-binding-linux-arm64-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz#023eb0c3aac46066a10be7a3f362e7b34f3bdf9d" + integrity sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ== + +"@unrs/resolver-binding-linux-arm64-musl@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz#9e6f9abb06424e3140a60ac996139786f5d99be0" + integrity sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w== + +"@unrs/resolver-binding-linux-ppc64-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz#b111417f17c9d1b02efbec8e08398f0c5527bb44" + integrity sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA== + +"@unrs/resolver-binding-linux-riscv64-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz#92ffbf02748af3e99873945c9a8a5ead01d508a9" + integrity sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ== + +"@unrs/resolver-binding-linux-riscv64-musl@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz#0bec6f1258fc390e6b305e9ff44256cb207de165" + integrity sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew== + +"@unrs/resolver-binding-linux-s390x-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz#577843a084c5952f5906770633ccfb89dac9bc94" + integrity sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg== + +"@unrs/resolver-binding-linux-x64-gnu@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz#36fb318eebdd690f6da32ac5e0499a76fa881935" + integrity sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w== + +"@unrs/resolver-binding-linux-x64-musl@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz#bfb9af75f783f98f6a22c4244214efe4df1853d6" + integrity sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA== + +"@unrs/resolver-binding-wasm32-wasi@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz#752c359dd875684b27429500d88226d7cc72f71d" + integrity sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ== + dependencies: + "@napi-rs/wasm-runtime" "^0.2.11" + +"@unrs/resolver-binding-win32-arm64-msvc@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz#ce5735e600e4c2fbb409cd051b3b7da4a399af35" + integrity sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw== + +"@unrs/resolver-binding-win32-ia32-msvc@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz#72fc57bc7c64ec5c3de0d64ee0d1810317bc60a6" + integrity sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ== + +"@unrs/resolver-binding-win32-x64-msvc@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz#538b1e103bf8d9864e7b85cc96fa8d6fb6c40777" + integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g== + "@unstoppabledomains/resolution@^8.3.3": version "8.5.0" resolved "https://registry.npmjs.org/@unstoppabledomains/resolution/-/resolution-8.5.0.tgz" @@ -7536,6 +7793,22 @@ escape-string-regexp@^4.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +eslint-config-next@^15.3.2: + version "15.4.6" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.4.6.tgz#c5602665f31009e5165f6d9642a94a736f88c5cd" + integrity sha512-4uznvw5DlTTjrZgYZjMciSdDDMO2SWIuQgUNaFyC2O3Zw3Z91XeIejeVa439yRq2CnJb/KEvE4U2AeN/66FpUA== + dependencies: + "@next/eslint-plugin-next" "15.4.6" + "@rushstack/eslint-patch" "^1.10.3" + "@typescript-eslint/eslint-plugin" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" + eslint-import-resolver-node "^0.3.6" + eslint-import-resolver-typescript "^3.5.2" + eslint-plugin-import "^2.31.0" + eslint-plugin-jsx-a11y "^6.10.0" + eslint-plugin-react "^7.37.0" + eslint-plugin-react-hooks "^5.0.0" + eslint-config-prettier@^10.1.5: version "10.1.5" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782" @@ -7546,7 +7819,7 @@ eslint-import-resolver-alias@^1.1.2: resolved "https://registry.npmjs.org/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz" integrity sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w== -eslint-import-resolver-node@^0.3.9: +eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: version "0.3.9" resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz" integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== @@ -7555,6 +7828,19 @@ eslint-import-resolver-node@^0.3.9: is-core-module "^2.13.0" resolve "^1.22.4" +eslint-import-resolver-typescript@^3.5.2: + version "3.10.1" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz#23dac32efa86a88e2b8232eb244ac499ad636db2" + integrity sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ== + dependencies: + "@nolyfill/is-core-module" "1.0.39" + debug "^4.4.0" + get-tsconfig "^4.10.0" + is-bun-module "^2.0.0" + stable-hash "^0.0.5" + tinyglobby "^0.2.13" + unrs-resolver "^1.6.2" + eslint-module-utils@^2.12.0: version "2.12.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b" @@ -7587,7 +7873,7 @@ eslint-plugin-import@^2.31.0: string.prototype.trimend "^1.0.8" tsconfig-paths "^3.15.0" -eslint-plugin-jsx-a11y@^6.10.2: +eslint-plugin-jsx-a11y@^6.10.0, eslint-plugin-jsx-a11y@^6.10.2: version "6.10.2" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz#d2812bb23bf1ab4665f1718ea442e8372e638483" integrity sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q== @@ -7621,7 +7907,12 @@ eslint-plugin-react-hooks@^4.6.0: resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz" integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== -eslint-plugin-react@^7.37.5: +eslint-plugin-react-hooks@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz#1be0080901e6ac31ce7971beed3d3ec0a423d9e3" + integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg== + +eslint-plugin-react@^7.37.0, eslint-plugin-react@^7.37.5: version "7.37.5" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065" integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA== @@ -7683,6 +7974,11 @@ eslint-visitor-keys@^4.2.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== +eslint-visitor-keys@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" + integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== + eslint@^8.0.0: version "8.57.1" resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz" @@ -8333,6 +8629,11 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fdir@^6.4.4: + version "6.4.6" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.6.tgz#2b268c0232697063111bbf3f64810a2a741ba281" + integrity sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w== + fflate@^0.4.8: version "0.4.8" resolved "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz" @@ -8817,6 +9118,13 @@ get-symbol-description@^1.1.0: es-errors "^1.3.0" get-intrinsic "^1.2.6" +get-tsconfig@^4.10.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.1.tgz#d34c1c01f47d65a606c37aa7a177bc3e56ab4b2e" + integrity sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ== + dependencies: + resolve-pkg-maps "^1.0.0" + getos@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz" @@ -9546,6 +9854,13 @@ is-boolean-object@^1.2.1: call-bound "^1.0.3" has-tostringtag "^1.0.2" +is-bun-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-2.0.0.tgz#4d7859a87c0fcac950c95e666730e745eae8bddd" + integrity sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ== + dependencies: + semver "^7.7.1" + is-callable@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" @@ -10837,6 +11152,11 @@ napi-build-utils@^1.0.1: resolved "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== +napi-postinstall@^0.3.0: + version "0.3.2" + resolved "https://registry.yarnpkg.com/napi-postinstall/-/napi-postinstall-0.3.2.tgz#03c62080e88b311c4d7423b0f15f0c920bbcc626" + integrity sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw== + napi-wasm@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/napi-wasm/-/napi-wasm-1.1.0.tgz" @@ -11604,6 +11924,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + pify@^2.0.0, pify@^2.2.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" @@ -12491,6 +12816,11 @@ resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + resolve@^1.1.7, resolve@^1.14.2, resolve@^1.22.4, resolve@^1.22.8: version "1.22.10" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" @@ -13245,6 +13575,11 @@ sshpk@^1.14.1: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +stable-hash@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stable-hash/-/stable-hash-0.0.5.tgz#94e8837aaeac5b4d0f631d2972adef2924b40269" + integrity sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA== + stack-generator@^2.0.5: version "2.0.10" resolved "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz" @@ -13890,6 +14225,14 @@ tinyexec@^0.3.2: resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== +tinyglobby@^0.2.13: + version "0.2.14" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d" + integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ== + dependencies: + fdir "^6.4.4" + picomatch "^4.0.2" + tinypool@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz" @@ -13915,7 +14258,7 @@ tinyspy@^3.0.2: resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== -tippy.js@^6.3.1: +tippy.js@^6.3.1, tippy.js@^6.3.7: version "6.3.7" resolved "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz" integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ== @@ -14354,6 +14697,33 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +unrs-resolver@^1.6.2: + version "1.11.1" + resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz#be9cd8686c99ef53ecb96df2a473c64d304048a9" + integrity sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg== + dependencies: + napi-postinstall "^0.3.0" + optionalDependencies: + "@unrs/resolver-binding-android-arm-eabi" "1.11.1" + "@unrs/resolver-binding-android-arm64" "1.11.1" + "@unrs/resolver-binding-darwin-arm64" "1.11.1" + "@unrs/resolver-binding-darwin-x64" "1.11.1" + "@unrs/resolver-binding-freebsd-x64" "1.11.1" + "@unrs/resolver-binding-linux-arm-gnueabihf" "1.11.1" + "@unrs/resolver-binding-linux-arm-musleabihf" "1.11.1" + "@unrs/resolver-binding-linux-arm64-gnu" "1.11.1" + "@unrs/resolver-binding-linux-arm64-musl" "1.11.1" + "@unrs/resolver-binding-linux-ppc64-gnu" "1.11.1" + "@unrs/resolver-binding-linux-riscv64-gnu" "1.11.1" + "@unrs/resolver-binding-linux-riscv64-musl" "1.11.1" + "@unrs/resolver-binding-linux-s390x-gnu" "1.11.1" + "@unrs/resolver-binding-linux-x64-gnu" "1.11.1" + "@unrs/resolver-binding-linux-x64-musl" "1.11.1" + "@unrs/resolver-binding-wasm32-wasi" "1.11.1" + "@unrs/resolver-binding-win32-arm64-msvc" "1.11.1" + "@unrs/resolver-binding-win32-ia32-msvc" "1.11.1" + "@unrs/resolver-binding-win32-x64-msvc" "1.11.1" + unstorage@^1.9.0: version "1.10.2" resolved "https://registry.npmjs.org/unstorage/-/unstorage-1.10.2.tgz"