diff --git a/.claude/skills/freighter-best-practices b/.claude/skills/freighter-best-practices new file mode 120000 index 0000000000..c81db6d3f4 --- /dev/null +++ b/.claude/skills/freighter-best-practices @@ -0,0 +1 @@ +../../../docs/skills/freighter-best-practices \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..93038ae1c0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,166 @@ +# Freighter + +> Non-custodial Stellar wallet browser extension for Chrome, Firefox, and +> Safari. Monorepo: extension, client SDK (`@stellar/freighter-api`), shared +> utilities, and Docusaurus docs site. + +## Glossary + +Domain terms you will encounter throughout this codebase: + +| Term | Meaning | +| ------------------ | ---------------------------------------------------------------------------------- | +| **Popup** | Browser extension UI — React app at `extension/src/popup/`, runs in its own page | +| **Background** | Service worker at `extension/src/background/` — holds keys, signs, manages storage | +| **Content Script** | Injected into web pages; bridges dApp `window.postMessage` to the extension | +| **dApp** | Decentralized app that connects to Freighter via `@stellar/freighter-api` | +| **XDR** | Stellar binary serialization format used for transactions and ledger entries | +| **Soroban** | Stellar smart contract platform; transactions carry `SorobanData` | +| **Blockaid** | Third-party malicious-transaction scanning service integrated into sign flows | +| **Horizon** | Stellar REST API for querying ledger data and submitting transactions | +| **Mnemonic** | BIP-39 seed phrase used to derive HD wallet private keys | +| **Redux slice** | Feature-scoped reducer + actions created with `createSlice` (Redux Toolkit) | +| **i18n key** | Translation token; all user-facing text must go through `t()` from `react-i18next` | + +## Documentation + +- [User Guides](https://docs.freighter.app/docs/guide/introduction) +- [API Playgrounds](https://docs.freighter.app/docs/playground/getAddress) +- [Extension Dev Guide](./extension/README.md) +- [E2E Testing Guide](./extension/e2e-tests/README.md) +- [Style Guide](./extension/STYLEGUIDE.MD) +- [Localization](./extension/LOCALIZATION.MD) +- [Hardware Wallet Integration](./extension/INTEGRATING_HARDWARE_WALLET.MD) +- [Soroswap Integration](./extension/INTEGRATING_SOROSWAP.MD) +- [@stellar/freighter-api SDK](./@stellar/freighter-api/README.md) +- [Getting Started](./README.md) +- [Contributing](./CONTRIBUTING.MD) + +## Quick Reference + +| Item | Value | +| ----------------- | ------------------------------------------------------------ | +| Language | TypeScript, React | +| Node | >= 22 (`.nvmrc`) | +| Package Manager | Yarn 4.10.0 (workspaces) | +| State Management | Redux Toolkit | +| Testing | Jest (unit), Playwright (e2e) | +| Linting | ESLint flat config + Prettier | +| Default Branch | `master` | +| Branch Convention | `type/description` (`feature/`, `fix/`, `chore/`, `bugfix/`) | + +## Build & Test Commands + +```bash +yarn setup # Install + allow scripts +yarn start # Dev mode (all workspaces) +yarn start:extension # Extension only +yarn build:extension # Dev build +yarn build:extension:experimental # Experimental features enabled +yarn build:extension:production # Minified production build +yarn build:extension:translations # Auto-generate translation keys +yarn test # Jest watch mode +yarn test:ci # Jest CI mode (use before pushing) +yarn test:e2e # Playwright e2e +``` + +## Environment Setup + +Create `extension/.env`: + +```env +INDEXER_URL=https://freighter-backend-prd.stellar.org/api/v1 +INDEXER_V2_URL=https://freighter-backend-v2.stellar.org/api/v1 +``` + +## Repository Structure + +``` +freighter/ +├── extension/ +│ ├── src/popup/ # React UI +│ ├── src/background/ # Service worker (keys, signing, storage) +│ ├── src/contentScript/ # dApp ↔ extension bridge +│ ├── e2e-tests/ # Playwright tests +│ └── public/ # Static assets, manifest +├── @stellar/freighter-api/ # npm SDK for dApp integration +├── @shared/ # Shared api, constants, helpers +├── docs/ # Docusaurus site +├── config/ # Shared Jest/Babel/Webpack configs +└── .github/ + ├── workflows/ # 10 CI/CD workflows + └── agents/ # Playwright test AI agents (generator, healer, planner) +``` + +## Architecture + +Three runtime contexts communicate exclusively via message passing: + +1. **Popup** — React UI, dispatches to background via + `chrome.runtime.sendMessage` +2. **Background** — service worker, processes messages, never directly touches + the DOM +3. **Content Script** — injected per tab, relays between `window.postMessage` + and background + +Dev server URLs: + +| URL | Purpose | +| ----------------------------------- | ----------------------------------------- | +| `localhost:9000` | Extension popup (hot reload) | +| `localhost:9000/#/debug` | Blockaid debug panel (dev only) | +| `localhost:9000/#/integration-test` | Integration test helper (clears app data) | + +Background and content script changes require `yarn build:extension` + extension +reload. Popup changes hot reload automatically. + +## Security-Sensitive Areas + +Do not modify these without fully understanding the security implications: + +- `extension/src/background/` — private keys, signing, encrypted storage +- `extension/src/contentScript/` — postMessage attack surface from dApps +- `extension/public/static/manifest/` — extension permissions and CSP +- `@shared/constants/` — network endpoints, key derivation parameters +- Any code touching `chrome.storage` or key material + +## Known Complexity / Gotchas + +- **Message passing** uses typed enums. Follow existing patterns — do not add + raw string messages. +- **Build variants** (standard / experimental / production) have different + feature flags; test the right one. +- **Translations** require running `yarn build:extension:translations` after + adding any `t()` call; the pre-commit hook handles this automatically but + re-run if the hook is skipped. +- **Soroswap** swap logic lives in `popup/helpers/sorobanSwap.ts` — three + chained async calls; debug with logs before refactoring. +- **Store submissions** use separate CI workflows per platform (Chrome, Firefox, + Safari, npm). +- **AI test agents** live in `.github/agents/` — don't delete or rename without + updating the workflows. + +## Pre-submission Checklist + +```bash +yarn test:ci # All unit tests must pass +yarn build:extension # Build must succeed (catches type errors) +``` + +## Best Practices Entry Points + +Read the relevant file when working in that area: + +| Concern | Entry Point | When to Read | +| -------------------- | ------------------------------------------------------------------- | -------------------------------------------------------- | +| Code Style | `docs/skills/freighter-best-practices/references/code-style.md` | Writing or reviewing any code | +| Architecture | `docs/skills/freighter-best-practices/references/architecture.md` | Adding features, understanding context/message flow | +| Security | `docs/skills/freighter-best-practices/references/security.md` | Touching keys, messages, storage, or dApp interactions | +| Testing | `docs/skills/freighter-best-practices/references/testing.md` | Writing or fixing tests | +| Performance | `docs/skills/freighter-best-practices/references/performance.md` | Optimizing renders, bundle size, or load times | +| Error Handling | `docs/skills/freighter-best-practices/references/error-handling.md` | Adding error states, catch blocks, or user-facing errors | +| Internationalization | `docs/skills/freighter-best-practices/references/i18n.md` | Adding or modifying user-facing strings | +| Messaging | `docs/skills/freighter-best-practices/references/messaging.md` | Adding popup/background/content script communication | +| Git & PR Workflow | `docs/skills/freighter-best-practices/references/git-workflow.md` | Branching, committing, opening PRs, CI | +| Dependencies | `docs/skills/freighter-best-practices/references/dependencies.md` | Adding, updating, or auditing packages | +| Anti-Patterns | `docs/skills/freighter-best-practices/references/anti-patterns.md` | Code review, avoiding common mistakes | diff --git a/docs/skills/freighter-best-practices/SKILL.md b/docs/skills/freighter-best-practices/SKILL.md new file mode 100644 index 0000000000..9fc58888a0 --- /dev/null +++ b/docs/skills/freighter-best-practices/SKILL.md @@ -0,0 +1,41 @@ +--- +name: freighter-best-practices +description: + Comprehensive best practices for the Freighter browser extension. Covers code + style, architecture, security, testing, performance, error handling, i18n, + messaging, git workflow, dependencies, and anti-patterns. Use when writing new + features, reviewing PRs, adding message handlers, working with Redux, + modifying content scripts, handling keys, building transactions, or any + development work in the freighter extension repo. Triggers on any task + touching the freighter codebase. +--- + +# Freighter Best Practices + +Freighter is a non-custodial Stellar wallet shipped as a browser extension +(Manifest V3) for Chrome, Firefox, and Safari. The repository is a Yarn +workspaces monorepo with four main workspaces: + +1. **extension/** -- the browser extension itself (popup, background service + worker, content scripts) +2. **@stellar/freighter-api/** -- the npm SDK that dApps use to interact with + Freighter +3. **@shared/** -- shared packages (api, constants, helpers) consumed by + multiple workspaces +4. **docs/** -- documentation and skills + +## Reference Guide + +| Concern | File | When to Read | +| -------------------- | ---------------------------- | -------------------------------------------------------- | +| Code Style | references/code-style.md | Writing or reviewing any code | +| Architecture | references/architecture.md | Adding features, understanding the codebase | +| Security | references/security.md | Touching keys, messages, storage, or dApp interactions | +| Testing | references/testing.md | Writing or fixing tests | +| Performance | references/performance.md | Optimizing renders, bundle size, or load times | +| Error Handling | references/error-handling.md | Adding error states, catch blocks, or user-facing errors | +| Internationalization | references/i18n.md | Adding or modifying user-facing strings | +| Messaging | references/messaging.md | Adding popup<>background<>content script communication | +| Git & PR Workflow | references/git-workflow.md | Branching, committing, opening PRs, CI | +| Dependencies | references/dependencies.md | Adding, updating, or auditing packages | +| Anti-Patterns | references/anti-patterns.md | Code review, avoiding common mistakes | diff --git a/docs/skills/freighter-best-practices/references/anti-patterns.md b/docs/skills/freighter-best-practices/references/anti-patterns.md new file mode 100644 index 0000000000..2980eebcf5 --- /dev/null +++ b/docs/skills/freighter-best-practices/references/anti-patterns.md @@ -0,0 +1,164 @@ +# Anti-Patterns -- Freighter Extension + +Common mistakes to watch for during code review and development. + +## 1. Suppressing exhaustive-deps + +```typescript +// eslint-disable-next-line react-hooks/exhaustive-deps +``` + +Found ~50 times in the codebase. This indicates tight coupling between hooks and +external state, or an intentional override of the dependency array. + +**For new code:** fix the dependency array instead of suppressing the warning. +Refactor the hook to accept stable references, or use `useCallback`/`useMemo` to +stabilize dependencies. + +**If truly needed:** add a comment explaining WHY the suppression is necessary. +A bare `eslint-disable` without explanation is not acceptable. + +## 2. Non-null Assertions on Optional Data + +```typescript +// WRONG: crashes silently if icons[asset] is undefined +const icon = icons[asset]!; +``` + +Always handle the missing case explicitly: + +```typescript +// CORRECT +const icon = icons[asset]; +if (!icon) { + return ; +} +``` + +## 3. Type Assertion Workarounds + +```typescript +// WRONG: forced cast indicates incomplete types +const payload = action?.payload as typeof action.payload & { extra?: string }; +``` + +This pattern works around incomplete type definitions. Fix the types at the +source instead of casting at the usage site. Update the action creator's return +type, the thunk's generic parameters, or the slice's state type. + +## 4. Module-Level State in Background + +```typescript +// WRONG: this variable will be lost when the MV3 worker restarts +let cachedData = {}; + +export const handler = () => { + cachedData.foo = "bar"; // lost on next restart +}; +``` + +MV3 service workers are ephemeral. Chrome can terminate and restart them at any +time. Any variable declared outside a function will be reset. + +**Use `chrome.storage.session`** for ephemeral data or +**`chrome.storage.local`** for persistent data. + +## 5. Direct chrome.runtime.sendMessage from Popup + +```typescript +// WRONG: bypasses the shared API layer +chrome.runtime.sendMessage({ type: SERVICE_TYPES.GET_DATA }); +``` + +Always use the `sendMessageToBackground()` wrapper from `@shared/api/internal`: + +```typescript +// CORRECT +import { sendMessageToBackground } from "@shared/api/internal"; +await sendMessageToBackground({ type: SERVICE_TYPES.GET_DATA }); +``` + +The wrapper provides consistent typing, response parsing, and error handling. + +## 6. Hardcoded URL Paths in Navigation + +```typescript +// WRONG +navigate("/account/send"); +``` + +Use the `ROUTES` enum and `navigateTo()` helper: + +```typescript +// CORRECT +navigateTo(ROUTES.sendPayment); +``` + +## 7. Inline Object/Function Creation in JSX Props + +```typescript +// WRONG: new object/function on every render, defeats memo() + handleSelect(asset)} +/> +``` + +Extract to `useMemo` and `useCallback`: + +```typescript +// CORRECT +const filter = useMemo(() => ({ type: "native" }), []); +const onSelect = useCallback((asset) => handleSelect(asset), [handleSelect]); + +``` + +## 8. Non-Serializable Objects in Redux Store + +```typescript +// WRONG: Stellar SDK objects contain methods and circular references +state.transaction = new Transaction(xdr); +``` + +Store serializable representations only: + +```typescript +// CORRECT: store the XDR string, reconstruct the object when needed +state.transactionXdr = transaction.toXDR(); +``` + +## 9. Silent Error Swallowing + +```typescript +// WRONG: error is completely lost +try { + await riskyOperation(); +} catch (e) { + // do nothing +} +``` + +At minimum, report to Sentry: + +```typescript +// CORRECT +try { + await riskyOperation(); +} catch (error) { + captureException(error); +} +``` + +## 10. TODO Comments Without Tracking + +```typescript +// TODO: refactor this later +``` + +If leaving a TODO, create a GitHub issue and reference it: + +```typescript +// TODO(#1234): refactor balance calculation to use BigNumber +``` + +This ensures TODOs are tracked and not forgotten. diff --git a/docs/skills/freighter-best-practices/references/architecture.md b/docs/skills/freighter-best-practices/references/architecture.md new file mode 100644 index 0000000000..1eddf9f135 --- /dev/null +++ b/docs/skills/freighter-best-practices/references/architecture.md @@ -0,0 +1,184 @@ +# Architecture -- Freighter Extension + +## Three Runtime Contexts + +Freighter runs code in three isolated contexts, each with different lifetimes +and capabilities: + +### Popup (`extension/src/popup/`) + +- A React single-page application rendered when the user clicks the extension + icon +- **Lifetime:** created when opened, destroyed when closed -- no persistent + state in memory +- Communicates with the background via `chrome.runtime.sendMessage` (wrapped by + `sendMessageToBackground()`) +- Served from `localhost:9000` during development (hot reload) + +### Background Service Worker (`extension/src/background/`) + +- A Manifest V3 service worker that handles all privileged operations (key + management, signing, storage) +- **Lifetime:** ephemeral -- Chrome may terminate it at any time after + inactivity. All state must be persisted to `chrome.storage` +- Listens for messages from the popup and content scripts via + `chrome.runtime.onMessage` +- Uses `chrome.storage.session` for decrypted keys (memory-only, cleared on + session end) and `chrome.storage.local` for the encrypted vault + +### Content Script (`extension/src/contentScript/`) + +- Injected into every tab at `document_start` +- **Lifetime:** per tab, lives as long as the tab +- Bridges dApp requests from the page to the background: listens for + `window.postMessage` from the page, forwards valid messages via + `chrome.runtime.sendMessage` +- Filters messages by source (`EXTERNAL_MSG_REQUEST`) and only forwards valid + `EXTERNAL_SERVICE_TYPES` + +## Monorepo Workspaces + +``` +freighter/ + extension/ -- The browser extension (popup, background, content script) + @stellar/freighter-api/ -- npm SDK for dApp integration + @shared/ -- Shared packages + api/ -- API helpers (internal messaging, external endpoints) + constants/ -- Shared constants and enums + helpers/ -- Utility functions + docs/ -- Documentation and skills + config/ -- Shared build configuration +``` + +## State Management Patterns + +The codebase uses two patterns depending on the scope of the state: + +### Redux Toolkit Duck Pattern (Cross-Cutting Global State) + +For state shared across multiple components or that must persist across +navigation (account data, settings, cache): + +- **`createAsyncThunk`** for all async operations (API calls, background + messages) +- **`createSlice`** for reducers with immer-based immutable updates +- **`createSelector`** (reselect) for memoized derived state -- never compute + inline in components +- **`ActionStatus` enum** tracks async lifecycle: `IDLE`, `PENDING`, `ERROR`, + `SUCCESS` + +```typescript +// Example duck structure +export const fetchAccountBalances = createAsyncThunk( + "accountBalances/fetch", + async (publicKey: string, { rejectWithValue }) => { + try { + const balances = await getBalances(publicKey); + return balances; + } catch (error) { + if (error instanceof Error) { + return rejectWithValue({ errorMessage: error.message }); + } + } + }, +); + +const accountBalancesSlice = createSlice({ + name: "accountBalances", + initialState, + reducers: { + /* sync reducers */ + }, + extraReducers: (builder) => { + builder + .addCase(fetchAccountBalances.pending, (state) => { + state.status = ActionStatus.PENDING; + }) + .addCase(fetchAccountBalances.fulfilled, (state, action) => { + state.status = ActionStatus.SUCCESS; + state.balances = action.payload; + }) + .addCase(fetchAccountBalances.rejected, (state, action) => { + state.status = ActionStatus.ERROR; + state.error = action.payload?.errorMessage; + }); + }, +}); +``` + +### useReducer + RequestState Pattern (View-Level Local State) + +For state scoped to a single view that doesn't need to persist (data-fetching +hooks for specific screens): + +- **`useReducer`** with the shared `reducer`/`initialState` from + `helpers/request.ts` +- **`RequestState` enum** from `constants/request` tracks: `IDLE`, `LOADING`, + `SUCCESS`, `ERROR` +- Composes existing hooks (e.g., `useGetCollectibles`, `useGetBalances`) for + data fetching +- Used by 8+ views: Discover, AddCollectibles, GrantAccess, SignMessage, + RecoverAccount, etc. + +```typescript +// Example: view-level data hook +import { initialState, reducer } from "helpers/request"; +import { RequestState } from "constants/request"; + +const useGetScreenData = () => { + const [state, dispatch] = useReducer(reducer, initialState); + + useEffect(() => { + dispatch({ type: "FETCH_DATA_START" }); + fetchData() + .then((data) => dispatch({ type: "FETCH_DATA_SUCCESS", payload: data })) + .catch(() => dispatch({ type: "FETCH_DATA_ERROR" })); + }, []); + + return state; +}; +``` + +**When to use which:** If the data is needed by multiple screens or must survive +navigation, use Redux. If it's a single screen's fetch-and-display lifecycle, +use `useReducer`. + +## Background Store Hydration + +The background service worker persists the Redux store to survive ephemeral +restarts: + +1. Store is saved to `chrome.storage.session` on every change via + `store.subscribe(() => saveStore(store.getState()))` +2. On startup, the store is hydrated from `chrome.storage.session` using + `REDUX_STORE_KEY` +3. Firefox fallback: uses `chrome.storage.local` where session storage is + unavailable + +## Component Patterns + +- **Functional components only** -- no class components except `ErrorBoundary` +- **View/hook separation** -- complex components split into a view (JSX) and a + hook (logic) +- **`memo()`** for component memoization. `lodash/isEqual` is available in the + codebase for deep comparisons where needed + +## Cache Duck + +A centralized cache duck manages network data: + +- Keyed by `[network][publicKey]` for per-account, per-network isolation +- 3-minute expiration enforced by `isCacheValid()` helper +- Prevents redundant API calls when switching between accounts or networks + +## Custom Middleware + +- **`metricsMiddleware`** -- dispatches analytics events for tracked actions +- **`activePublicKeyMiddleware`** -- detects account mismatch between popup and + background state + +## Navigation + +- All routes defined in `ROUTES` enum +- Navigation uses `navigateTo()` helper function +- Never hardcode route paths as strings -- always reference the enum diff --git a/docs/skills/freighter-best-practices/references/code-style.md b/docs/skills/freighter-best-practices/references/code-style.md new file mode 100644 index 0000000000..997d58340f --- /dev/null +++ b/docs/skills/freighter-best-practices/references/code-style.md @@ -0,0 +1,485 @@ +# Code Style -- Freighter Extension + +## Prettier Configuration + +The project uses Prettier (`.prettierrc.yaml`) with the following settings: + +- **Quotes:** double quotes (`singleQuote: false`) +- **Indentation:** 2 spaces (`tabWidth: 2`) +- **Print width:** 80 characters +- **Trailing commas:** all (`trailingComma: "all"`) +- **Semicolons:** always (`semi: true`) +- **Arrow parens:** always (`arrowParens: "always"` — `(x) => x`, never + `x => x`) +- **Bracket spacing:** true (`{ foo }` not `{foo}`) +- **Prose wrap:** always + +## ESLint Configuration + +Flat config (`eslint.config.js`) with: + +- **Parser:** `@typescript-eslint/parser` +- **Plugins:** React, React Hooks, JSX A11y, Import, TypeScript ESLint +- **Key rules:** + - `semi` -- error (semicolons required) + - `react-hooks/rules-of-hooks` -- error + - `react-hooks/exhaustive-deps` -- warn + - `react/prop-types` -- off (TypeScript replaces prop-types) + - `import/no-unresolved` -- warn + +## Import Ordering + +External packages first, then internal modules. Convention only — no ESLint +`import/order` rule enforces it: + +```typescript +// 1. External packages (react, redux, i18next, design system) +import React, { useState } from "react"; +import { useDispatch } from "react-redux"; +import { useTranslation } from "react-i18next"; +import { Button } from "@stellar/design-system"; + +// 2. Internal @shared/* packages +import { sendMessageToBackground } from "@shared/api/internal"; +import { SERVICE_TYPES } from "@shared/constants/services"; + +// 3. Internal popup/* modules +import { ROUTES } from "popup/constants/routes"; +import { navigateTo } from "popup/helpers/navigate"; +``` + +Note: `@shared/*` and `@stellar/*` are INTERNAL packages despite the `@` prefix. +Group them with internal imports. + +## Enum Conventions + +### Enum Naming (Two Patterns) + +The codebase uses two naming patterns for enums. Follow the established +convention: + +| Pattern | When | Examples | +| ------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| **SCREAMING_SNAKE_CASE** | Constants, service types, routes, network enums | `SERVICE_TYPES`, `EXTERNAL_SERVICE_TYPES`, `ROUTES`, `NETWORKS`, `OPERATION_TYPES`, `STEPS` | +| **PascalCase** | Domain types, status enums, classification enums | `ActionStatus`, `WalletType`, `AccountType`, `RequestState`, `SecurityLevel`, `NetworkCongestion` | + +### Enum Values + +All enums use **string values** — no numeric enums in the codebase: + +```typescript +// SCREAMING_SNAKE enum with SCREAMING_SNAKE values +export enum SERVICE_TYPES { + GET_ACCOUNT_HISTORY = "GET_ACCOUNT_HISTORY", + LOAD_SETTINGS = "LOAD_SETTINGS", +} + +// PascalCase enum with PascalCase values +export enum ActionStatus { + IDLE = "IDLE", + PENDING = "PENDING", + SUCCESS = "SUCCESS", + ERROR = "ERROR", +} + +// PascalCase enum with display-name values +export enum WalletType { + LEDGER = "Ledger", +} + +// SCREAMING_SNAKE enum with kebab-case values (for CSS/URL identifiers) +export enum STEPS { + SELECT_ASSET = "select-asset", + SET_AMOUNT = "set-amount", +} +``` + +### const enum vs enum + +Never use `const enum` — all 37+ enums in the codebase are standard runtime +enums. + +### Prefer Enums Over Union Type Aliases + +When you have a finite set of named string values, use an enum, not a union +type: + +```typescript +// BAD — union type for a finite set +type SwapStatus = "pending" | "confirming" | "completed" | "failed"; +type InputType = "crypto" | "fiat"; +type AssetVisibility = "visible" | "hidden"; +type NotificationType = "warning" | "info"; + +// GOOD — enum +enum SwapStatus { + Pending = "pending", + Confirming = "confirming", + Completed = "completed", + Failed = "failed", +} +``` + +The codebase has ~4 remaining union types that should be enums (`InputType`, +`AssetVisibility`, `NotificationType`, `PillType`). Don't add more. + +## Type vs Interface + +Follow this pattern (observed in ~200+ type definitions): + +| Use | When | Example | +| --------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| **`interface`** | Object shapes with known properties | `interface Response { ... }`, `interface UserInfo { ... }` | +| **`type`** | Unions, intersections, aliases, mapped types | `type ExternalRequest = ExternalRequestToken \| ...`, `type MigratableAccount = Account & { keyIdIndex: number }` | + +## No Magic Numbers + +Extract all numeric literals into named SCREAMING_SNAKE_CASE constants: + +```typescript +// BAD +const fee = amount * 0.003; +if (retries > 5) { ... } +setTimeout(() => setDidSaveFail(false), 750); + +// GOOD +const FEE_RATE = 0.003; +const MAX_RETRIES = 5; +const SAVE_FEEDBACK_DELAY_MS = 750; +``` + +**Currently well-extracted:** Storage keys (34 constants in +`localStorageTypes.ts`), queue timeouts (`QUEUE_ITEM_TTL_MS`, +`CLEANUP_INTERVAL_MS`), image loader timeout (`DEFAULT_TIMEOUT_MS`). + +**Still hardcoded in codebase:** Some `setTimeout` delays (500ms debounce, 750ms +feedback) and `setInterval` values. Extract these when modifying those files. + +## No Loose Strings + +### User-Facing Strings + +All user-facing text must be wrapped in `t()` from `react-i18next`. Both English +and Portuguese translations required. + +### Error Messages + +Error messages in catch blocks are currently hardcoded strings (e.g., +`"Failed to scan site"`, `"Failed to scan transaction"`). There is no +centralized error constants file. When adding new error messages, use +descriptive string literals — but consider extracting to a constants file for +shared messages. + +### Storage Keys + +All `chrome.storage` keys must use constants from +`extension/src/constants/localStorageTypes.ts` — never hardcoded strings. + +### Action Type Strings + +The reducer action types (`"FETCH_DATA_START"`, `"FETCH_DATA_SUCCESS"`, +`"FETCH_DATA_ERROR"`) are string literals in the shared reducer. This is +acceptable for the shared `useReducer` pattern, but Redux slice action types +should always use `createSlice` (which auto-generates them). + +## Function Style + +Arrow functions dominate (~85%). Function declarations are used rarely: + +```typescript +// STANDARD — arrow function export (85% of codebase) +export const getAccountBalances = async (publicKey: string) => { ... }; +export const useNetworkFees = () => { ... }; + +// ACCEPTABLE — function declaration for complex utilities (15% of codebase) +export function cleanupQueue(responseQueue: ResponseQueue) { ... } +``` + +Both styles are acceptable. Don't refactor between them. + +## JSX Return Style + +Multi-line JSX wrapped in parentheses. Single-expression returns may omit +parens: + +```typescript +// Single expression — implicit return OK +const Loading = () =>
; + +// Multi-line — always use parentheses +const AccountHeader = () => { + return ( +
+

{t("Account")}

+ +
+ ); +}; +``` + +## Boolean Naming + +Use `is`, `has`, or `should` prefixes consistently: + +```typescript +// CORRECT +const isConnected = true; +const hasInitialized = false; +const shouldShowWarning = true; +const isMainnet = network === NETWORKS.PUBLIC; +const isHttpsDomain = url.startsWith("https"); +const hasValidMemo = memo.length > 0; + +// WRONG — missing prefix +const connected = true; // Should be isConnected +const initialized = false; // Should be hasInitialized +``` + +## Event Handler Naming + +Props use `on` prefix, internal handlers use `handle` prefix: + +```typescript +// Props: on + Event +interface Props { + onClick: () => void; + onSelect: (asset: Asset) => void; + onClose: () => void; +} + +// Internal handlers: handle + Action +const handleClick = () => { ... }; +const handleApprove = () => { ... }; +const handleTokenLookup = () => { ... }; +``` + +## Naming Conventions Summary + +| Category | Convention | Example | +| -------------------------- | -------------------------------- | ---------------------------------------- | +| Components | PascalCase directory + index.tsx | `AccountHeader/index.tsx` | +| Functions / variables | camelCase | `getAccountBalances`, `isConnected` | +| Constants | SCREAMING_SNAKE_CASE | `NETWORKS`, `QUEUE_ITEM_TTL_MS` | +| File names (helpers/hooks) | camelCase | `useNetworkFees.ts`, `getSiteFavicon.ts` | +| Enums (constants) | SCREAMING_SNAKE_CASE | `SERVICE_TYPES`, `ROUTES` | +| Enums (domain types) | PascalCase | `ActionStatus`, `SecurityLevel` | +| Booleans | `is/has/should` prefix | `isMainnet`, `hasInitialized` | +| Event props | `on` prefix | `onClick`, `onSelect` | +| Event handlers | `handle` prefix | `handleClick`, `handleApprove` | + +## Pre-commit Hooks + +Runs via Husky (`.husky/pre-commit`): + +1. **Translation build** -- `addTranslations.sh` regenerates locale files and + stages them +2. **pretty-quick --staged** -- formats staged files with Prettier + +Note: `lint-staged` config exists in `package.json` but is not invoked by the +current hook. + +## Destructuring Patterns + +Inline parameter destructuring is the dominant pattern (75% of components): + +```typescript +// PREFERRED — inline destructuring (75%) +const AccountCard = ({ balance, network, onSelect }: AccountCardProps) => { ... }; + +// ACCEPTABLE — separate destructuring (25%) +const AccountCard = (props: AccountCardProps) => { + const { balance, network } = props; +}; +``` + +For selectors, destructure the result: + +```typescript +// PREFERRED +const { balances, status } = useSelector(accountBalancesSelector); + +// LESS COMMON +const balances = useSelector((state) => state.accountBalances.balances); +``` + +Renaming during destructuring (~12% of destructuring operations): + +```typescript +const { data: balanceData, error: fetchError } = response; +``` + +## Optional Chaining and Nullish Coalescing + +The codebase uses `&&` (659 occurrences) more than `?.` (405 occurrences). Both +are acceptable: + +```typescript +// Both patterns used — pick whichever reads better +const name = account?.name; // optional chaining (405 occurrences) +const name = account && account.name; // logical AND (659 occurrences) +``` + +`??` (nullish coalescing) is underutilized (41 occurrences vs `||`). Prefer `??` +for new code when you need to preserve `0` or `""`: + +```typescript +// PREFERRED for new code +const timeout = config.timeout ?? DEFAULT_TIMEOUT; + +// LEGACY — still common but treats 0 and "" as falsy +const timeout = config.timeout || DEFAULT_TIMEOUT; +``` + +## Export Patterns + +Named exports dominate (95-97%). Default exports are rare (~17 occurrences): + +```typescript +// STANDARD — named export (97%) +export const AccountHeader = () => { ... }; +export const getBalance = async () => { ... }; + +// RARE — default export (3%, mostly layout components) +export default View; +``` + +Barrel files (index.ts re-exports) exist but are not heavily used (~15-20 +instances). Most imports go directly to source files. + +## Component Prop Types + +Separate Props interface is preferred (60%) over inline (40%): + +```typescript +// PREFERRED — separate interface (60%) +interface AccountCardProps { + balance: Balance; + network: string; + onSelect: (account: Account) => void; +} +const AccountCard = ({ balance, network, onSelect }: AccountCardProps) => { ... }; + +// ACCEPTABLE — inline for simple props (40%) +const Loading = ({ size }: { size: number }) => { ... }; +``` + +Do NOT use `React.FC` — only 17 occurrences remain (legacy). Use plain +typed params: + +```typescript +// WRONG — legacy pattern +const MyComponent: React.FC = ({ title }) => { ... }; + +// CORRECT — modern pattern +const MyComponent = ({ title }: Props) => { ... }; +``` + +## Conditional Rendering + +`&&` pattern dominates for conditional rendering (169 files): + +```typescript +// MOST COMMON — logical AND +{isLoading && } +{error && } + +// USED FOR EITHER/OR — ternary +{isLoading ? : } +``` + +Early returns for loading/error states (~15-20 files): + +```typescript +if (!data) return ; +if (error) return ; +return ; +``` + +## Array/Object Operations + +- `.map()` preferred over `.forEach()` (1.3:1 ratio, 162 vs 124 occurrences) +- Spread `{ ...obj }` overwhelmingly preferred over `Object.assign` (554 + occurrences vs <2%) +- `.reduce()` is rare (<10 files) — prefer `.map()` + `.filter()` chains +- Template literals are near-universal (99%+) — never use string concatenation + +## Async Patterns + +`async/await` is the universal pattern (>97%). `.then().catch()` is legacy +(<3%): + +```typescript +// STANDARD +const data = await fetchBalance(publicKey); + +// LEGACY — avoid for new code +fetchBalance(publicKey).then(data => { ... }).catch(e => { ... }); +``` + +`try/catch` wraps whole functions (60%) or specific operations (40%). +`Promise.allSettled` preferred over `Promise.all` for robustness. + +## Type Assertions and Generics + +Always use `as Type` syntax (TSX files forbid angle-bracket `` syntax): + +```typescript +const payload = action.payload as ErrorMessage; // CORRECT +const payload = action.payload; // WRONG in TSX +``` + +Prefer descriptive generic names over single letters: + +```typescript +// PREFERRED (50+ files) +function fetchData(url: string): Promise { ... } + +// ACCEPTABLE for simple generics (15 files) +function identity(value: T): T { ... } +``` + +Common utility types in use: `Pick<>` (30 files), `Omit<>` (40 files), +`Partial<>` (30 files), `Record<>` (15 files), `keyof`/`typeof` (167 occurrences +in 71 files). + +## TypeScript Strictness + +- `any` exists in ~330 occurrences (mostly test files). Avoid in production + code. Use `unknown` instead. +- `unknown` has low adoption (41 occurrences) — prefer it over `any` for new + code +- `as unknown as Type` double-cast: only 13 occurrences (acceptable in tests + only) + +## CSS/SCSS Patterns + +110 SCSS files using BEM naming with kebab-case: + +```scss +.AccountDetail { + &__wrapper { ... } + &__balance-info { ... } + &__action-button { ... } +} +``` + +Design system tokens: use `var(--sds-clr-*)` CSS variables and `pxToRem()` mixin +from `styles/utils.scss`. Never use raw pixel values or hardcoded colors. + +## Test File Patterns + +- File naming: always `.test.ts` / `.test.tsx` (never `.spec.ts`) +- Structure: `describe("feature")` + `it("should...")` (universal — 922 test + cases across 95 files) +- Setup: `beforeEach` in 88 of 95 test files (93%) +- Assertions: `expect().toBe()` for primitives, `expect().toEqual()` for objects + +## Comments + +Comments are intentionally sparse (~2-5 per 500 lines). Don't add unnecessary +comments. + +- **JSDoc:** Only on exported types/interfaces in shared packages +- **Inline comments:** Only for non-obvious logic +- **TODOs:** Must reference a GitHub issue (`// TODO(#1234): description`) +- **No license headers** — files begin directly with imports diff --git a/docs/skills/freighter-best-practices/references/dependencies.md b/docs/skills/freighter-best-practices/references/dependencies.md new file mode 100644 index 0000000000..c134c3b88e --- /dev/null +++ b/docs/skills/freighter-best-practices/references/dependencies.md @@ -0,0 +1,116 @@ +# Dependencies -- Freighter Extension + +## Package Manager + +- **Yarn 4.10.0** with workspaces enabled +- Node requirement: **>= 22** (specified in `.nvmrc`) +- Always use `nvm use` or equivalent before running commands to ensure the + correct Node version + +## Workspace Structure + +The root `package.json` defines 6 workspaces: + +```json +{ + "workspaces": [ + "extension", + "@stellar/freighter-api", + "@shared/api", + "@shared/constants", + "@shared/helpers", + "docs" + ] +} +``` + +## Dependency Placement + +| Dependency Type | Where to Add | Example | +| -------------------------- | ------------------------------------- | ----------------------------------------- | +| Extension runtime deps | `extension/package.json` | `react`, `redux`, `stellar-sdk` | +| freighter-api runtime deps | `@stellar/freighter-api/package.json` | Minimal, SDK-only deps | +| Shared module deps | `@shared/*/package.json` | Shared utilities | +| Dev tooling (shared) | Root `package.json` | `typescript`, `eslint`, `webpack`, `jest` | +| Docs | `docs/package.json` | Documentation tooling | + +## @shared Modules + +Shared packages are imported as workspace dependencies. Yarn resolves them via +the workspaces configuration: + +```json +// extension/package.json +{ + "dependencies": { + "@shared/api": "1.0.0", + "@shared/constants": "1.0.0", + "@shared/helpers": "1.0.0" + } +} +``` + +In code, import directly: + +```typescript +import { sendMessageToBackground } from "@shared/api/internal"; +import { SERVICE_TYPES } from "@shared/constants/services"; +``` + +## Adding Dependencies + +1. Identify the correct workspace (see table above) +2. Add from the workspace directory: + +```bash +cd extension && yarn add some-package +# or from root: +yarn workspace extension add some-package +``` + +3. For dev dependencies shared across workspaces, add to root: + +```bash +yarn add -D some-dev-tool +``` + +## Lavamoat + +Lavamoat provides a script allowlist in `package.json` for supply-chain +security. It restricts which packages can run install scripts (postinstall, +preinstall, etc.). When adding a package that requires install scripts, you may +need to update the Lavamoat configuration. + +## npmMinimalAgeGate + +The `.yarnrc.yml` file includes: + +```yaml +npmMinimalAgeGate: 7d +``` + +This means newly published packages must be at least 7 days old before Yarn will +allow installation. This protects against supply-chain attacks using freshly +published malicious packages. If you need to install a package published less +than 7 days ago, you will need to wait or temporarily override this setting +(with team approval). + +## Upgrading Dependencies + +1. Run `yarn install` to resolve and update the lockfile +2. Verify workspace symlinks are intact (`@shared/*` packages resolve correctly) +3. Build all workspaces -- some depend on others: + +```bash +yarn build # builds all workspaces in dependency order +``` + +4. Run the full test suite to verify nothing broke: + +```bash +yarn test:ci +yarn test:e2e +``` + +5. Check for breaking changes in changelogs, especially for `stellar-sdk`, + `react`, and `webpack` diff --git a/docs/skills/freighter-best-practices/references/error-handling.md b/docs/skills/freighter-best-practices/references/error-handling.md new file mode 100644 index 0000000000..dafffa537b --- /dev/null +++ b/docs/skills/freighter-best-practices/references/error-handling.md @@ -0,0 +1,100 @@ +# Error Handling -- Freighter Extension + +## Async Thunks + +All async thunks must catch errors and use `rejectWithValue` to propagate them +through Redux: + +```typescript +export const submitTransaction = createAsyncThunk( + "transactionSubmission/submit", + async (xdr: string, { rejectWithValue }) => { + try { + const result = await sendTransaction(xdr); + return result; + } catch (error) { + if (error instanceof Error) { + return rejectWithValue({ errorMessage: error.message }); + } + return rejectWithValue({ errorMessage: "Unknown error" }); + } + }, +); +``` + +Handle rejected cases in extra reducers: + +```typescript +builder.addCase(submitTransaction.rejected, (state, action) => { + state.status = ActionStatus.ERROR; + state.error = (action.payload as ErrorMessage)?.errorMessage; +}); +``` + +## Background Handlers + +Background message handlers must never throw. Always return structured response +objects: + +```typescript +// CORRECT: return result or error objects +export const handleGetBalance = async (publicKey: string) => { + try { + const balance = await fetchBalance(publicKey); + return { result: balance }; + } catch (error) { + captureException(error); + return { error: "Failed to fetch balance" }; + } +}; + +// WRONG: throwing from a handler +export const handleGetBalance = async (publicKey: string) => { + const balance = await fetchBalance(publicKey); // throws on failure + return balance; +}; +``` + +Use `captureException()` from Sentry for unexpected errors that should be +tracked. + +## ErrorBoundary + +The popup wraps its component tree in a class-based `ErrorBoundary` at +`popup/components/ErrorBoundary/`: + +- Implements `getDerivedStateFromError` to catch render errors +- Implements `componentDidCatch` to report errors to Sentry +- Displays a fallback UI when a render crash occurs +- This is the only class component in the codebase -- React does not support + error boundaries as functional components + +## Network Errors + +Horizon transaction submission can return specific error codes that need +user-friendly mapping: + +| Horizon Error Code | Meaning | +| --------------------- | ------------------------------------------ | +| `op_underfunded` | Insufficient balance for the operation | +| `tx_insufficient_fee` | Fee too low for current network conditions | +| `op_no_destination` | Destination account does not exist | + +These are parsed by the `getResultCodes()` helper and mapped to translated +user-facing messages. + +## Error Type Hierarchy + +- **`FreighterApiDeclinedError`** -- thrown when the user declines a dApp + request (signing, access). This is an expected error, not a bug. +- **`ErrorMessage` type** -- `{ errorMessage: string }` used as the payload for + thunk rejections via `rejectWithValue`. + +## Sentry Integration + +- `captureException()` for unexpected errors in handlers and async operations +- Error boundaries catch and report render crashes automatically +- Do not call `captureException()` for expected errors (user cancellation, + invalid input) +- Never swallow errors silently -- at minimum, report to Sentry if the error is + unexpected diff --git a/docs/skills/freighter-best-practices/references/git-workflow.md b/docs/skills/freighter-best-practices/references/git-workflow.md new file mode 100644 index 0000000000..1b14a9cd03 --- /dev/null +++ b/docs/skills/freighter-best-practices/references/git-workflow.md @@ -0,0 +1,76 @@ +# Git & PR Workflow -- Freighter Extension + +## Main Branch + +The primary branch is `master`. All feature work branches from and merges back +to `master`. + +## Branch Naming + +Use descriptive prefixes: + +| Prefix | Use Case | Example | +| ---------- | -------------------------- | ------------------------------ | +| `feature/` | New functionality | `feature/token-swap-ui` | +| `fix/` | Bug fixes | `fix/balance-display-rounding` | +| `chore/` | Maintenance, config, deps | `chore/upgrade-webpack` | +| `bugfix/` | Alternative bug fix prefix | `bugfix/content-script-race` | + +## Commit Messages + +- Start with an action verb in present tense: **Add**, **Fix**, **Update**, + **Improve**, **Remove**, **Refactor** +- Keep the subject line concise (under 72 characters) +- PR number is auto-added by GitHub on squash merge + +``` +Add token swap confirmation dialog +Fix balance rounding for very small amounts +Update webpack to v5.90 +Refactor background message handler registration +``` + +## Pull Request Expectations + +- **Focused scope** -- one concern per PR. Split large changes into stacked PRs. +- **Screenshots** for any UI changes (before/after) +- **Self-review** -- review your own diff before requesting reviews +- **Tests** for new features and bug fixes +- **Cross-browser testing** -- verify in both Chrome and Firefox +- **Translations updated** -- run `yarn build:extension:translations` and commit + locale changes + +## CI on Pull Requests + +Two workflows run automatically on every PR: + +1. **`runTests.yml`** -- runs Jest unit tests and Playwright e2e tests +2. **`codeql.yml`** -- runs CodeQL security analysis + +Both must pass before merging. + +## Release Process + +Releases are managed by the `newRelease.yml` GitHub Actions workflow: + +1. Manual dispatch with `appVersion` parameter (e.g., `5.12.0`) +2. Workflow creates a release branch and version tag +3. Bumps `package.json` version and `manifest.json` version +4. Creates a GitHub release with changelog + +## Release Branches + +| Branch | Purpose | +| ------------------- | ------------------------------------------------------------ | +| `release` | Auto-created by the release workflow for the current release | +| `emergency-release` | Hotfix branch for critical production issues | +| `v{X.Y.Z}` | Version tag branches for historical reference | + +## Submission Workflows + +Separate workflows handle submission to each distribution channel: + +- **Chrome Web Store** -- uploads the extension to the Chrome Web Store +- **Firefox AMO** -- submits to Firefox Add-ons +- **Safari** -- builds and submits the Safari version +- **npm** -- publishes `@stellar/freighter-api` to the npm registry diff --git a/docs/skills/freighter-best-practices/references/i18n.md b/docs/skills/freighter-best-practices/references/i18n.md new file mode 100644 index 0000000000..d6eb569dd4 --- /dev/null +++ b/docs/skills/freighter-best-practices/references/i18n.md @@ -0,0 +1,85 @@ +# Internationalization -- Freighter Extension + +## Framework + +Freighter uses `react-i18next` for internationalization: + +- Auto-detects the browser's language setting +- Falls back to English when a translation is not available +- String keys use the English text directly, making them greppable in the + codebase + +## Locale Files + +Translation JSON files live in `extension/src/popup/locales/` with one file per +language: + +``` +extension/src/popup/locales/ + en/ + translation.json + pt/ + translation.json +``` + +## Adding New Strings + +1. Wrap the string in `t()` from the `useTranslation` hook: + +```typescript +import { useTranslation } from "react-i18next"; + +const MyComponent = () => { + const { t } = useTranslation(); + return

{t("Send Payment")}

; +}; +``` + +2. Add the string to ALL language JSON files (currently English and Portuguese) + +3. Run the translation build to verify: + +```bash +yarn build:extension:translations +``` + +## Auto-Generation + +The command `yarn build:extension:translations` scans the codebase for `t()` +calls and generates missing keys in locale files. Missing translations default +to the English text. + +## Pre-commit Hook + +The translation build runs automatically as a pre-commit hook: + +1. Scans for new or modified `t()` calls +2. Regenerates locale JSON files +3. Stages the updated locale files for the commit + +This ensures translations are never out of sync with the code. + +## Testing Translations + +To verify translations locally: + +1. Change the browser's language setting (e.g., to Portuguese) +2. Clear `react-i18next` localStorage cache +3. Refresh the extension popup +4. Verify all strings display in the target language + +## Rules + +- Every user-facing string must be wrapped in `t()` +- Every string must have both English (`en`) and Portuguese (`pt`) translations +- The custom lint rule `i18n-pt-en` enforces that both translations exist +- Never use template literals or string concatenation for user-facing text -- + use `t()` with interpolation instead: + +```typescript +// CORRECT +t("Send {{amount}} XLM", { amount: "100" }); + +// WRONG +`Send ${amount} XLM`; +``` diff --git a/docs/skills/freighter-best-practices/references/messaging.md b/docs/skills/freighter-best-practices/references/messaging.md new file mode 100644 index 0000000000..f742de1697 --- /dev/null +++ b/docs/skills/freighter-best-practices/references/messaging.md @@ -0,0 +1,169 @@ +# Messaging -- Freighter Extension + +## Message Flow Architecture + +``` +dApp (web page) + | + | window.postMessage (EXTERNAL_MSG_REQUEST) + v +@stellar/freighter-api (injected SDK) + | + | window.postMessage + v +Content Script (extension/src/contentScript/) + | + | chrome.runtime.sendMessage (EXTERNAL_SERVICE_TYPES) + v +Background Service Worker (extension/src/background/) + ^ | + | | Opens approval window (if needed) + | v + | Approval Popup + | | + | chrome.runtime.sendMessage (response) + | + v +Popup (extension/src/popup/) + | + | sendMessageToBackground() --> chrome.runtime.sendMessage (SERVICE_TYPES) + v +Background Service Worker +``` + +## Two Message Type Enums + +- **`SERVICE_TYPES`** -- messages from the popup to the background (internal + extension communication) +- **`EXTERNAL_SERVICE_TYPES`** -- messages from dApps to the background via the + content script (external communication) + +Both enums are defined in `@shared/constants/` and must be used as the single +source of truth for message types. + +## Adding a New Message Type + +### Step 1: Add to the Enum + +Add the new type to `SERVICE_TYPES` (for popup-to-background) or +`EXTERNAL_SERVICE_TYPES` (for dApp-to-background): + +```typescript +// @shared/constants/services.ts +export enum SERVICE_TYPES { + // ... existing types + GET_NEW_DATA = "GET_NEW_DATA", +} +``` + +### Step 2: Create a Handler + +Create a handler function in +`extension/src/background/messageListener/handlers/`: + +```typescript +// extension/src/background/messageListener/handlers/getNewData.ts +export const handleGetNewData = async (request: MessageRequest) => { + try { + const data = await fetchNewData(request.params); + return { result: data }; + } catch (error) { + captureException(error); + return { error: "Failed to fetch data" }; + } +}; +``` + +### Step 3: Register in the Listener + +Register the handler in the background message listener: + +```typescript +case SERVICE_TYPES.GET_NEW_DATA: + return handleGetNewData(request); +``` + +### Step 4: Send from the Popup + +Use `sendMessageToBackground()` from `@shared/api/internal`: + +```typescript +import { sendMessageToBackground } from "@shared/api/internal"; +import { SERVICE_TYPES } from "@shared/constants/services"; + +const response = await sendMessageToBackground({ + type: SERVICE_TYPES.GET_NEW_DATA, + params: { + /* ... */ + }, +}); +``` + +## Response Queue Pattern + +For operations requiring user approval (signing, connecting): + +1. Background generates a unique ID via `crypto.randomUUID()` +2. A promise resolver is stored in a response queue array, keyed by the UUID +3. Background opens the approval window with the UUID as a URL parameter +4. When the user responds, the approval window sends the result back +5. The matching resolver is found by UUID, called with the result, and spliced + from the array +6. Timeout cleanup removes stale entries if the user closes the window without + responding + +## Response Structure (CRITICAL) + +Handlers MUST return structured objects rather than throwing. Follow this exact +pattern: + +### Success Response + +Return domain-specific fields WITHOUT an `error` field: + +```typescript +// CORRECT success responses +return { signedTransaction: "XDR..." }; +return { publicKey: "G...", allAccounts: [...] }; +return { preparedTransaction: xdr, simulationResponse }; +``` + +### Error Response + +Return ONLY `{ error: string }` — no domain-specific fields mixed in: + +```typescript +// CORRECT error response +return { error: "Soroban simulation failed" }; + +// WRONG — mixing error with domain fields +return { error: "Soroban simulation failed", simulationResponse }; // DON'T DO THIS +``` + +### Why This Matters + +The SDK and popup code check for the presence of `error` to determine +success/failure. Mixing error with data fields creates ambiguous responses. + +### Exception: apiError for SDK consumers + +When the caller is `@stellar/freighter-api`, include `apiError` alongside +`error` for detailed diagnostics: + +```typescript +return { + error: "User declined", + apiError: "User rejected the transaction request", +}; +``` + +## Shared API Layer + +The `sendMessageToBackground()` function is the only approved way to send +messages from the popup to the background. It is defined in +`@shared/api/helpers/extensionMessaging.ts` and re-exported from +`@shared/api/internal.ts`: + +- Wraps `browser.runtime.sendMessage` with proper typing +- Handles response parsing and error extraction +- Never call `browser.runtime.sendMessage` directly from popup code diff --git a/docs/skills/freighter-best-practices/references/performance.md b/docs/skills/freighter-best-practices/references/performance.md new file mode 100644 index 0000000000..2677759f8f --- /dev/null +++ b/docs/skills/freighter-best-practices/references/performance.md @@ -0,0 +1,241 @@ +# Performance -- Freighter Extension + +Based on analysis of 224 component files. Current performance score: **5.9/10**. + +## CRITICAL: Complete Implementations First + +**Performance patterns are optimizations WITHIN complete implementations, not +replacements.** + +When asked to create a component that needs derived state: + +1. **First** produce the complete React component with UI, props interface, and + rendering logic +2. **Then** apply performance optimizations (createSelector, useMemo, + useCallback, React.memo) + +```typescript +// WRONG — only selectors, no component +export const selectTotalBalance = createSelector([...], ...); +// Missing: the actual component that USES these selectors + +// CORRECT — complete implementation with performance optimizations +// 1. Memoized selectors (performance layer) +const selectTotalBalance = createSelector([balancesSelector], (balances) => ...); + +// 2. Props interface (type safety) +interface DashboardProps { + title: string; +} + +// 3. Complete component (UI layer) +export const Dashboard = ({ title }: DashboardProps) => { + const totalBalance = useSelector(selectTotalBalance); + const formattedBalance = useMemo(() => formatBalance(totalBalance), [totalBalance]); + + return ( +
+

{title}

+ {formattedBalance} +
+ ); +}; +``` + +**Never produce only selectors when a component is requested.** + +## React.memo -- CRITICAL GAP + +Only **1 memo() usage** in the entire popup codebase (`AccountAssets/AssetIcon` +with custom `shouldAssetIconSkipUpdate` comparator). This is a critical +performance gap. + +**RULE: Components rendered in lists or receiving frequently-changing parent +props MUST use `React.memo()`.** + +Components that should be memoized: + +- All list item components (TokenList items, AssetRows, OperationsKeyVal) +- AccountTabs, AccountHeader sub-components +- ManageAssetRows children + +```typescript +// REQUIRED for list-rendered components +export const AssetRow = memo( + ({ asset, onSelect }: AssetRowProps) => { ... }, + (prev, next) => isEqual(prev.asset, next.asset), +); +``` + +## useMemo -- 13 occurrences across 7 files (GOOD) + +Currently used for: + +- Conditional computations (muxed checks, contract validation) +- Debounced function creation +- Object creation in dependencies + +**RULE: Wrap in useMemo when:** + +1. Computing derived values from expensive operations (parsing, formatting, + asset lookups) +2. Creating objects/arrays that are passed as props to memoized children +3. Values used as useEffect dependencies that would otherwise change every + render + +```typescript +// REQUIRED — computed value passed as prop +const formattedBalances = useMemo( + () => balances.map((b) => formatBalance(b, network)), + [balances, network], +); + +// NOT NEEDED — simple primitive +const isMainnet = network === NETWORKS.PUBLIC; +``` + +## useCallback -- 15 occurrences across 9 files (MODERATE) + +**RULE: ANY callback passed as a prop MUST be wrapped in useCallback.** + +```typescript +// REQUIRED — passed to child component +const handleSelect = useCallback( + (asset: Asset) => { dispatch(selectAsset(asset)); }, + [dispatch], +); + +// CRITICAL ANTI-PATTERN — 130 inline arrows found in codebase +// WRONG: + handleClick(asset)} /> + +// CORRECT: +const onClick = useCallback(() => handleClick(asset), [asset]); + +``` + +**High-priority files needing useCallback refactoring:** + +- `AccountAssets/index.tsx` — 10+ inline handlers +- `ManageAssetRows/ChangeTrustInternal/index.tsx` — 10 inline handlers +- `AccountHeader/index.tsx` — 10+ inline handlers + +## Inline Functions in JSX -- CRITICAL (130 occurrences) + +**130 inline arrow functions** across 54 files. **15 inline style objects** +across 8 files. + +**RULE: Never create inline functions inside .map() or list renders. Extract to +useCallback.** + +```typescript +// WRONG — creates new function per render per item +{balances.map((b) => ( + handleClick(b.canonical)} key={b.canonical} /> +))} + +// CORRECT +const handleRowClick = useCallback((canonical: string) => { + handleClick(canonical); +}, [handleClick]); + +{balances.map((b) => ( + +))} +``` + +## Selector Memoization -- createSelector in 4 files (GOOD) + +`createSelector` from reselect is used in `remoteConfig.ts`, `settings.ts`, +`cache.ts`, `accountServices.ts`. 161 `useSelector` calls across 81 files. + +**RULE: All derived/computed state MUST use `createSelector`. Never compute +inline in components.** + +```typescript +// CORRECT — memoized selector +export const selectFormattedBalances = createSelector( + [selectBalances, selectNetwork], + (balances, network) => balances.map((b) => formatBalance(b, network)), +); + +// WRONG — recomputes every render +const formatted = balances.map((b) => formatBalance(b, network)); +``` + +## useEffect Dependencies -- 48 eslint-disable-next-line + +48 `react-hooks/exhaustive-deps` suppressions across the codebase. + +**RULE: Every eslint-disable for exhaustive-deps MUST have a comment explaining +WHY.** + +```typescript +// ACCEPTABLE — documented intentional mount-only effect +useEffect(() => { + fetchInitialData(); + // eslint-disable-next-line react-hooks/exhaustive-deps — intentional mount-only fetch +}, []); + +// WRONG — bare suppression +useEffect(() => { + fetchData(publicKey, network); + // eslint-disable-next-line react-hooks/exhaustive-deps +}, [publicKey]); +``` + +## Code Splitting -- 0 React.lazy (MISSED OPPORTUNITY) + +No `React.lazy` or `Suspense` usage. + +**RULE: Lazy-load components not needed on initial render:** + +- Debug views (`Debug.tsx`, `IntegrationTest.tsx`) +- Heavy modal/drawer components +- Detail views that load on navigation + +```typescript +const Debug = React.lazy(() => import("popup/views/Debug")); + +// In router: +}> + } /> + +``` + +## Context Usage -- EXCELLENT (3 contexts only) + +Only 3 React contexts exist (`InputWidthContext`, `AccountTabsContext`, `View`). +All hold simple primitive values. Do not add more contexts for data state — use +Redux. + +## Key Props -- 4 index-as-key anti-patterns + +4 files use array index as key. Fix these: + +- `GrantAccess/index.tsx` +- `SignMessage/index.tsx` +- `TransactionDetail/index.tsx` +- `ConfirmMnemonicPhrase/index.tsx` + +**RULE: Use stable unique identifiers as keys, not array indices.** + +## useReducer + Redux Dispatch Separation + +For complex async flows, separate local UI loading state from global Redux +state: + +- Use `useReducer` for local component state (loading spinners, form validation, + step tracking) +- Use Redux dispatch for global state changes (account data, network state, + transaction results) + +## Performance Priority Actions + +| Priority | Action | Impact | +| -------- | ------------------------------------------------- | ---------------------------- | +| **P0** | Add React.memo() to list item components | 30-40% fewer re-renders | +| **P0** | Convert 130 inline arrow functions to useCallback | Stabilize reference equality | +| **P1** | Add React.lazy for debug/modal views | Reduce initial bundle | +| **P1** | Document all 48 exhaustive-deps suppressions | Prevent future bugs | +| **P2** | Add useMemo for computed values in map() renders | Prevent recomputation | diff --git a/docs/skills/freighter-best-practices/references/security.md b/docs/skills/freighter-best-practices/references/security.md new file mode 100644 index 0000000000..69155ff0cd --- /dev/null +++ b/docs/skills/freighter-best-practices/references/security.md @@ -0,0 +1,112 @@ +# Security -- Freighter Extension + +## Key Storage Architecture + +Freighter is a non-custodial wallet. Private keys never leave the device. + +- **Encrypted vault:** stored in `chrome.storage.local`, encrypted with the + user's password +- **Decrypted keys:** held only in `chrome.storage.session` (memory-only, + cleared when the browser session ends) +- **Keys exist only in the background context** -- the popup and content scripts + never have access to raw key material + +## Popup Never Sees Raw Keys + +The popup sends unsigned XDR to the background and receives signed XDR back. The +signing flow: + +1. Popup builds an unsigned transaction (XDR) +2. Popup sends XDR to background via `sendMessageToBackground()` +3. Background decrypts the key from session storage, signs the XDR +4. Background returns the signed XDR to the popup +5. Popup submits the signed transaction to the network + +## Hardware Wallet Path + +Hardware wallets (Ledger) use the same interface as software signing. The +background detects the key type and routes to the appropriate signing backend +(local key vs. hardware device). The popup code is identical for both paths. + +## Message Validation + +Every message received by the background must pass validation. The required +checks depend on data sensitivity: + +1. **Valid message type** -- the message type must be a member of the + `SERVICE_TYPES` or `EXTERNAL_SERVICE_TYPES` enum (all messages) +2. **Sender origin check** -- verify the message comes from a trusted source + (sensitive endpoints only) +3. **Allow list check** -- the dApp's origin must be in the user's approved + list, or trigger an approval prompt (sensitive endpoints only) +4. **Public key existence** -- the requested account must exist in the wallet + (user-specific endpoints only) + +### Allowlist Rules by Endpoint Type + +| Endpoint Type | Allowlist Check | Examples | +| --------------------------------------------- | --------------- | -------------------------------------------------------------------- | +| Read-only non-sensitive (network info) | NOT required | `requestNetwork`, `requestNetworkDetails`, `requestConnectionStatus` | +| User-specific sensitive (public key, signing) | Required | `requestPublicKey`, `requestAccess`, `signTransaction` | +| State-modifying | Required | `setAllowedStatus` | + +## Window-Based Approval Flow + +When a dApp requests a sensitive operation (signing, account access): + +1. Background generates a unique request ID via `crypto.randomUUID()` +2. Background opens a new browser window with the approval UI, passing params + via URL encoding +3. A promise resolver is stored in a response queue, keyed by the request ID +4. When the user approves or rejects, the approval window sends the response + back +5. The promise resolves with the result, and the entry is spliced from the + response queue + +## Content Script Filtering + +The content script acts as a gatekeeper between the page and the extension: + +- Listens for `window.postMessage` events from the page +- Filters by source: only messages with `EXTERNAL_MSG_REQUEST` source are + processed +- Validates the message type against `EXTERNAL_SERVICE_TYPES` enum +- Only valid messages are forwarded to the background via + `chrome.runtime.sendMessage` +- All other messages are silently dropped + +## Content Security Policy + +Manifest V3 enforces strict CSP by default: + +- No inline scripts (`script-src 'self'`) +- No `eval()` or dynamic code generation +- Minimal permissions: only `storage` and `alarms` are declared in the manifest + +## Blockaid Integration + +- Every dApp transaction is scanned by Blockaid before signing +- Blockaid provides risk assessment (malicious, benign, unknown) +- Debug panel available at the `/debug` route in the extension popup during + development (requires dev mode) +- Scan results are displayed to the user in the approval UI + +## Production Guardrails + +- Production builds block connections to the dev server +- Additional security checks are enabled in production mode +- Source maps are not shipped in production builds + +## Common Security Mistakes to Avoid + +- **Logging keys:** never log private keys, mnemonics, or decrypted key material + to the console +- **Weakening CSP:** never add `unsafe-inline` or `unsafe-eval` to the manifest + CSP +- **Trusting page content:** never use data from `window.postMessage` without + validation against the enum +- **Non-null assertions on key material:** `privateKey!` can mask missing keys + -- always handle the undefined case explicitly +- **Storing secrets in local storage:** `chrome.storage.local` persists across + sessions and is not memory-only. Use `chrome.storage.session` for decrypted + material diff --git a/docs/skills/freighter-best-practices/references/testing.md b/docs/skills/freighter-best-practices/references/testing.md new file mode 100644 index 0000000000..1b388e65d5 --- /dev/null +++ b/docs/skills/freighter-best-practices/references/testing.md @@ -0,0 +1,91 @@ +# Testing -- Freighter Extension + +## Jest Unit Tests + +- Test files live in `__tests__/` directories alongside the source they test +- Environment: JSDOM (`jest-fixed-jsdom`) +- Coverage collected from `src/**/*.{ts,tsx,mjs}` +- Configuration in the root `jest.config.js` + +### Mocking Pattern + +Use typed mocks via `jest.requireMock` for type-safe mock access: + +```typescript +const { someFunction } = jest.requireMock< + typeof import("@shared/api/internal") +>("@shared/api/internal"); +``` + +Factory functions create consistent test fixtures: + +- `makeStore()` -- creates a Redux store with test-specific initial state +- `store.getState()` -- standard Redux method to get a snapshot of the current + store state + +### Testing Redux + +1. Create a test store with `configureStore()` and the real reducers +2. Dispatch thunks against the test store +3. Assert state changes via `store.getState()` + +```typescript +const store = makeStore({ + preloadedState: { + /* ... */ + }, +}); +await store.dispatch(fetchAccountBalances("GABC...")); +expect(store.getState().accountBalances.status).toBe(ActionStatus.SUCCESS); +``` + +### Testing Background Handlers + +Call handler functions directly with mock parameters: + +```typescript +const response = await handleSignTransaction({ + transactionXdr: "AAAA...", + publicKey: "GABC...", +}); +expect(response).toEqual({ signedTransaction: "signed-xdr" }); +expect(captureException).not.toHaveBeenCalled(); +``` + +## Playwright End-to-End Tests + +- **Browser:** Chromium only +- **Viewport:** 1280x720 +- **Timeout:** 15 seconds per test +- **Retries:** 5 +- **Workers:** 8 locally, 4 in CI + +### Fixtures + +The `test-fixtures.ts` file provides: + +- A browser context with the extension loaded +- Extension ID extraction from the service worker URL +- Direct access to the service worker for background testing + +### Snapshot Testing + +- Snapshots stored in `[testName].test.ts-snapshots/` +- Update snapshots with `--update-snapshots` flag + +## Running Tests + +| Command | Description | +| ------------------------- | ------------------------------------ | +| `yarn test` | Run Jest in watch mode | +| `yarn test:ci` | Run Jest for CI (no watch, coverage) | +| `yarn test:e2e` | Run all Playwright e2e tests | +| `yarn test:e2e --headed` | Run e2e with visible browser | +| `yarn test:e2e --ui` | Run e2e with Playwright UI mode | +| `PWDEBUG=1 yarn test:e2e` | Run e2e with Playwright debugger | + +## CI Pipeline + +- **`runTests.yml`** -- runs Jest unit tests and Playwright e2e tests on every + pull request +- **`codeql.yml`** -- runs CodeQL security analysis on every pull request