diff --git a/.changeset/poor-readers-juggle.md b/.changeset/poor-readers-juggle.md new file mode 100644 index 0000000000..5dd1fe76a2 --- /dev/null +++ b/.changeset/poor-readers-juggle.md @@ -0,0 +1,19 @@ +--- +"@talismn/chaindata-provider": patch +"@talismn/chain-connectors": patch +"@talismn/connection-meta": patch +"@talismn/balances-react": patch +"@talismn/on-chain-id": patch +"@talismn/token-rates": patch +"@talismn/balances": patch +"@talismn/keyring": patch +"@talismn/crypto": patch +"@talismn/solana": patch +"@talismn/icons": patch +"@talismn/scale": patch +"@talismn/sapi": patch +"@talismn/util": patch +"@talismn/orb": patch +--- + +migration from preconstruct to tsup diff --git a/.changeset/yellow-clocks-think.md b/.changeset/yellow-clocks-think.md new file mode 100644 index 0000000000..7efc42db38 --- /dev/null +++ b/.changeset/yellow-clocks-think.md @@ -0,0 +1,19 @@ +--- +"@talismn/chaindata-provider": patch +"@talismn/chain-connectors": patch +"@talismn/connection-meta": patch +"@talismn/balances-react": patch +"@talismn/on-chain-id": patch +"@talismn/token-rates": patch +"@talismn/balances": patch +"@talismn/keyring": patch +"@talismn/crypto": patch +"@talismn/solana": patch +"@talismn/icons": patch +"@talismn/scale": patch +"@talismn/sapi": patch +"@talismn/util": patch +"@talismn/orb": patch +--- + +migrate eslint+prettier to biome diff --git a/.dockerignore b/.dockerignore index 124188cd43..741d858e30 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,7 +4,9 @@ .next .turbo .i18next-parser -dist +# Exclude dist directories except for .papi (pre-built polkadot-api descriptors) +**/dist +!.papi/**/dist review build node_modules diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 7cdac4ded2..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -!.prettierrc.js -**/dist/ diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index b3d188e76e..0000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - root: true, - // default config (common + react) - extends: ["@talismn/eslint-config/react"], -} diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5070c8527b..6e3fbfb108 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,10 +8,14 @@ ## Coding & Tooling Standards -- Use **Node >= 20**, `corepack enable`, and **pnpm** commands from `package.json`/`turbo.json`. -- Formatting & linting: Prettier + `@talismn/eslint-config` (`eslint --max-warnings 0`). Keep `_`-prefixed unused vars if needed. -- Write/maintain unit tests (Jest) and E2E tests (Playwright). Commands: - - `pnpm test` (workspace-wide Jest) +- Use **Node >= 20**, `corepack enable`, and **pnpm** commands from `package.json`. +- Formatting & linting: **[Biome](https://biomejs.dev/)** handles both. Commands: + - `pnpm lint` (check linting on changed files) + - `pnpm chore:format` (format changed files) + - Pre-commit hook runs `biome check --staged` automatically +- Keep `_`-prefixed unused vars if needed (configured in `biome.json`). +- Write/maintain unit tests (Vitest) and E2E tests (Playwright). Commands: + - `pnpm test` (workspace-wide Vitest) - `pnpm exec playwright test` (E2E) and variants in `package.json` - Use `pnpm changeset` for versioned packages; respect CI expectations in `.github/workflows/ci.yml`. - I18n: wrap UI strings with `t()`/`Trans` and run `pnpm chore:update-translations` when keys change. @@ -69,9 +73,9 @@ ## Testing & Verification Checklist -1. Re-use existing Jest/Playwright helpers when adding tests; keep mocks aligned with `apps/extension/tests`. +1. Re-use existing Vitest/Playwright helpers when adding tests; keep mocks aligned with `apps/extension/tests`. 2. Validate new hooks/components with real APIs (RxJS streams, Dexie, background APIs) and cover race/error paths. -3. Ensure new commands/config entries work with `turbo run build` and `pnpm build:extension*` matrix. +3. Ensure new commands/config entries work with `pnpm build` and `pnpm build:extension*` matrix. 4. Any change that touches the keyring or secret storage must ship with dedicated unit tests. ## Commenting & Documentation diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78aab1391e..ff2964ad96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,16 +20,15 @@ jobs: name: "Build and test the wallet" timeout-minutes: 15 runs-on: ubuntu-latest - # To use Remote Caching, uncomment the next lines and follow the steps below. - # env: - # TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - # TURBO_TEAM: ${{ secrets.TURBO_TEAM }} steps: - name: Checkout repo uses: actions/checkout@v4 with: - fetch-depth: 2 # also get the previous commit + fetch-depth: 2 + - name: Fetch base branch for comparison + if: github.event_name == 'pull_request' + run: git fetch origin ${{ github.base_ref }} --depth=1 - name: Update & enable corepack run: npm install -g corepack@latest && corepack enable - name: Setup pnpm @@ -37,12 +36,33 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 24 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Biome check + run: | + BASE_REF="${{ github.base_ref || 'dev' }}" + # Get changed files using two-dot diff (works with shallow clones) + mapfile -t CHANGED_FILES < <( + git diff --name-only --diff-filter=ACMR origin/$BASE_REF HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx' '*.json' '*.css' '*.cjs' '*.mjs' + ) + + if [ "${#CHANGED_FILES[@]}" -eq 0 ]; then + echo "No supported files changed - skipping biome check" + exit 0 + fi + + echo "Changed files:" + printf ' %q\n' "${CHANGED_FILES[@]}" + pnpm biome check --error-on-warnings -- "${CHANGED_FILES[@]}" - name: Test - run: pnpm preconstruct:dev && pnpm test + run: pnpm test + - name: Typecheck + run: | + # All packages and apps use customConditions: ["@talismn/source"] to resolve + # @talismn/* packages directly from source. This means no package build is needed. + pnpm typecheck - name: Extract short SHA + package version id: vars run: | @@ -59,8 +79,8 @@ jobs: command: "upload" cli-version: "2.2.0" args: "--uploadPath ./apps/extension/.i18next-parser/locales/{lang}/{ns}.json --apiKey ${{ secrets.SIMPLE_LOCALIZE_API_KEY }}" - - name: Build - run: pnpm build:extension:ci + - name: Build extension + run: pnpm build:extension env: COMMIT_SHA_SHORT: ${{ steps.vars.outputs.sha_short }} POSTHOG_AUTH_TOKEN: ${{ secrets.POSTHOG_AUTH_TOKEN }} @@ -71,7 +91,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: build - path: ./apps/extension/dist/chrome/talisman_extension_ci_${{ steps.vars.outputs.sha_short }}_chrome.zip + path: ./apps/extension/dist/*.zip retention-days: 5 # This job will use the files from the build job to run Playwright tests @@ -92,7 +112,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 24 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile @@ -105,8 +125,8 @@ jobs: path: ./build-artifact - name: Unzip build artifact to the playwright extension path run: | - mkdir -p ./apps/extension/dist/chrome - unzip ./build-artifact/*.zip -d ./apps/extension/dist/chrome + mkdir -p ./apps/extension/dist/chrome-mv3 + unzip ./build-artifact/*.zip -d ./apps/extension/dist/chrome-mv3 - name: Cache playwright browsers uses: actions/cache@v3 with: @@ -133,6 +153,7 @@ jobs: # This job will build and publish a snapshot version of the packages which have changesets in this PR publish_snapshot: name: "Publish a snapshot version of any packages with changesets in this PR to npm" + needs: ensure_pr_has_changeset timeout-minutes: 15 runs-on: ubuntu-latest if: github.event_name == 'pull_request' @@ -149,13 +170,13 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 24 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile - name: Bump changed package versions to a snapshot version run: pnpm changeset version --snapshot pr${{ github.event.pull_request.number }} - - name: Build snapshot packages + - name: Build packages for npm publish run: pnpm build:packages - name: Set publish config run: pnpm config set '//registry.npmjs.org/:_authToken' "${PNPM_TOKEN}" @@ -185,7 +206,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 24 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile @@ -323,7 +344,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 24 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile @@ -337,7 +358,7 @@ jobs: env: # GITHUB_TOKEN is automatically added into the ENV by GitHub CI GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Build packages + - name: Build packages for npm publish if: steps.changesets.outputs.hasChangesets == 'false' run: pnpm build:packages - name: Set publish config diff --git a/.gitignore b/.gitignore index e85d458ac9..89c236ebea 100644 --- a/.gitignore +++ b/.gitignore @@ -31,9 +31,16 @@ yarn-error.log* # turbo .turbo +# typescript +*.tsbuildinfo + +# tsup temp files +tsup.config.bundled_* + # extension dist review +.wxt # Playwright /test-results/ @@ -42,4 +49,6 @@ review /playwright/.cache/ # tmp folder for translations extraction -.i18next-parser/ \ No newline at end of file +.i18next-parser/ + +/.verify-build/ \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index cb2c84d5c3..0ec747e7dc 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,22 @@ -pnpm lint-staged +# Run Biome on staged files and auto-fix issues +# Disable errexit for this block since we handle exit codes manually +set +e +output=$(pnpm biome check --staged --write --error-on-warnings 2>&1) +exit_code=$? +set -e +echo "$output" + +if [ $exit_code -eq 0 ]; then + # Biome succeeded - all checks passed + exit 0 +elif echo "$output" | grep -q "No files were processed"; then + # No supported files staged (e.g., only YAML/MD files) - skip gracefully + # Biome only supports JS/TS/JSX/TSX/JSON/CSS + exit 0 +elif echo "$output" | grep -q "The list is empty"; then + # No files staged at all (e.g., --allow-empty commit) + exit 0 +else + # Actual lint/format errors - fail the commit + exit $exit_code +fi diff --git a/.importsortrc b/.importsortrc deleted file mode 100644 index 582751bb6b..0000000000 --- a/.importsortrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - ".ts, .tsx, .js, .jsx": { - "style": "module", - "parser": "typescript" - } -} diff --git a/.npmrc b/.npmrc index ac3b1180ec..3c9b59921b 100644 --- a/.npmrc +++ b/.npmrc @@ -28,4 +28,7 @@ use-node-version=24.8.0 ; Once that's done, we can delete this comment! shamefully-hoist=true +; Increase Node.js heap size for memory-intensive builds (WXT/Vite bundling). +; This helps on memory-constrained systems (e.g., WSL with limited RAM). +node-options="--max-old-space-size=4096" save-exact=true \ No newline at end of file diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 378e22f49e..0000000000 --- a/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -.DS_Store -node_modules -dist - -package.json -pnpm-lock.yaml diff --git a/.prettierrc.cjs b/.prettierrc.cjs deleted file mode 100644 index b6c52ebb8d..0000000000 --- a/.prettierrc.cjs +++ /dev/null @@ -1,38 +0,0 @@ -/** @type {import('prettier').Config} */ -const importSortConfig = { - importOrder: [ - // built-ins like `node:fs` - "^(node:)", // type imports - "", // imports - "", // a gap - - // anything which doesn't match any other rules - "", // type imports - "", // imports - "", // a gap - - // local aliases / packages starting with one of these prefixes - "^(@common|@talisman|@ui|@tests)(/.*)?$", // type imports - "^(@common|@talisman|@ui|@tests)(/.*)?$", // imports - "", // a gap - - // local `./blah/something` packages - "^[.]", // type imports - "^[.]", // imports - ], - - // defaults to "1.0.0" - higher versions of typescript unlock more import-sort capabilities - // https://github.com/IanVS/prettier-plugin-sort-imports?tab=readme-ov-file#importordertypescriptversion - importOrderTypeScriptVersion: "5.6.3", -} - -/** @type {import('prettier').Config} */ -module.exports = { - plugins: ["@ianvs/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"], - - ...importSortConfig, - - printWidth: 100, - quoteProps: "consistent", - semi: false, -} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index ad84f89809..d81f6d9405 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,3 @@ { - "recommendations": [ - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode", - "bradlc.vscode-tailwindcss" - ] + "recommendations": ["biomejs.biome", "bradlc.vscode-tailwindcss"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 2c9b775cff..c1c8758fca 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,5 +11,34 @@ // defaults to 1rem=16px, but we use 1rem=10px in the extension // this setting will provide accurate intellisense values - "tailwindCSS.rootFontSize": 10 + "tailwindCSS.rootFontSize": 10, + + "biome.lsp.bin": "node_modules/.bin/biome", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.biome": "always" + }, + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[css]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "prettier.enable": false, + "eslint.format.enable": false } diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 3f923178d8..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM node:24.8.0 -RUN npm install -g corepack@latest && corepack enable - -WORKDIR /talisman -COPY . ./ - -RUN pnpm clean -RUN pnpm install --frozen-lockfile diff --git a/Dockerfile.firefox b/Dockerfile.firefox new file mode 100644 index 0000000000..f3ab695ead --- /dev/null +++ b/Dockerfile.firefox @@ -0,0 +1,43 @@ +# Dockerfile for reproducible Firefox extension builds +# +# This Dockerfile creates a consistent build environment to ensure +# deterministic output across different build machines. +# +# Usage: +# docker build -t talisman-builder -f Dockerfile.firefox . +# docker run --rm -v $(pwd)/apps/extension/dist:/talisman/apps/extension/dist talisman-builder +# +FROM node:24.8.0-slim + +# Set locale for consistent file sorting (critical for deterministic builds) +ENV LC_ALL=C.UTF-8 +ENV LANG=C.UTF-8 +ENV LANGUAGE=C.UTF-8 + +# Disable color output for consistency +ENV NO_COLOR=1 +ENV FORCE_COLOR=0 + +# Set timezone to UTC for consistent timestamps +ENV TZ=UTC + +# Git SHA for build identification (passed from host where git is available) +ARG COMMIT_SHA_SHORT=unknown +ENV COMMIT_SHA_SHORT=$COMMIT_SHA_SHORT + +# Install corepack for pnpm support +RUN npm install -g corepack@latest && corepack enable + +WORKDIR /talisman + +# Copy all source files (we need .papi for postinstall) +COPY . ./ + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Build the production Firefox extension +# WXT bundles directly from package source directories via Vite aliases, +# so we don't need to pre-build the @talismn/* packages with tsup +# The entrypoint builds and then copies output to /output if mounted +CMD ["sh", "-c", "pnpm run --filter extension build:prod:firefox && if [ -d /output ]; then cp /talisman/apps/extension/dist/*.zip /output/; fi"] diff --git a/FIREFOX_SOURCE_CODE_REVIEW.md b/FIREFOX_SOURCE_CODE_REVIEW.md new file mode 100644 index 0000000000..d0d33cfa1c --- /dev/null +++ b/FIREFOX_SOURCE_CODE_REVIEW.md @@ -0,0 +1,45 @@ +# Source Code Review Instructions + +This document provides instructions for Firefox Add-on reviewers to build the Talisman browser extension from source. + +## Prerequisites + +- **Docker**: Any recent version (20.10+) +- **Network access**: Docker must be able to reach the npm registry to download dependencies during the build + +## Build Instructions + +1. **Extract the sources ZIP** to a directory of your choice. + +2. **Build using Docker**: + + ```bash + docker build --no-cache -t talisman-builder -f Dockerfile.firefox . + docker run --rm -v $(pwd)/output:/output talisman-builder + ``` + +3. **Find the built extension**: + The built extension ZIP will be in the `output/` directory. + +## Build Reproducibility + +This build produces **byte-identical, reproducible output**. The ZIP file checksums will match exactly when rebuilt from the same sources. + +### Key reproducibility features: + +- **Docker isolation**: Deterministic Node.js environment with fixed locale/timezone +- **Normalized timestamps**: All ZIP entries use a fixed timestamp (2000-01-01T00:00:00Z) +- **Deterministic bundling**: Rollup output is sorted and consistent across builds +- **Two-pass build**: Production builds are always built from `sources.zip` to ensure what's shipped matches what reviewers build + +### Verification + +To verify the build matches the submitted extension, compare SHA-256 checksums: + +```bash +# The rebuilt ZIP should have the exact same hash +shasum -a 256 output/*-firefox.zip +shasum -a 256 submitted-firefox.zip +``` + +The hashes should be **identical**. No extraction or file-by-file comparison is needed. diff --git a/README.md b/README.md index 3e872289f7..86e154b70f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![firefox-addon-link](https://img.shields.io/amo/v/talisman-wallet-extension?logo=firefox&logoColor=white&style=flat-square)](https://addons.mozilla.org/en-US/firefox/addon/talisman-wallet-extension) **Multi-Chain Made Easy** with Talisman Wallet. -An ultra-secure Ethereum and Polkadot wallet for both beginners and pros. +An ultra-secure Ethereum, Polkadot and Solana wallet for both beginners and pros. ## What's inside? @@ -49,19 +49,28 @@ Once you have installed **Node.js**, run `corepack enable` to turn it on, then f `pnpm install` -1. Start the dev server, waiting for it to generate the `dist` directory. +1. Start the dev server (uses WXT/Vite for fast rebuilds and HMR). `pnpm dev:extension` 1. Open Chrome and navigate to `chrome://extensions`. 1. Turn on the `Developer mode` toggle on the top right of the page. -1. Click `Load unpacked` on the top left of the page and select the `apps/extension/dist/chrome` directory. -1. Change some code! +1. Click `Load unpacked` on the top left of the page and select the `apps/extension/dist/chrome-mv3-dev` directory. +1. Change some code! The extension will hot-reload automatically. + +### Firefox Development + +To develop for Firefox instead: + +```bash +pnpm dev:extension:firefox +``` + +Then load the extension from `apps/extension/dist/firefox-mv3-dev` in Firefox's `about:debugging` page. ## Apps and packages -- `apps/extension`: the non-custodial Talisman Wallet browser extension -- `packages/eslint-config`: shared `eslint` configurations +- `apps/extension`: the non-custodial Talisman Wallet browser extension (built with [WXT](https://wxt.dev/)/Vite) - `packages/tsconfig`: shared `tsconfig.json`s used throughout the monorepo - `packages/util`: library containing shared non-react code. It is not meant to be npm published. @@ -69,11 +78,21 @@ All our apps and packages are 100% [TypeScript](https://www.typescriptlang.org/) ## Writing and running tests -- Testing is carried out with Jest. +- Testing is carried out with [Vitest](https://vitest.dev/). - Tests can be written in `*.spec.ts` files, inside a `__tests__` folder. - Follow the pattern in `apps/extension/src/core/handlers/Extension.spec.ts` or `apps/extension/src/core/domains/signing/__tests__/requestsStore.spec.ts` - Tests are run with `pnpm test` +## Code quality + +We use [Biome](https://biomejs.dev/) for linting and formatting across the monorepo. + +- **Format code**: `pnpm chore:format` +- **Lint code**: `pnpm lint` +- **Pre-commit hook**: Automatically runs Biome checks on staged files + +If you're using VS Code, install the [Biome extension](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) for automatic formatting on save. + ## i18n (wallet extension development) We use i18next in the wallet to make it available in a bunch of languages. @@ -83,13 +102,13 @@ When building UI features, please follow the following spec to ensure they're tr 1. Import the `useTranslation` hook into your React components: ```tsx - import { useTranslation } from "react-i18next" + import { useTranslation } from "react-i18next"; ``` 1. Use the hook in your component to get access to the `t` function: ```tsx - const { t } = useTranslation() + const { t } = useTranslation(); ``` 1. Wrap any user-visible language in your component with the `t` function: @@ -102,7 +121,7 @@ When building UI features, please follow the following spec to ensure they're tr {t("Account has {{assetCount}} assets", { assetCount: assets.length })} - ) + ); ``` 1. If you want to include any react components in your translation, you will need to use the `Trans` component: @@ -158,20 +177,41 @@ When building UI features, please follow the following spec to ensure they're tr ### Scripts - `chore:update-translations` : finds all of the i18n strings in the codebase and adds them to the english translations files which i18next loads in development builds of the wallet -- `dev` : builds and watches all packages/apps with hot reloading -- `dev:extension` : when working on extension only, for better color output -- `build`: builds the wallet in `packages/apps/extension/dist/chrome` folder, without sentry keys -- `build:firefox`: builds the wallet in `packages/apps/extension/dist/firefox` folder, without sentry keys -- `build:extension:prod` builds the Talisman browser extension (requires sentry settings, Talisman team only) -- `build:extension:canary` : builds the Talisman browser extension test version, with different ID and icon than prod -### Build the wallet browser extension using Docker +#### Development + +- `dev` : starts the extension dev server (alias for `dev:extension`) +- `dev:extension` : starts WXT dev server with hot module replacement for Chrome +- `dev:extension:firefox` : starts WXT dev server for Firefox + +#### Production Builds + +- `build:extension` : builds the extension for Chrome (outputs to `dist/chrome-mv3`) +- `build:extension:firefox` : builds the extension for Firefox (outputs to `dist/firefox-mv3`) +- `build:extension:prod` : production Chrome build with Sentry sourcemap upload +- `build:extension:prod:firefox` : production Firefox build via Docker (reproducible) +- `build:extension:canary` : canary Chrome build for internal testing +- `build:extension:canary:firefox` : canary Firefox build for internal testing + +### Firefox Production Builds + +Firefox production builds use a **two-pass Docker build** to ensure reproducibility: + +1. **Pass 1**: Build from repo → generates `sources.zip` +2. **Pass 2**: Rebuild from `sources.zip` → produces final deliverables + +This guarantees that the shipped extension is built from the exact sources that are shipped, which is what Firefox Add-on reviewers will do. The build produces byte-identical output with matching SHA-256 checksums. ```bash -# builds with docker, outputs in dist folder at the root of the monorepo -rm -rf dist && DOCKER_BUILDKIT=1 docker build --output type=local,dest=./dist . +# Build Firefox production extension (requires Docker with network access) +pnpm build:extension:prod:firefox + +# Verify the build is reproducible +./scripts/verify-reproducible-build.sh ``` +See [FIREFOX_SOURCE_CODE_REVIEW.md](FIREFOX_SOURCE_CODE_REVIEW.md) for reviewer instructions. + ### Update packages ```bash diff --git a/apps/balances-bench/README.md b/apps/balances-bench/README.md new file mode 100644 index 0000000000..cc0f0c8921 --- /dev/null +++ b/apps/balances-bench/README.md @@ -0,0 +1,24 @@ +# Balances Bench + +Benchmarking scripts for testing and debugging the `@talismn/balances` module against various chains. + +## Prerequisites + +Build workspace packages before running any dev scripts: + +```sh +pnpm build:packages +``` + +## Usage + +Run a benchmark script in watch mode: + +```sh +pnpm --filter balances-bench dev:pah # Polkadot Asset Hub +pnpm --filter balances-bench dev:polkadot # Polkadot +pnpm --filter balances-bench dev:ethereum # Ethereum +pnpm --filter balances-bench dev:solana # Solana +``` + +See `package.json` for the full list of available chains. diff --git a/apps/balances-bench/package.json b/apps/balances-bench/package.json index cf07740a87..643e510199 100644 --- a/apps/balances-bench/package.json +++ b/apps/balances-bench/package.json @@ -1,8 +1,8 @@ { "name": "balances-bench", "version": "1.0.0", - "description": "", - "main": "index.js", + "private": true, + "description": "Benchmarking scripts for balances", "scripts": { "dev:ethereum": "tsx watch src/ethereum.ts", "dev:hydration": "tsx watch src/hydration.ts", @@ -18,14 +18,14 @@ "dev:ewx": "tsx watch src/ewx.ts", "dev:interlay": "tsx watch src/interlay.ts", "dev:solana": "tsx watch src/solana.ts", - "clean": "rm -rf node_modules" + "clean": "rm -rf node_modules", + "typecheck": "tsc --noEmit" }, "keywords": [], "author": "", "license": "ISC", "packageManager": "pnpm@10.10.0", "dependencies": { - "@polkadot-api/utils": "0.2.0", "@polkadot/rpc-provider": "16.1.2", "@polkadot/util": "13.5.3", "@polkadot/util-crypto": "13.5.3", @@ -34,14 +34,9 @@ "@talismn/chaindata-provider": "workspace:^", "@talismn/sapi": "workspace:^", "@talismn/scale": "workspace:^", - "extension-shared": "workspace:^", - "lodash-es": "4.17.21", - "rxjs": "^7.8.2", - "viem": "^2.27.3", - "zod": "^3.25.76" + "extension-shared": "workspace:^" }, "devDependencies": { - "@types/lodash-es": "4.17.12", "tsx": "^4.20.3" } } diff --git a/apps/balances-bench/src/browser-stubs.d.ts b/apps/balances-bench/src/browser-stubs.d.ts new file mode 100644 index 0000000000..90a3bf3f31 --- /dev/null +++ b/apps/balances-bench/src/browser-stubs.d.ts @@ -0,0 +1,129 @@ +/** + * Minimal browser API type stubs for typechecking. + * This allows for balances-bench to typecheck without building @talismn/* packages + * + * balances-bench is a Node.js app, but it imports from @talismn/* packages + * that have browser API references. These stubs allow typechecking to pass + * without pulling in full DOM types (which conflict with Node.js types). + * + * At runtime, the necessary polyfills are applied (e.g., globalThis.crypto = webcrypto). + */ + +// Stub for window global (used in chain-connectors for browser detection) +declare const window: Window & typeof globalThis + +// Stub for indexedDB global (used in chaindata-provider and token-rates for Dexie) +declare const indexedDB: IDBFactory + +// Minimal types needed for the stubs +interface Window { + addEventListener: typeof globalThis.addEventListener + removeEventListener: typeof globalThis.removeEventListener +} + +interface IDBFactory { + open(name: string, version?: number): IDBOpenDBRequest + deleteDatabase(name: string): IDBOpenDBRequest + databases(): Promise + cmp(first: unknown, second: unknown): number +} + +interface IDBOpenDBRequest extends IDBRequest { + onupgradeneeded: ((this: IDBOpenDBRequest, ev: IDBVersionChangeEvent) => unknown) | null + onblocked: ((this: IDBOpenDBRequest, ev: Event) => unknown) | null +} + +interface IDBRequest extends EventTarget { + readonly error: DOMException | null + readonly result: T + readonly source: IDBObjectStore | IDBIndex | IDBCursor | null + readonly readyState: IDBRequestReadyState + readonly transaction: IDBTransaction | null + onerror: ((this: IDBRequest, ev: Event) => unknown) | null + onsuccess: ((this: IDBRequest, ev: Event) => unknown) | null +} + +type IDBRequestReadyState = "pending" | "done" + +interface IDBDatabase extends EventTarget { + readonly name: string + readonly version: number + readonly objectStoreNames: DOMStringList + close(): void + createObjectStore(name: string, options?: IDBObjectStoreParameters): IDBObjectStore + deleteObjectStore(name: string): void + transaction(storeNames: string | string[], mode?: IDBTransactionMode): IDBTransaction +} + +interface IDBDatabaseInfo { + name?: string + version?: number +} + +interface IDBVersionChangeEvent extends Event { + readonly newVersion: number | null + readonly oldVersion: number +} + +interface IDBObjectStoreParameters { + autoIncrement?: boolean + keyPath?: string | string[] | null +} + +interface IDBObjectStore { + readonly name: string + readonly keyPath: string | string[] + readonly indexNames: DOMStringList + readonly transaction: IDBTransaction + readonly autoIncrement: boolean +} + +interface IDBIndex { + readonly name: string + readonly keyPath: string | string[] + readonly multiEntry: boolean + readonly unique: boolean +} + +interface IDBCursor { + readonly direction: IDBCursorDirection + readonly key: IDBValidKey + readonly primaryKey: IDBValidKey + readonly source: IDBObjectStore | IDBIndex +} + +interface IDBTransaction extends EventTarget { + readonly db: IDBDatabase + readonly mode: IDBTransactionMode + readonly objectStoreNames: DOMStringList + objectStore(name: string): IDBObjectStore + abort(): void + commit(): void +} + +type IDBTransactionMode = "readonly" | "readwrite" | "versionchange" +type IDBCursorDirection = "next" | "nextunique" | "prev" | "prevunique" +type IDBValidKey = number | string | Date | BufferSource | IDBValidKey[] + +interface DOMStringList { + readonly length: number + contains(string: string): boolean + item(index: number): string | null + [index: number]: string +} + +interface DOMException extends Error { + readonly code: number + readonly name: string +} + +// MessageEvent needs to be generic (Node.js has non-generic MessageEvent) +// Use 'any' for ports to avoid conflicts with Node.js MessagePort +declare class MessageEvent extends Event { + readonly data: T + readonly lastEventId: string + readonly origin: string + // biome-ignore lint/suspicious/noExplicitAny: Avoid conflict with Node.js MessagePort + readonly ports: readonly any[] + readonly source: unknown +} diff --git a/apps/balances-bench/src/common/testNetworkDot.ts b/apps/balances-bench/src/common/testNetworkDot.ts index 7b12d80d8b..c1257363a8 100644 --- a/apps/balances-bench/src/common/testNetworkDot.ts +++ b/apps/balances-bench/src/common/testNetworkDot.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs" -import { dirname } from "path" -import { BALANCE_MODULES, MiniMetadata } from "@talismn/balances" -import { ChainConnectorDotStub, IChainConnectorDot } from "@talismn/chain-connectors" -import { DotNetwork, Token, TokenType } from "@talismn/chaindata-provider" +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs" +import { dirname } from "node:path" +import { BALANCE_MODULES, type MiniMetadata } from "@talismn/balances" +import { ChainConnectorDotStub, type IChainConnectorDot } from "@talismn/chain-connectors" +import type { DotNetwork, Token, TokenType } from "@talismn/chaindata-provider" import { fetchBestMetadata } from "@talismn/sapi" import { decAnyMetadata, @@ -25,6 +25,7 @@ const TEST_ADDRESS_EMPTY = "14BbPtmnepvdw2t34CvUbNGDxXazc4iHJZPc8vS3MiCDFzpn" export type DotNetworkConfig = Pick & { nativeCurrency?: Partial tokens: Partial> + // biome-ignore lint/suspicious/noExplicitAny: balances config varies per token type balancesConfig?: Partial> } @@ -36,7 +37,7 @@ type TestOptions = { const DEFAULT_OPTIONS: TestOptions = { modules: BALANCE_MODULES.filter((mod) => mod.platform === "polkadot").map( - (mod) => mod.type as TokenType, + (mod) => mod.type as TokenType ), fetchBalances: true, transfer: true, @@ -47,10 +48,11 @@ export const testNetworkDot = async (network: DotNetworkConfig, options?: TestOp const connector: IChainConnectorDot = new ChainConnectorDotStub(network as unknown as DotNetwork) - const stopAll = log.timer("testDotNetwork " + network.id) + const stopAll = log.timer(`testDotNetwork ${network.id}`) const miniMetadatas: MiniMetadata[] = [] let tokens: Token[] | null = null + // biome-ignore lint/suspicious/noExplicitAny: dry run result shape is dynamic let dryRun: any = null try { @@ -58,7 +60,7 @@ export const testNetworkDot = async (network: DotNetworkConfig, options?: TestOp const { specVersion } = await connector.send<{ specVersion: number }>( network.id, "state_getRuntimeVersion", - [], + [] ) stop2() log.log("RuntimeVersion", { specVersion }) @@ -73,7 +75,7 @@ export const testNetworkDot = async (network: DotNetworkConfig, options?: TestOp const stop = log.timer("Fetched metadata") const metadataRpc = await fetchBestMetadata( (...args) => connector.send(networkId, ...args), - false, + false ) stop() writeFileSync(metadataFilePath, metadataRpc) @@ -85,7 +87,7 @@ export const testNetworkDot = async (network: DotNetworkConfig, options?: TestOp log.log("Metadata version", metadata.version) for (const mod of BALANCE_MODULES.filter( - (mod) => mod.platform === "polkadot", // then we can use a ChainConnector + (mod) => mod.platform === "polkadot" // then we can use a ChainConnector ).filter((mod) => opts.modules?.includes(mod.type as TokenType))) { const source = mod.type log.log() @@ -117,8 +119,10 @@ export const testNetworkDot = async (network: DotNetworkConfig, options?: TestOp tokens = await mod.fetchTokens({ networkId, + // biome-ignore lint/suspicious/noExplicitAny: partial config types don't match module expectations tokens: tokenConfigs as any, connector, + // biome-ignore lint/suspicious/noExplicitAny: miniMetadata type varies per module miniMetadata: miniMetadata as any, cache: {}, }) @@ -136,6 +140,7 @@ export const testNetworkDot = async (network: DotNetworkConfig, options?: TestOp networkId, tokensWithAddresses: tokens.map((token) => [token, BALANCES_ADDRESSES] as const), connector, + // biome-ignore lint/suspicious/noExplicitAny: miniMetadata type varies per module miniMetadata: miniMetadata as any, }) @@ -147,6 +152,7 @@ export const testNetworkDot = async (network: DotNetworkConfig, options?: TestOp .concat(balances.dynamicTokens) .map((token) => [token, BALANCES_ADDRESSES] as const), connector, + // biome-ignore lint/suspicious/noExplicitAny: miniMetadata type varies per module miniMetadata: miniMetadata as any, }) } @@ -170,7 +176,7 @@ export const testNetworkDot = async (network: DotNetworkConfig, options?: TestOp (b) => b.address === TEST_ADDRESS_SUB && ((b.value && !!BigInt(b.value)) || - b.values?.find((v) => v.type === "free" && !!BigInt(v.amount))), + b.values?.find((v) => v.type === "free" && !!BigInt(v.amount))) ) if (!anyPositiveBalance) { log.log("No positive balance found for the test address") @@ -188,7 +194,7 @@ export const testNetworkDot = async (network: DotNetworkConfig, options?: TestOp const available = anyPositiveBalance.value ?? - anyPositiveBalance.values?.find((v) => v.type === "free")!.amount + anyPositiveBalance.values?.find((v) => v.type === "free")?.amount if (!available || BigInt(available) <= BigInt(0)) { log.error("No available balance found for the test address") continue @@ -233,8 +239,11 @@ export const testNetworkDot = async (network: DotNetworkConfig, options?: TestOp log.log(papiStringify(dryRun, 2)) } stopAll() + + return { tokens, miniMetadatas, dryRun } } catch (err) { log.error(err) connector.asProvider(network.id).disconnect() + return { tokens: null, miniMetadatas: [], dryRun: null } } } diff --git a/apps/balances-bench/src/common/testNetworkSol.ts b/apps/balances-bench/src/common/testNetworkSol.ts index 5d741abcbf..4498933e40 100644 --- a/apps/balances-bench/src/common/testNetworkSol.ts +++ b/apps/balances-bench/src/common/testNetworkSol.ts @@ -1,12 +1,10 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { existsSync, readFileSync, writeFileSync } from "fs" - +import { existsSync, readFileSync, writeFileSync } from "node:fs" import { BALANCE_MODULES } from "@talismn/balances" import { ChainConnectorSolStub } from "@talismn/chain-connectors" -import { TokenType } from "@talismn/chaindata-provider" -import { SolNetwork } from "@talismn/chaindata-provider/src/chaindata/networks/SolNetwork" +import type { SolNetwork, TokenType } from "@talismn/chaindata-provider" import { log } from "extension-shared" export type SolNetworkConfig = Pick & { @@ -26,7 +24,7 @@ type TestOptions = { const DEFAULT_OPTIONS: TestOptions = { modules: BALANCE_MODULES.filter((mod) => mod.platform === "solana").map( - (mod) => mod.type as TokenType, + (mod) => mod.type as TokenType ), fetchBalances: true, transfer: true, @@ -48,7 +46,7 @@ export const testNetworkSol = async (network: SolNetworkConfig, options?: TestOp const connector = new ChainConnectorSolStub(network) for (const mod of BALANCE_MODULES.filter((mod) => mod.platform === "solana").filter((mod) => - opts.modules?.includes(mod.type as TokenType), + opts.modules?.includes(mod.type as TokenType) )) { const source = mod.type log.log() @@ -58,6 +56,7 @@ export const testNetworkSol = async (network: SolNetworkConfig, options?: TestOp log.log() const tokenConfigs = + // biome-ignore lint/suspicious/noExplicitAny: token config shape varies by module type mod.type === "sol-native" ? [network.nativeCurrency] : (network.tokens[mod.type] as any) log.log("Token configs", tokenConfigs) log.log() @@ -66,7 +65,7 @@ export const testNetworkSol = async (network: SolNetworkConfig, options?: TestOp networkId, tokens: tokenConfigs, connector, - // @ts-ignore + // @ts-expect-error cache: cache[mod.type] ?? {}, }) diff --git a/apps/balances-bench/src/common/utils.ts b/apps/balances-bench/src/common/utils.ts deleted file mode 100644 index f215909083..0000000000 --- a/apps/balances-bench/src/common/utils.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { EthNetwork, EthNetworkId } from "@talismn/chaindata-provider" -import { camelCase, fromPairs, toPairs } from "lodash-es" -import { Chain, ChainContract, createPublicClient, fallback, http, PublicClient } from "viem" -import * as viemChains from "viem/chains" - -// exclude zoraTestnet which uses Hyperliquid's chain id -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const { zoraTestnet, ...validViemChains } = viemChains - -// viem chains benefit from multicall config & other viem goodies -const VIEM_CHAINS = Object.keys(validViemChains).reduce( - (acc, curr) => { - const chain = validViemChains[curr as keyof typeof validViemChains] - acc[chain.id] = chain - return acc - }, - {} as Record, -) - -const chainsCache = new Map() - -export const clearChainsCache = (networkId?: EthNetworkId) => { - if (networkId) chainsCache.delete(networkId) - else chainsCache.clear() -} - -export const getChainFromEvmNetwork = (network: EthNetwork): Chain => { - const { symbol, decimals } = network.nativeCurrency - - if (!chainsCache.has(network.id)) { - const chainRpcs = network.rpcs ?? [] - - const viemChain = VIEM_CHAINS[Number(network.id)] ?? {} - - const chain: Chain = { - ...viemChain, - id: Number(network.id), - name: network.name ?? `Ethereum Chain ${network.id}`, - rpcUrls: { - public: { http: chainRpcs }, - default: { http: chainRpcs }, - }, - nativeCurrency: { - symbol, - decimals, - name: symbol, - }, - contracts: { - ...viemChain.contracts, - ...(network.contracts - ? fromPairs( - toPairs(network.contracts).map(([name, address]): [string, ChainContract] => [ - camelCase(name), - { address }, - ]), - ) - : {}), - }, - } - - chainsCache.set(network.id, chain) - } - - return chainsCache.get(network.id) as Chain -} - -export type TransportOptions = { - batch?: - | boolean - | { - batchSize?: number | undefined - wait?: number | undefined - } -} - -export const getTransportForEvmNetwork = ( - evmNetwork: EthNetwork, - options: TransportOptions = {}, -) => { - if (!evmNetwork.rpcs?.length) throw new Error("No RPCs found for EVM network") - - const { batch } = options - - return fallback( - evmNetwork.rpcs.map((url) => http(url, { batch, retryCount: 0 })), - { retryCount: 0 }, - ) -} - -const MUTLICALL_BATCH_WAIT = 25 -const MUTLICALL_BATCH_SIZE = 100 - -const HTTP_BATCH_WAIT = 25 -const HTTP_BATCH_SIZE_WITH_MULTICALL = 10 -const HTTP_BATCH_SIZE_WITHOUT_MULTICALL = 30 - -// cache to reuse previously created public clients -const publicClientCache = new Map() - -export const clearPublicClientCache = (evmNetworkId?: string) => { - clearChainsCache(evmNetworkId) - - if (evmNetworkId) publicClientCache.delete(evmNetworkId) - else publicClientCache.clear() -} - -export const getEvmNetworkPublicClient = (network: EthNetwork): PublicClient => { - const chain = getChainFromEvmNetwork(network) - - if (!publicClientCache.has(network.id)) { - if (!network.rpcs.length) throw new Error("No RPCs found for Ethereum network") - - const batch = chain.contracts?.multicall3 - ? { multicall: { wait: MUTLICALL_BATCH_WAIT, batchSize: MUTLICALL_BATCH_SIZE } } - : undefined - - const transportOptions = { - batch: { - batchSize: chain.contracts?.multicall3 - ? HTTP_BATCH_SIZE_WITH_MULTICALL - : HTTP_BATCH_SIZE_WITHOUT_MULTICALL, - wait: HTTP_BATCH_WAIT, - }, - } - - const transport = getTransportForEvmNetwork(network, transportOptions) - - publicClientCache.set( - network.id, - createPublicClient({ - chain, - transport, - batch, - }), - ) - } - - return publicClientCache.get(network.id) as PublicClient -} diff --git a/apps/balances-bench/tsconfig.json b/apps/balances-bench/tsconfig.json index 8a5955ad4c..b6d27e03b2 100644 --- a/apps/balances-bench/tsconfig.json +++ b/apps/balances-bench/tsconfig.json @@ -1,22 +1,11 @@ { + "extends": "@talismn/tsconfig/es6-node.json", "compilerOptions": { "lib": ["ESNext"], - "module": "ESNext", - "target": "ESNext", - "moduleResolution": "Node", - "moduleDetection": "force", - "allowImportingTsExtensions": true, "noEmit": true, - "composite": true, - "strict": true, - "downlevelIteration": true, - "skipLibCheck": true, - "jsx": "react-jsx", - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "allowJs": true, - "types": ["node"] + "allowImportingTsExtensions": true, + "types": ["node"], + "skipLibCheck": true }, "include": ["src"], "exclude": ["**/node_modules", "dist", "test", "tests"] diff --git a/apps/balances-demo/package.json b/apps/balances-demo/package.json index ec60503a2b..9a09402ff2 100644 --- a/apps/balances-demo/package.json +++ b/apps/balances-demo/package.json @@ -8,41 +8,30 @@ "dev": "vite --host localhost --port 3001", "build": "tsc && vite build", "preview": "vite preview", - "lint": "eslint . --max-warnings 0 --ext ts,tsx", - "lint:fix": "eslint . --ext ts,tsx --fix", - "clean": "rm -rf dist .turbo node_modules" + "clean": "rm -rf dist .turbo node_modules", + "typecheck": "tsc --noEmit" }, "devDependencies": { "@polkadot/api": "16.1.2", "@polkadot/extension-dapp": "0.59.2", - "@tailwindcss/forms": "^0.5.9", - "@talismn/balances": "workspace:*", "@talismn/balances-react": "workspace:*", - "@talismn/chaindata-provider": "workspace:*", - "@talismn/eslint-config": "workspace:*", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.3", "anylogger": "^1.0.11", "anylogger-loglevel": "^1.0.0", "autoprefixer": "^10.4.20", - "eslint": "^8.57.1", + "buffer": "6.0.3", "jotai": "^2.10.1", - "jotai-effect": "^1.0.3", "loglevel": "^1.9.2", "postcss": "^8.4.47", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.3.0", "rxjs": "^7.8.1", "tailwindcss": "^3.4.14", "typescript": "^5.6.3", "vite": "^5.4.10", - "react-icons": "^5.3.0" - }, - "eslintConfig": { - "root": true, - "extends": [ - "@talismn/eslint-config/react" - ] + "vite-plugin-node-polyfills": "0.24.0" } } diff --git a/apps/balances-demo/postcss.config.cjs b/apps/balances-demo/postcss.config.cjs index 2eb9662520..d8aedef63b 100644 --- a/apps/balances-demo/postcss.config.cjs +++ b/apps/balances-demo/postcss.config.cjs @@ -1,5 +1,3 @@ -/* eslint-env es2021 */ - /** @type {import('postcss-load-config').Config} */ const config = { plugins: [ diff --git a/apps/balances-demo/src/constants.ts b/apps/balances-demo/src/constants.ts deleted file mode 100644 index 13b8d80151..0000000000 --- a/apps/balances-demo/src/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const UNKNOWN_NETWORK_URL = "/assets/unknown-network.svg" -export const UNKNOWN_TOKEN_URL = "/assets/unknown-token.svg" diff --git a/apps/balances-demo/src/hooks/useSetCustomTokens.ts b/apps/balances-demo/src/hooks/useSetCustomTokens.ts deleted file mode 100644 index 3dbf4f0a5d..0000000000 --- a/apps/balances-demo/src/hooks/useSetCustomTokens.ts +++ /dev/null @@ -1,54 +0,0 @@ -export type CustomTokensConfig = CustomTokenConfig[] -export type CustomTokenConfig = { - evmChainId: string - contractAddress: `0x${string}` - symbol: string - decimals: number - coingeckoId?: string -} - -/** - * For app.talisman.xyz, we typically sync the custom tokens list with the user's wallet config. - * - * For other dapps which use `@talismn/balances-react`, we might want to specify a custom list of tokens - * to be fetched. - * - * This hook is an example of how to do just that. - * - * @example - * // tell `@talismn/balances-react` that we want to fetch some - * // more erc20 tokens than just the defaults from chaindata - * useSetCustomTokens([{ - * evmChainId: "11155111", - * contractAddress: "0x56BCB4864B12aB96efFc21fDd59Ea66DB2811c55", - * symbol: "TALI", - * decimals: 18, - * }]) - */ -export const useSetCustomTokens = (_customTokensConfig: CustomTokensConfig) => { - // TODO This needs to be reimplemented so custom tokens are provided in the ChaindataProvider constructor - // // const chaindataProvider = useChaindataProvider() - // // const customTokensConfigMemoised = useMemo( - // // () => customTokensConfig, - // // [JSON.stringify(customTokensConfig)], // eslint-disable-line react-hooks/exhaustive-deps - // // ) - // // const evmNetworks = useEvmNetworks() - // // useEffect(() => { - // // const customTokens = customTokensConfigMemoised.map( - // // ({ evmChainId, symbol, decimals, contractAddress, coingeckoId }): EvmErc20Token => ({ - // // id: evmErc20TokenId(evmChainId, contractAddress), - // // type: "evm-erc20", - // // platform: "ethereum", - // // symbol, - // // name: symbol, - // // decimals, - // // logo: UNKNOWN_TOKEN_URL, - // // coingeckoId, - // // contractAddress, - // // networkId: evmChainId, - // // isCustom: true, - // // }), - // // ) - // // chaindataProvider.setCustomTokens(customTokens) - // // }, [chaindataProvider, customTokensConfigMemoised, evmNetworks]) -} diff --git a/apps/balances-demo/tailwind.config.cjs b/apps/balances-demo/tailwind.config.cjs index 15ccb6ab30..d063981026 100644 --- a/apps/balances-demo/tailwind.config.cjs +++ b/apps/balances-demo/tailwind.config.cjs @@ -1,4 +1,3 @@ -/* eslint-env es2021 */ const TALISMAN_TAILWIND_CONFIG = require("talisman-ui/tailwind.config.cjs") /** @type {import('tailwindcss').Config} */ diff --git a/apps/balances-demo/tsconfig.json b/apps/balances-demo/tsconfig.json index 3d0a51a86e..d3089df52b 100644 --- a/apps/balances-demo/tsconfig.json +++ b/apps/balances-demo/tsconfig.json @@ -1,20 +1,7 @@ { + "extends": "@talismn/tsconfig/es6-react.json", "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ESNext"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "Node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx" + "noEmit": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/apps/balances-demo/vite.config.ts b/apps/balances-demo/vite.config.ts index 0f9ffce8d2..700e1783b4 100644 --- a/apps/balances-demo/vite.config.ts +++ b/apps/balances-demo/vite.config.ts @@ -1,7 +1,7 @@ -import dns from "dns" - import react from "@vitejs/plugin-react" +import dns from "dns" import { defineConfig } from "vite" +import { nodePolyfills } from "vite-plugin-node-polyfills" import svgr from "vite-plugin-svgr" // without this dns trick, link provided in terminal will be http://127.0.0.1:3000 @@ -13,7 +13,14 @@ export default defineConfig({ server: { host: "localhost", }, - plugins: [react(), svgr()], + plugins: [ + react(), + svgr(), + nodePolyfills({ + include: ["buffer"], + globals: { Buffer: true }, + }), + ], esbuild: { logOverride: { // spams warnings because ui library doesn't define import.meta diff --git a/apps/extension/.gitignore b/apps/extension/.gitignore new file mode 100644 index 0000000000..18df38c7fc --- /dev/null +++ b/apps/extension/.gitignore @@ -0,0 +1,2 @@ +# Bundle analysis output +stats.html diff --git a/apps/extension/.swcrc b/apps/extension/.swcrc deleted file mode 100644 index 42cb8ef599..0000000000 --- a/apps/extension/.swcrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "jsc": { - "parser": { - "syntax": "typescript", - "tsx": true, - "decorators": true, - "dynamicImport": false - } - } -} diff --git a/apps/extension/README.md b/apps/extension/README.md new file mode 100644 index 0000000000..61b56ec71c --- /dev/null +++ b/apps/extension/README.md @@ -0,0 +1,212 @@ +# Talisman Wallet Browser Extension + +The non-custodial Talisman Wallet browser extension for Chrome and Firefox. + +## Build System + +This extension uses [WXT](https://wxt.dev/) (built on Vite) for development and production builds. WXT provides: + +- ⚡ **Fast rebuilds** (~10s) with hot module replacement +- 📦 **Optimized production builds** (~30MB vs ~300MB with webpack) +- 🔄 **Automatic browser reload** when code changes +- 🎯 **Manifest V3** support for Chrome and Firefox + +## Development + +### Quick Start + +From the monorepo root: + +```bash +# Install dependencies +pnpm install + +# Start dev server (Chrome) +pnpm dev:extension +``` + +Then load the extension in Chrome: + +1. Navigate to `chrome://extensions` +2. Enable "Developer mode" +3. Click "Load unpacked" +4. Select `apps/extension/dist/chrome-mv3-dev` + +### Development Commands + +```bash +# Chrome development (with HMR) +pnpm wxt:dev + +# Firefox development +pnpm wxt:dev:firefox +``` + +### How Dev Mode Works + +In development mode: + +- Workspace packages (`@talismn/*`, `extension-core`, etc.) are aliased to their **source directories** +- Changes to package source files trigger immediate rebuilds without needing to rebuild packages +- The Vite dev server provides hot module replacement for React components + +### Persistent Browser Profile + +Your extension data (accounts, settings) persists between dev sessions: + +| Browser | How It Works | +| ------- | ----------------------------------------------------------------------------- | +| Chrome | Profile stored in `~/.talisman-dev/chrome-data` (outside repo for security) | +| Firefox | Uses Firefox's native profile storage, identified by extension ID in manifest | + +The Chrome profile is stored **outside the repository** for security, since it may contain real wallet data. This location: + +- Is not synced to cloud drives (iCloud, Dropbox) if your repo is in a synced folder +- Is not accessible to npm packages that might scan the repo +- Survives `pnpm clean` and repo deletion +- Is shared across all Talisman repo clones on your machine + +## Production Builds + +### Build Commands + +All build commands produce both an unpacked extension directory and a distributable zip file. + +```bash +# Build for Chrome (local testing) +pnpm build + +# Build for Firefox (local testing) +pnpm build:firefox + +# Production builds (Chrome Web Store / Firefox Add-ons) +# Enables Sentry sourcemap upload +pnpm build:prod +pnpm build:prod:firefox # Runs via Docker for reproducibility + +# Canary builds (internal testing) +pnpm build:canary +pnpm build:canary:firefox +``` + +#### Firefox Production Builds + +Firefox production builds use a **two-pass Docker build** to ensure reproducibility. See the [root README](../../README.md#firefox-production-builds) and [FIREFOX_SOURCE_CODE_REVIEW.md](../../FIREFOX_SOURCE_CODE_REVIEW.md) for details. + +#### Environment Variables for Production Builds + +| Variable | Required | Description | +| ------------------- | -------- | ----------------------------------------------- | +| `SENTRY_AUTH_TOKEN` | Yes | Sentry authentication token | +| `SENTRY_ORG` | Yes | Sentry organization slug | +| `BUILD_TYPE` | Auto | Set by build scripts (`production` or `canary`) | + +#### Sourcemap Handling + +- **Production/Canary builds**: Generate hidden sourcemaps (no inline reference in JS) +- **Sentry upload**: Sourcemaps are uploaded to Sentry for error tracking +- **Cleanup**: Sourcemaps are automatically deleted before zipping to keep them out of the final distribution + +### Output Directories + +| Command | Unpacked Directory | Zip File | +| ------------------- | ---------------------- | -------------------------------------------- | +| `dev` | `dist/chrome-mv3-dev` | - | +| `dev:firefox` | `dist/firefox-mv2-dev` | - | +| `build` / `build:*` | `dist/chrome-mv3` | `dist/talisman-wallet-{version}-chrome.zip` | +| `build:*:firefox` | `dist/firefox-mv3` | `dist/talisman-wallet-{version}-firefox.zip` | + +### Build Variants + +| Build Type | Name Suffix | Version Name Example | Sentry Upload | +| ----------- | ----------- | ---------------------- | ------------- | +| Production | (none) | `3.1.16` | ✅ | +| Canary | ` - Canary` | `3.1.16 - abc1234` | ✅ | +| Dev Server | ` - Dev` | `3.1.16 - abc1234 dev` | ❌ | +| Local Build | (none) | `3.1.16 - abc1234 dev` | ❌ | + +### How Production Builds Work + +In production mode: + +- Workspace packages resolve to their **pre-built `dist/` directories** (via tsup) +- Vite/Rollup performs full tree-shaking and minification +- Result is ~10x smaller than development builds + +> **Note:** Use the root-level build commands (e.g., `pnpm build:extension`) which automatically build packages first. Running `pnpm build` directly in `apps/extension` requires packages to be pre-built. + +## Project Structure + +``` +apps/extension/ +├── entrypoints/ # WXT entrypoints (background, content, popup, etc.) +│ ├── background.ts # Service worker entry +│ ├── content.ts # Content script entry +│ ├── page.ts # Injected page script entry +│ ├── popup.html # Popup UI +│ ├── dashboard.html # Full-page dashboard +│ ├── onboarding.html # Onboarding flow +│ └── support.html # Support page +├── public/ # Static assets (icons, fonts, etc.) +├── src/ # Application source code +│ ├── @talisman/ # Talisman-specific utilities +│ ├── common/ # Shared utilities +│ ├── inject/ # Page injection scripts +│ └── ui/ # React UI components +├── wxt.config.ts # WXT/Vite configuration +└── dist/ # Build outputs (gitignored) +``` + +## Configuration + +### wxt.config.ts + +The main configuration file controls: + +- **Manifest generation** - Extension metadata, permissions, icons +- **Vite plugins** - React, SVG-to-component, markdown handling +- **Path aliases** - Conditional dev/prod resolution for workspace packages +- **Build options** - Target browsers, chunk splitting, optimizations + +### Environment-Specific Behavior + +| Feature | Development | Production/Canary | +| ------------------ | --------------- | ----------------------------------------- | +| Package resolution | Source (`src/`) | Built (`dist/`) | +| Icon suffix | `-dev` | `-prod` | +| Minification | Disabled | Enabled | +| Source maps | Inline | Hidden (uploaded to Sentry, then deleted) | +| Sentry upload | No | Yes | + +## Testing + +```bash +# Run unit tests +pnpm test + +# Run E2E tests (Playwright) +pnpm test:e2e +``` + +## Troubleshooting + +### Extension not loading in browser + +1. Ensure you've run `pnpm install` from the monorepo root +2. Check that the dev server is running (`pnpm dev`) +3. Verify the correct output directory is loaded (`dist/chrome-mv3-dev`) + +### Changes not reflecting + +1. Check the terminal for build errors +2. Try reloading the extension in `chrome://extensions` +3. For background script changes, click the "service worker" link to inspect/reload + +### Build failures + +1. Clean temp folders: `pnpm clean` +2. Reinstall dependencies: `pnpm install` + +## License + +GPL-3.0-or-later - See [LICENSE](./LICENSE) diff --git a/apps/extension/babel.config.js b/apps/extension/babel.config.js deleted file mode 100644 index 5da057532e..0000000000 --- a/apps/extension/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: ["@talismn"], -} diff --git a/apps/extension/entrypoints/background.ts b/apps/extension/entrypoints/background.ts new file mode 100644 index 0000000000..cfc24b5f95 --- /dev/null +++ b/apps/extension/entrypoints/background.ts @@ -0,0 +1,22 @@ +// WXT Background Script Entry Point +// Imports the main background logic from extension-core + +import { defineBackground } from "wxt/utils/define-background" + +// Import the background module - this is a side-effect module that initializes the extension +// The import must happen at the top level, not inside defineBackground callback +import "extension-core/background" +import { log } from "extension-shared" + +export default defineBackground({ + // Use ES modules for the service worker - this allows code splitting/chunking + // Required to keep individual files under the 4MB store limit + // Supported in Chrome 121+, Firefox 128+ + type: "module", + + main() { + // Background initialization is handled by extension-core/background import above + // This callback runs when the service worker starts + log.log("[Talisman] Background script initialized") + }, +}) diff --git a/apps/extension/entrypoints/content.ts b/apps/extension/entrypoints/content.ts new file mode 100644 index 0000000000..81daee5cfb --- /dev/null +++ b/apps/extension/entrypoints/content.ts @@ -0,0 +1,73 @@ +// WXT Content Script Entry Point +// Handles communication between injected page script and extension background + +import type { Message } from "@polkadot/extension-base/types" +import { PORT_CONTENT } from "extension-shared" +import { browser } from "wxt/browser" +import { defineContentScript } from "wxt/utils/define-content-script" + +// file:// matching is not supported by WXT's dev mode hot reload (MatchPattern class) +// This causes harmless errors in dev console, so we exclude file:// in dev mode +// In production, file:// pages are fully supported +const matches = + import.meta.env.MODE === "development" + ? ["http://*/*", "https://*/*"] + : ["file://*/*", "http://*/*", "https://*/*"] + +export default defineContentScript({ + matches, + runAt: "document_start", + allFrames: true, + + main() { + class PortManager { + port: chrome.runtime.Port | undefined = undefined + + constructor() { + this.handleResponse = this.handleResponse.bind(this) + this.createPort = this.createPort.bind(this) + + // all messages from the page, pass them to the extension + window.addEventListener("message", ({ data, source }: Message): void => { + // listener will also fire on messages from extension to the page + // only allow messages from our window, by the inject + if (source !== window || data.origin !== "talisman-page") { + return + } + + if (!this.port) { + this.createPort() + } + this.port?.postMessage(data) + }) + } + + createPort() { + this.port = chrome.runtime.connect({ name: PORT_CONTENT }) + this.port.onMessage.addListener(this.handleResponse) + const handleDisconnect = () => { + this.port?.onMessage.removeListener(this.handleResponse) + this.port?.onDisconnect.removeListener(handleDisconnect) + this.port = undefined + } + this.port.onDisconnect.addListener(handleDisconnect) + } + + // biome-ignore lint/suspicious/noExplicitAny: message data from port can be any shape + handleResponse = (data: any) => { + window.postMessage({ ...data, origin: "talisman-content" }, window.location.toString()) + } + } + + new PortManager() + + // inject script that will run in page context + const script = document.createElement("script") + script.src = browser.runtime.getURL("/page.js") + + // inject before head element so that it executes before everything else + const parent = document?.head || document?.documentElement + parent?.insertBefore(script, parent.children[0]) + parent?.removeChild(script) + }, +}) diff --git a/apps/extension/src/template.dashboard.html b/apps/extension/entrypoints/dashboard/index.html similarity index 68% rename from apps/extension/src/template.dashboard.html rename to apps/extension/entrypoints/dashboard/index.html index 98cdb01bc5..44ed73f005 100644 --- a/apps/extension/src/template.dashboard.html +++ b/apps/extension/entrypoints/dashboard/index.html @@ -2,13 +2,12 @@ - + - - - <%= htmlWebpackPlugin.options.title || 'Talisman'%> + + Talisman