Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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.
12 changes: 9 additions & 3 deletions apps/next/src/hooks/useAllTokensFromEnabledNetworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ import { useNetworkContext } from '@core/ui';
import { useAllTokens } from './useAllTokens';

// TODO: Currently the hook is using favoriteNetwork. It should be changed to enabledNetworks once added.
export const useAllTokensFromEnabledNetworks = () => {
const { favoriteNetworks } = useNetworkContext();
export const useAllTokensFromEnabledNetworks = (
onlyTokensWithBalances?: boolean,
hideMalicious?: boolean,
) => {
const { enabledNetworks } = useNetworkContext();
const tokens = useAllTokens(enabledNetworks, hideMalicious);

return useAllTokens(favoriteNetworks, false);
return !onlyTokensWithBalances
? tokens
: tokens.filter((token) => token.balance);
};
2 changes: 1 addition & 1 deletion apps/next/src/hooks/useTokensForAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ const isNativeToken = (
): token is FungibleTokenBalance & { type: TokenType.NATIVE } =>
token.type === TokenType.NATIVE;

const isAvaxToken = (
export const isAvaxToken = (
token: FungibleTokenBalance,
): token is FungibleTokenBalance & { type: TokenType.NATIVE } =>
token.type === TokenType.NATIVE && token.symbol === 'AVAX';
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
8 changes: 8 additions & 0 deletions apps/next/src/localization/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"Add using Ledger": "Add using Ledger",
"Address copied!": "Address copied!",
"All": "All",
"All Networks": "All Networks",
"All keys contained in this file are already imported.": "All keys contained in this file are already imported.",
"All networks": "All networks",
"All types": "All types",
Expand Down Expand Up @@ -103,6 +104,7 @@
"Average password - this will do": "Average password - this will do",
"Awesome!": "Awesome!",
"Back": "Back",
"Balance": "Balance",
"Balance change unavailable.": "Balance change unavailable.",
"Balances loading...": "Balances loading...",
"Best price available": "Best price available",
Expand Down Expand Up @@ -379,6 +381,7 @@
"Ledger requires you to set up a wallet policy in the Bitcoin app. Please approve or reject this action on your Ledger device.": "Ledger requires you to set up a wallet policy in the Bitcoin app. Please approve or reject this action on your Ledger device.",
"Ledger {{number}}": "Ledger {{number}}",
"Lending": "Lending",
"Let's go": "Let's go",
"Let's go!": "Let's go!",
"Light": "Light",
"Limit exceeded": "Limit exceeded",
Expand Down Expand Up @@ -441,6 +444,7 @@
"No account is active": "No account is active",
"No active network": "No active network",
"No address": "No address",
"No assets yet": "No assets yet",
"No collectibles": "No collectibles",
"No connected sites": "No connected sites",
"No custom headers are configured.": "No custom headers are configured.",
Expand All @@ -454,10 +458,12 @@
"Normal": "Normal",
"Not connected": "Not connected",
"Notify me when it’s done": "Notify me when it’s done",
"On-ramp using Core in two minutes": "On-ramp using Core in two minutes",
"Only Keystore files from the Avalanche Wallet are supported": "Only Keystore files from the Avalanche Wallet are supported",
"Only keystore files exported from the Avalanche Wallet are supported.": "Only keystore files exported from the Avalanche Wallet are supported.",
"Oops! \n Something went wrong": "Oops! \n Something went wrong",
"Oops! It seems like you have no internet connection. Please try again later.": "Oops! It seems like you have no internet connection. Please try again later.",
"Oops! Something went wrong": "Oops! Something went wrong",
"Open any authenticator app and scan the QR code below or enter the code manually": "Open any authenticator app and scan the QR code below or enter the code manually",
"Open the Avalanche app on your Keystone device in order to continue with this transaction": "Open the Avalanche app on your Keystone device in order to continue with this transaction",
"Open the Solana app on your Ledger device": "Open the Solana app on your Ledger device",
Expand Down Expand Up @@ -530,6 +536,7 @@
"Protocol": "Protocol",
"Provider": "Provider",
"Public key not found": "Public key not found",
"Quantity": "Quantity",
"Quote includes an {{coreFee}} Core fee": "Quote includes an {{coreFee}} Core fee",
"RPC URL reset successfully": "RPC URL reset successfully",
"Rate": "Rate",
Expand Down Expand Up @@ -695,6 +702,7 @@
"To use Solana in Core you will need to add an account from your Ledger device. You can always add this later at any time": "To use Solana in Core you will need to add an account from your Ledger device. You can always add this later at any time",
"Token": "Token",
"Token Added": "Token Added",
"Token Price": "Token Price",
"Token already exists in the wallet.": "Token already exists in the wallet.",
"Token contract address": "Token contract address",
"Token name": "Token name",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { useAllTokensFromEnabledNetworks } from '@/hooks/useAllTokensFromEnabledNetworks';
import { Box } from '@avalabs/k2-alpine';
import { TokenType } from '@avalabs/vm-module-types';
import { isTokenMalicious } from '@core/common';
import { FungibleTokenBalance } from '@core/types';
import { FC, useMemo } from 'react';
import { FixedSizeList, ListChildComponentProps } from 'react-window';
Expand All @@ -16,41 +14,29 @@ interface Props {

export const TokenSwitchList: FC<Props> = ({ filter, spam }) => {
const [height, containerRef] = useContainerHeight<HTMLDivElement>(400);
const tokensWithBalances = useAllTokensFromEnabledNetworks();
const tokensWithBalances = useAllTokensFromEnabledNetworks(false, !spam);

const nonNative = useMemo(() => {
return tokensWithBalances.filter(
(token) => token.type !== TokenType.NATIVE,
);
}, [tokensWithBalances]);

const spamless = useMemo(
() =>
spam ? nonNative : nonNative.filter((token) => !isTokenMalicious(token)),
[spam, nonNative],
);

const filtered = useMemo(
const filteredTokensList = useMemo(
() =>
filter
? spamless.filter((token) => {
? tokensWithBalances.filter((token) => {
const normalizedFilter = filter.toLowerCase();
return (
token.name.toLowerCase().includes(normalizedFilter) ||
token.symbol.toLowerCase().includes(normalizedFilter)
);
})
: spamless,
[filter, spamless],
: tokensWithBalances,
[filter, tokensWithBalances],
);

return (
<Box height={1} ref={containerRef}>
<FixedSizeList
height={height}
width="100%"
itemData={filtered}
itemCount={filtered.length}
itemData={filteredTokensList}
itemCount={filteredTokensList.length}
itemSize={54}
overscanCount={5}
style={{ overflow: 'auto', scrollbarWidth: 'none' }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,9 @@ export const PortfolioHome: FC = () => {
const TabsContainer = styled(Stack)(({ theme }) => ({
position: 'sticky',
bottom: 0,
zIndex: 100,
paddingTop: theme.spacing(1),
background: `linear-gradient(180deg, ${getHexAlpha(theme.palette.background.default, 0)} 0%, ${theme.palette.background.default} 16px)`,

zIndex: theme.zIndex.appBar,
'> div': {
background: 'unset',
},
Expand Down
Loading
Loading