Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
Core Extension is a non-custodial browser extension for Chromium browsers built using Manifest V3. It enables users to seamlessly and securely interact with Web3 applications powered by Avalanche, supporting Bitcoin, Ethereum, and Solana networks.

The project uses a monorepo structure with yarn workspaces and contains two main applications:

- **Legacy**: The current production version (React 17, older architecture) in `apps/legacy/`
- **Next**: The next-generation version (React 19, modern architecture) in `apps/next/`

## Essential Commands

### Development

```bash
# Start development (defaults to legacy)
yarn dev
Expand All @@ -27,6 +29,7 @@ yarn setup
```

### Building

```bash
# Build for production
yarn build # Legacy (default)
Expand All @@ -44,6 +47,7 @@ yarn zip:next # Next-gen zip for Chrome Store
```

### Testing & Quality

```bash
# Run tests
yarn test # Legacy tests
Expand All @@ -64,13 +68,16 @@ yarn scanner:next # Next-gen i18n scan
```

### Build System

Built with Rsbuild (Rspack-based) instead of Webpack. Each package has its own build configuration:

- Apps use environment-specific configs (`rsbuild.{app}.{env}.ts`)
- Packages have shared common configs with dev/prod variants

## Architecture

### Extension Structure (Manifest V3)

The extension follows the standard Manifest V3 architecture with 4 isolated components:

1. **Service Worker** (`packages/service-worker/`): Background script handling business logic, network communication, transaction signing, and encrypted storage
Expand All @@ -79,6 +86,7 @@ The extension follows the standard Manifest V3 architecture with 4 isolated comp
4. **Injected Provider** (`packages/inpage/`): EIP-1193 compliant provider injected into web pages for dApp communication

### Monorepo Packages

- `@core/common`: Shared utilities, constants, and helper functions
- `@core/messaging`: Cross-context communication system with JSON serialization
- `@core/service-worker`: Background script services and business logic
Expand All @@ -89,9 +97,11 @@ The extension follows the standard Manifest V3 architecture with 4 isolated comp
- `@core/offscreen`: Offscreen document for secure DOM operations

### Service Architecture

The service worker uses dependency injection (TSyringe) with service-oriented architecture:

**Core Services:**

- `AccountsService`: Multi-wallet account management
- `BalancesService`: Token balances and portfolio aggregation
- `NetworkService`: Blockchain network configuration and switching
Expand All @@ -104,74 +114,87 @@ The service worker uses dependency injection (TSyringe) with service-oriented ar
- `StorageService`: Encrypted data persistence

**Architecture Patterns:**

- **Handlers**: RPC method processors combining service functionality
- **Events**: Pub/sub system for cross-component communication
- **Middleware**: Request processing pipeline (auth, logging, permissions)

### Extension Entry Points

The frontend supports multiple contexts determined by `isSpecificContextContainer.ts`:

- **POPUP**: Main extension UI (browser icon click)
- **CONFIRM**: Approval windows for dApp interactions
- **HOME**: Fullscreen onboarding experience

## Development Guidelines

### Critical Manifest V3 Constraints

- **No XMLHttpRequest**: Use `fetch()` only
- **No DOM/window**: Use offscreen documents for DOM operations
- **Service Worker Restarts**: Background script can restart anytime - never rely on in-memory state
- **Strict CSP**: No eval(), limited inline scripts
- **Storage**: Always use encrypted `StorageService`, never direct chrome.storage

### Security Requirements

- **Encrypt Everything**: All storage must be encrypted via `StorageService`
- **No Plain Text Secrets**: Never expose private keys outside respective services
- **Password Fields**: Use password inputs for sensitive data (Chrome caches regular inputs)
- **No Frontend Storage**: Never use localStorage - all state in encrypted background storage

### Code Organization

- **Single Purpose Services**: Keep services focused, combine functionality in handlers
- **Clean State**: Reset all state when wallet locks
- **Event-Driven Communication**: Services communicate via events, not direct calls
- **Handler-Service Pattern**: Handlers orchestrate multiple services for complex operations

### Multi-App Development

- Legacy and next-gen apps share the same service worker and core packages
- Test changes in both applications when modifying shared functionality
- Service worker changes affect both apps simultaneously
- Use appropriate commands: `yarn dev:legacy` vs `yarn dev:next`

### Component Library

- Uses [K2 Components](https://k2-components.pages.dev/) for UI consistency
- Avoid overriding MUI classes - update K2 or Figma instead
- Ask in `k2-product-design-system` Slack for design system questions

### Environment Setup

- Copy `.env.example` to `.env.dev` for development
- Requires Node.js 20.18.0 and Yarn 4.7.0 (managed by Volta)
- Requires access to `@avalabs` packages on npmjs.com with 2FA

### Chrome Extension Development

1. Build: `yarn dev` (legacy) or `yarn dev:next` (next-gen)
2. Chrome: Go to `chrome://extensions/`, enable Developer mode
3. Load: Click "Load unpacked", select `dist/` (legacy) or `dist-next/` (next-gen) folder
4. Hot reload: Changes reload automatically during development
5. Service Worker: Requires extension reload for background script changes

### Blockchain Network Support

- **Avalanche**: C-Chain, P-Chain, X-Chain (primary focus)
- **Bitcoin**: Native Bitcoin and testnet support
- **Ethereum**: EVM-compatible chains
- **Solana**: Next-gen app only

### Testing Strategy

- Unit tests co-located with source (`.test.ts` files)
- Integration tests for service interactions
- Mocks in `__mocks__/` directories
- Jest testing framework across all packages
- Test both legacy and next-gen when changing shared code

### Release Process

- Uses semantic versioning with conventional commits
- `feat:` = minor version bump
- `fix:` = patch version bump
Expand All @@ -180,6 +203,7 @@ The frontend supports multiple contexts determined by `isSpecificContextContaine
- Production: Manual GitHub Action trigger

### Common Gotchas

- Service worker 5-minute idle timeout and random restarts
- EIP-1193 provider race conditions with other wallets (MetaMask, Rabby)
- WebUSB limitations in service worker (requires frontend for Ledger)
Expand All @@ -188,9 +212,10 @@ The frontend supports multiple contexts determined by `isSpecificContextContaine
- Content Security Policy restrictions in Manifest V3

### Performance Considerations

- Balance polling service manages network requests efficiently
- Use incremental promise resolution for batch operations
- Implement proper cleanup in service constructors
- Avoid memory leaks in long-running background processes

Always run `yarn lint` and `yarn typecheck` before committing changes.
Always run `yarn lint` and `yarn typecheck` before committing changes.
16 changes: 14 additions & 2 deletions apps/next/src/components/AccountSelect/AccountSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { Typography } from '@avalabs/k2-alpine';
import { useTranslation } from 'react-i18next';

import { Account, AddressType } from '@core/types';
import { useAccountsContext, useWalletContext } from '@core/ui';
import {
useAccountsContext,
useAnalyticsContext,
useWalletContext,
} from '@core/ui';

import { SearchableSelect } from '@/components/SearchableSelect';

Expand Down Expand Up @@ -33,6 +37,7 @@ export const AccountSelect: FC<AccountSelectProps> = ({
const { t } = useTranslation();
const { allAccounts } = useAccountsContext();
const { getWallet } = useWalletContext();
const { capture } = useAnalyticsContext();

return (
<SearchableSelect<Account>
Expand All @@ -46,7 +51,14 @@ export const AccountSelect: FC<AccountSelectProps> = ({
query={query}
onQueryChange={onQueryChange}
value={value}
onValueChange={onValueChange}
onValueChange={(newAccount) => {
if (newAccount.id !== value?.id) {
capture('AccountSelectorAccountSwitched', {
type: newAccount.type,
});
}
onValueChange(newAccount);
}}
label={t('Account')}
renderValue={(val) =>
val ? (
Expand Down
8 changes: 6 additions & 2 deletions apps/next/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getHexAlpha, Stack, Typography, useTheme } from '@avalabs/k2-alpine';
import { useAccountsContext } from '@core/ui';
import { useAccountsContext, useAnalyticsContext } from '@core/ui';
import { useState } from 'react';
import { MdOutlineUnfoldMore } from 'react-icons/md';
import { useHistory } from 'react-router-dom';
Expand All @@ -19,6 +19,7 @@ export const Header = () => {
const [isAddressAppear, setIsAddressAppear] = useState(false);
const [isAIBackdropOpen, setIsAIBackdropOpen] = useState(false);
const history = useHistory();
const { capture } = useAnalyticsContext();

return (
<Stack
Expand Down Expand Up @@ -49,7 +50,10 @@ export const Header = () => {
<AccountSelectContainer
onMouseOver={() => setIsAddressAppear(true)}
onMouseLeave={() => setIsAddressAppear(false)}
onClick={() => history.push('/account-management')}
onClick={() => {
capture('AccountSelectorOpened');
history.push('/account-management');
}}
>
<AccountInfo>
<PersonalAvatar cached size="xsmall" sx={{ mr: 1 }} />
Expand Down
7 changes: 6 additions & 1 deletion apps/next/src/components/Header/components/HeaderActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Account } from '@core/types';
import { FC } from 'react';
import { FiSettings } from 'react-icons/fi';
import { useHistory } from 'react-router-dom';
import { useAnalyticsContext } from '@core/ui';
import { ConnectedSites } from '../ConnectedSites';
import { ViewModeSwitcher } from '../ViewModeSwitcher';

Expand All @@ -15,6 +16,7 @@ type Props = {
export const HeaderActions: FC<Props> = ({ account }) => {
const history = useHistory();
const theme = useTheme();
const { capture } = useAnalyticsContext();
const {
state: { pendingTransfers },
} = useNextUnifiedBridgeContext();
Expand All @@ -26,7 +28,10 @@ export const HeaderActions: FC<Props> = ({ account }) => {
<IconButton
disabled={!account}
size="small"
onClick={() => history.push(`/receive?accId=${account?.id}`)}
onClick={() => {
capture('TokenReceiveClicked');
history.push(`/receive?accId=${account?.id}`);
}}
>
<QrCodeIcon fill={theme.palette.text.primary} size={24} />
</IconButton>
Expand Down
9 changes: 8 additions & 1 deletion apps/next/src/components/RecipientSelect/RecipientSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Typography } from '@avalabs/k2-alpine';
import { useTranslation } from 'react-i18next';

import { AddressType } from '@core/types';
import { useAnalyticsContext } from '@core/ui';

import { SearchableSelect } from '@/components/SearchableSelect';

Expand Down Expand Up @@ -30,6 +31,7 @@ export const RecipientSelect: FC<RecipientSelectProps> = ({
onQueryChange,
}) => {
const { t } = useTranslation();
const { capture } = useAnalyticsContext();

const getGroupLabel = useGroupLabel();

Expand All @@ -45,7 +47,12 @@ export const RecipientSelect: FC<RecipientSelectProps> = ({
query={query}
onQueryChange={onQueryChange}
value={value}
onValueChange={onValueChange}
onValueChange={(recipient) => {
if (recipient && getType(recipient) === 'contact') {
capture('SendContactSelected', { contactSource: 'contacts' });
}
onValueChange(recipient);
}}
label={t('Send to')}
renderValue={(val) =>
val ? (
Expand Down
8 changes: 4 additions & 4 deletions apps/next/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,400..700;1,14..32,400..700&display=swap"
/>
<style>
* {
scrollbar-width: thin;
scrollbar-color: #949497 rgba(0, 0, 0, 0.05);
}
* {
scrollbar-width: thin;
scrollbar-color: #949497 rgba(0, 0, 0, 0.05);
}
html {
height: 100%;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
useBalancesContext,
useBalanceTotalInCurrency,
useSettingsContext,
useAnalyticsContext,
} from '@core/ui';
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
Expand All @@ -22,6 +23,7 @@ export const AccountDetailsHeader: FC<Props> = ({ account }) => {
const balance = useBalanceTotalInCurrency(account);
const { isTokensCached, updateBalanceOnNetworks } = useBalancesContext();
const { currencyFormatter } = useSettingsContext();
const { capture } = useAnalyticsContext();

return (
<header>
Expand All @@ -40,7 +42,12 @@ export const AccountDetailsHeader: FC<Props> = ({ account }) => {
variant="contained"
size="small"
color="secondary"
onClick={() => updateBalanceOnNetworks([account])}
onClick={() => {
capture('AccountSelectorRefreshBalanceClicked', {
type: account.type,
});
updateBalanceOnNetworks([account]);
}}
>
{t('Refresh')}
</EndAction>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const AddOrConnectWallet: FC = () => {
.then(goBack)
.then(() => {
toast.success(t('Account created successfully'));
capture('CreatedANewAccountSuccessfully');
})
.catch((error) => {
toast.error(t('Account creation failed'));
Expand All @@ -95,21 +96,30 @@ export const AddOrConnectWallet: FC = () => {
Icon={MdList}
primary={t('Import a recovery phrase')}
secondary={t('Enter your recovery phrase to import a wallet')}
onClick={() => openFullscreenTab('import-wallet/seedphrase')}
onClick={() => {
capture('AddWalletWithSeedphrase_Clicked');
openFullscreenTab('import-wallet/seedphrase');
}}
/>
<Divider />
<AccountListItem
Icon={LedgerIcon}
primary={t('Import Ledger wallet')}
secondary={t('Use Ledger to connect')}
onClick={() => openFullscreenTab('import-wallet/ledger')}
onClick={() => {
capture('AddWalletWithLedger_Clicked');
openFullscreenTab('import-wallet/ledger');
}}
/>
<Divider />
<AccountListItem
Icon={SiWalletconnect}
primary={t('Connect with WalletConnect')}
secondary={t('Scan QR code to connect your wallet')}
onClick={underDevelopmentClick}
onClick={() => {
capture('ImportWithWalletConnect_Clicked');
underDevelopmentClick();
}}
/>
<Divider />
<AccountListItem
Expand All @@ -123,7 +133,10 @@ export const AddOrConnectWallet: FC = () => {
Icon={FaSquareCaretUp}
primary={t('Import with Fireblocks account')}
secondary={t('Manually enter your private key to import')}
onClick={underDevelopmentClick}
onClick={() => {
capture('ImportWithFireblocks_Clicked');
underDevelopmentClick();
}}
/>
</List>
</Card>
Expand Down
Loading
Loading