diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index e100f9dcc..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,260 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -ZERO (zOS) is a flexible application platform for interacting with the Zer0 ecosystem, built with React, TypeScript, and Redux Saga. It features a modular app architecture with integrated messaging (Matrix protocol), Web3 wallet functionality, and social features. - -## Development Commands - -### Setup - -```bash -# Install dependencies (requires GitHub Personal Access Token with read:package scope) -npm config set //npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN -npm install --legacy-peer-deps - -# Copy and configure environment variables -cp .env.example .env -``` - -### Running the Application - -```bash -npm start # Start development server (Vite) -npm run start:legacy # Start with legacy OpenSSL provider (if needed) -npm run electron:start # Start Electron app -``` - -### Build - -```bash -npm run build # Production build (requires up to 6GB memory) -npm run build:legacy # Build with legacy OpenSSL provider -``` - -### Testing - -```bash -npm test # Run Jest tests -npm run test:vitest # Run Vitest tests (files matching *.vitest.*) -``` - -### Code Quality - -```bash -npm run lint # Lint TypeScript/React code -npm run lint:fix # Auto-fix linting issues -npm run code-format:validate # Check code formatting -npm run code-format:fix # Auto-format code with Prettier -``` - -### Other Commands - -```bash -npm run generate-mocks # Generate mock data for testing -``` - -## Technology Stack - -- **Build System**: Vite (migrated from Create React App) -- **React**: v18 with TypeScript -- **State Management**: Redux Toolkit + Redux Saga -- **Routing**: React Router v5 -- **Matrix SDK**: Matrix-js-sdk for chat/messaging -- **Web3**: Ethers.js, Wagmi, RainbowKit for blockchain integration -- **UI**: Custom components with @zero-tech/zui library -- **Styling**: SCSS with BEM methodology -- **Testing**: Jest (legacy) and Vitest (new tests) - -## Architecture - -### Component, Redux, Saga, Normalizr Pattern - -The application follows a unidirectional data flow architecture: - -1. **Components** display data and handle user input (no business logic) -2. **Redux Store** (`src/store/`) manages global state with domain-specific slices -3. **Sagas** (`src/store/*/saga.ts`) handle business logic, async operations, and side effects -4. **Normalizr** manages relational data in a normalized state structure - -### Key Architectural Patterns - -**Normalized State**: Data (users, channels, messages) is stored in a normalized format to avoid duplication: - -- `state.normalized.users` - User entities keyed by ID -- `state.normalized.channels` - Channel entities keyed by ID -- `state.normalized.messages` - Message entities keyed by ID - -**Redux Saga**: All business logic and async operations run in sagas, not thunks or components. Sagas provide better testability and handle complex concurrency scenarios. - -**Store Structure**: Each domain has its own folder in `src/store/` containing: - -- `index.ts` - Redux slice with actions and reducers -- `saga.ts` - Business logic and side effects -- `selectors.ts` - Reusable state selectors (if needed) - -### Directory Structure - -``` -src/ -├── apps/ # Application modules (Feed, Messenger, Wallet, etc.) -│ ├── app-router.tsx # Main app routing -│ └── */index.tsx # Individual app components -├── components/ # Reusable UI components -├── store/ # Redux slices and sagas -│ ├── index.ts # Store configuration -│ ├── saga.ts # Root saga -│ ├── reducer.ts # Root reducer -│ └── */ # Domain-specific state modules -├── lib/ # Utility functions and helpers -├── authentication/ # Auth-related components -└── pages/ # Top-level page components -``` - -### Apps vs Components - -- **Apps** (`src/apps/`) are route-based features (Messenger, Feed, Wallet, Profile, etc.) -- **Components** (`src/components/`) are reusable UI elements shared across apps -- Apps are registered in `src/apps/app-router.tsx` and mapped to routes - -### Styling with BEM - -All styles use SCSS with BEM (Block Element Modifier) naming: - -```tsx -import { bem } from '../../lib/bem'; -const c = bem('component-name'); - -
-

Title

-
...
-
; -``` - -SCSS files mirror the component structure: - -```scss -.component-name { - &__heading { ... } - &__content { ... } - &__content--active { ... } -} -``` - -Import theme variables from `@zero-tech/zui`: - -```scss -@use '~@zero-tech/zui/styles/theme' as theme; -``` - -## Component Guidelines - -### Function vs Class Components - -- **Function components**: Simple components with minimal logic (max 15-20 lines excluding markup) -- **Class components**: Complex components requiring significant logic or lifecycle methods - -### Hooks - -- Keep custom hooks under 15-20 lines -- Do not nest custom hooks (cannot call a custom hook from another hook) -- No business logic in hooks (only rendering/UI-related logic) -- Do not use hooks for external dependencies or global state interaction - -### Connected Components - -Use Redux `useSelector` and `useDispatch` hooks to connect components to the store. Keep mapStateToProps logic in selector functions when possible. - -## State Management - -### Creating a New Redux Slice - -1. Create folder in `src/store//` -2. Add `index.ts` with slice definition: - -```typescript -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; - -const slice = createSlice({ - name: 'domain', - initialState: { ... }, - reducers: { - receive: (state, action: PayloadAction) => { ... }, - }, -}); - -export const { receive } = slice.actions; -export const { reducer } = slice; -``` - -3. Add `saga.ts` for business logic: - -```typescript -import { takeEvery } from 'redux-saga/effects'; - -export function* saga() { - yield takeEvery(actionType, workerSaga); -} -``` - -4. Register in `src/store/reducer.ts` and `src/store/saga.ts` - -### Normalizr Usage - -For relational data (users, channels, messages), use the normalized store pattern. See `src/store/normalized/` for implementation details. - -## Testing - -- All PRs must include appropriate tests -- Tests should be isolated and not brittle -- Use `redux-saga-test-plan` for saga testing -- Component tests use Jest + Enzyme (legacy) or Vitest + Testing Library (new) -- Vitest tests: name files `*.vitest.tsx` or `*.vitest.ts` - -## Environment Variables - -All environment variables use `REACT_APP_` prefix (legacy from CRA, maintained for Vite compatibility). - -Key variables: - -- `REACT_APP_ETH_CHAIN` - Ethereum network (1 for mainnet, 11155111 for Sepolia) -- `REACT_APP_MATRIX_HOME_SERVER_URL` - Matrix chat server URL -- `REACT_APP_ZERO_API_URL` - Backend API URL -- Various Infura URLs for blockchain connectivity -- See `.env.example` for complete list - -## Git Workflow - -- Main branch: `development` (use this for PRs, not `main`) -- All changes require PR review -- Squash commits appropriately (no "WIP" commits in final PR) -- CI must pass before review -- Follow the [STYLE_GUIDE.md](STYLE_GUIDE.md) for code conventions - -## Matrix Chat Integration - -The app uses Matrix protocol for messaging. For local development, you can: - -- Use the development home server (default in `.env.example`) -- Run a local Matrix server: https://github.com/zer0-os/zOS-chat-server -- Set `REACT_APP_MATRIX_HOME_SERVER_URL=http://localhost:8008` to use local server - -## Adding a New App - -1. Create app folder in `src/apps//` -2. Implement app component with required interface -3. Add route in `src/apps/app-router.tsx` -4. Optionally add link in AppBar component -5. For external apps, use the `ExternalApp` wrapper component - -See [README.md](README.md) "Ways to Participate" section for detailed steps. - -## Code Principles - -- **No over-engineering**: Only implement what's requested, avoid unnecessary abstractions -- **Business logic in sagas**: Keep components focused on UI, move logic to sagas -- **Avoid hooks for business logic**: Use class components or sagas instead -- **BEM for all styles**: Maintain consistent naming conventions -- **Test isolation**: Write tests that are independent and maintainable diff --git a/NFT_TRANSFER_IMPLEMENTATION.md b/NFT_TRANSFER_IMPLEMENTATION.md deleted file mode 100644 index 7a7493047..000000000 --- a/NFT_TRANSFER_IMPLEMENTATION.md +++ /dev/null @@ -1,705 +0,0 @@ -# NFT Transfer Implementation Plan - -This document outlines the implementation plan for adding NFT transfer functionality to the zOS wallet app using the existing Send flow. - -## Overview - -**Goal:** Allow users to send NFTs (ERC-721 initially, ERC-1155 later) from the wallet app. - -**Approach:** Extend the existing Send flow with a Tokens/NFTs tab at the asset selection stage. - -**Current State:** - -- Backend endpoint exists: `POST /api/wallet/:address/transactions/transfer-nft` -- Frontend API call exists: `src/apps/wallet/queries/transferNFTRequest.ts` -- NFT types exist with `tokenType` and `quantity` fields -- Token transfer flow exists and will be extended - ---- - -## Flow Diagram - -``` - ┌─────────────────────────────────────┐ - │ Search Stage │ - │ (Select Recipient - reuse) │ - └─────────────────┬───────────────────┘ - │ - ┌─────────────────▼───────────────────┐ - │ Token Stage │ - │ ┌─────────────┬─────────────┐ │ - │ │ Tokens │ NFTs │ │ - │ │ Tab │ Tab │ │ - │ └──────┬──────┴──────┬──────┘ │ - └─────────┼─────────────┼─────────────┘ - │ │ - ┌───────────────▼──┐ ┌─────▼────────────────┐ - │ Amount Stage │ │ SKIP (ERC-721) │ - │ (Token only) │ │ or Amount (1155) │ - └────────┬─────────┘ └──────────┬───────────┘ - │ │ - └───────────┬─────────────┘ - │ - ┌──────────────▼──────────────┐ - │ Confirm Stage │ - │ (Adapt for Token vs NFT) │ - └──────────────┬──────────────┘ - │ - ┌──────────────▼──────────────┐ - │ Processing / Broadcasting │ - │ (reuse) │ - └──────────────┬──────────────┘ - │ - ┌──────────────▼──────────────┐ - │ Success / Error │ - │ (Adapt for Token vs NFT) │ - └─────────────────────────────┘ -``` - ---- - -## Phase 1: ERC-721 Implementation - -### Step 1: Redux State Changes - -**File:** `src/store/wallet/index.ts` - -```typescript -// Add to imports -import { NFT } from '../../apps/wallet/types'; - -// Update WalletState type -export type WalletState = { - // ... existing fields - nft: NFT | null; // NEW -}; - -// Update initialState -const initialState: WalletState = { - // ... existing fields - nft: null, -}; - -// Add to SagaActionTypes -export enum SagaActionTypes { - // ... existing - TransferNft = 'wallet/saga/transferNft', // NEW -} - -// Add new reducer in slice.reducers -setNft: (state, action: PayloadAction) => { - state.nft = action.payload; -}, - -// Update reset reducer -reset: (state) => { - // ... existing resets - state.nft = initialState.nft; -}, - -// Export new action -export const transferNft = createAction(SagaActionTypes.TransferNft); -export const { setNft } = slice.actions; // Add to exports -``` - -**File:** `src/store/wallet/selectors.ts` - -```typescript -// Add selector -export const nftSelector = (state: RootState) => state.wallet.nft; -``` - ---- - -### Step 2: Saga Changes - -**File:** `src/store/wallet/saga.ts` - -```typescript -// Add imports -import { transferNFTRequest } from '../../apps/wallet/queries/transferNFTRequest'; -import { nftSelector } from './selectors'; -import { setNft, transferNft } from '.'; -import { NFT } from '../../apps/wallet/types'; - -// Modify handleNext - skip Amount stage when NFT is selected -function* handleNext() { - const stage: SendStage = yield select(sendStageSelector); - const nft: NFT | null = yield select(nftSelector); - - switch (stage) { - case SendStage.Search: { - const recipient = yield select(recipientSelector); - if (recipient) { - yield put(setSendStage(SendStage.Token)); - } - break; - } - case SendStage.Token: { - const token = yield select(tokenSelector); - if (nft) { - // NFT selected - skip Amount, go straight to Confirm - yield put(setSendStage(SendStage.Confirm)); - } else if (token) { - // Token selected - go to Amount - yield put(setSendStage(SendStage.Amount)); - } - break; - } - case SendStage.Amount: { - const amount = yield select(amountSelector); - if (amount) { - yield put(setSendStage(SendStage.Confirm)); - } - break; - } - } -} - -// Modify handlePrevious - handle NFT flow -function* handlePrevious() { - const stage: SendStage = yield select(sendStageSelector); - const nft: NFT | null = yield select(nftSelector); - - switch (stage) { - case SendStage.Confirm: - if (nft) { - // NFT flow - go back to Token selection (skip Amount) - yield put(setNft(null)); - yield put(setSendStage(SendStage.Token)); - } else { - // Token flow - go back to Amount - yield put(setAmount(null)); - yield put(setSendStage(SendStage.Amount)); - } - break; - case SendStage.Amount: - yield put(setToken(null)); - yield put(setSendStage(SendStage.Token)); - break; - case SendStage.Token: - yield put(setRecipient(null)); - yield put(setNft(null)); // Clear NFT too - yield put(setToken(null)); - yield put(setSendStage(SendStage.Search)); - break; - case SendStage.Search: - break; - } -} - -// Add new saga for NFT transfer -function* handleTransferNft() { - const stage: SendStage = yield select(sendStageSelector); - - try { - if (stage === SendStage.Confirm) { - const recipient: Recipient = yield select(recipientSelector); - const selectedWallet: string | undefined = yield select(selectedWalletAddressSelector); - const nft: NFT = yield select(nftSelector); - - if (recipient && nft && selectedWallet) { - yield put(setSendStage(SendStage.Processing)); - - const result = yield call(() => - transferNFTRequest(selectedWallet, recipient.publicAddress, nft.id, nft.collectionAddress) - ); - - if (result.transactionHash) { - yield put(setSendStage(SendStage.Broadcasting)); - // Note: NFT doesn't have chainId currently - may need to add or use default - const receipt: TxReceiptResponse = yield call(() => - queryClient.fetchQuery(txReceiptQueryOptions(result.transactionHash)) - ); - yield put(setTxReceipt(receipt)); - if (receipt.status === 'confirmed') { - yield put(setSendStage(SendStage.Success)); - } else { - yield put(setSendStage(SendStage.Error)); - } - } else { - yield put(setSendStage(SendStage.Error)); - } - } - } - } catch (e) { - if (isWalletAPIError(e)) { - yield put(setErrorCode(e.response.body.code)); - } - yield put(setError(true)); - yield put(setSendStage(SendStage.Error)); - } -} - -// Register in saga -export function* saga() { - yield fork(initializeWalletSaga); - yield takeLatest(nextStage.type, handleNext); - yield takeLatest(previousStage.type, handlePrevious); - yield takeLatest(transferToken.type, handleTransferToken); - yield takeLatest(transferNft.type, handleTransferNft); // NEW -} -``` - ---- - -### Step 3: Token Selection with Tabs - -**File:** `src/apps/wallet/send/token-select/wallet-token-select.tsx` - -```typescript -import { useDispatch, useSelector } from 'react-redux'; -import { nextStage, previousStage, setToken, setNft } from '../../../../store/wallet'; -import { SendHeader } from '../components/send-header'; -import { Input } from '@zero-tech/zui/components'; -import { IconSearchMd } from '@zero-tech/zui/icons'; -import { useState } from 'react'; -import { useBalancesQuery } from '../../queries/useBalancesQuery'; -import { useNFTsQuery } from '../../queries/useNFTsQuery'; -import { Token } from '../../tokens/token'; -import { NFTTile } from '../../nfts/nft/nft-tile'; // or create simplified version -import { TokenBalance, NFT } from '../../types'; -import { recipientSelector, selectedWalletSelector } from '../../../../store/wallet/selectors'; -import { truncateAddress } from '../../utils/address'; - -import styles from './wallet-token-select.module.scss'; - -type AssetTab = 'tokens' | 'nfts'; - -export const WalletTokenSelect = () => { - const dispatch = useDispatch(); - const [assetQuery, setAssetQuery] = useState(''); - const [activeTab, setActiveTab] = useState('tokens'); - const selectedWallet = useSelector(selectedWalletSelector); - const recipient = useSelector(recipientSelector); - - const { data: balancesData } = useBalancesQuery(selectedWallet.address); - const { nfts } = useNFTsQuery(selectedWallet.address); - - const filteredTokens = balancesData?.tokens?.filter((asset) => - asset.name.toLowerCase().includes(assetQuery.toLowerCase()) - ); - - const filteredNfts = nfts?.filter( - (nft) => - nft.metadata?.name?.toLowerCase().includes(assetQuery.toLowerCase()) || - nft.collectionName?.toLowerCase().includes(assetQuery.toLowerCase()) - ); - - const handleTokenClick = (token: TokenBalance) => { - dispatch(setNft(null)); // Clear any selected NFT - dispatch(setToken(token)); - dispatch(nextStage()); - }; - - const handleNftClick = (nft: NFT) => { - dispatch(setToken(null)); // Clear any selected token - dispatch(setNft(nft)); - dispatch(nextStage()); // Saga will skip Amount stage - }; - - const handleBack = () => { - dispatch(previousStage()); - }; - - return ( -
- - -
- {/* Tab switcher */} -
- - -
- -
- } - /> -
- -
- {activeTab === 'tokens' && ( -
-
Tokens
-
- {filteredTokens?.map((asset) => ( - handleTokenClick(asset)} - /> - ))} - {filteredTokens?.length === 0 &&
No tokens found
} -
-
- )} - - {activeTab === 'nfts' && ( -
-
NFTs
-
- {filteredNfts?.map((nft) => ( - handleNftClick(nft)} - /> - ))} - {filteredNfts?.length === 0 &&
No NFTs found
} -
-
- )} -
-
- - {/* Footer showing recipient - same as before */} -
{/* ... existing footer code ... */}
-
- ); -}; - -// Simple NFT item for selection list -const NFTSelectItem = ({ nft, onClick }: { nft: NFT; onClick: () => void }) => { - return ( - - ); -}; -``` - -**File:** `src/apps/wallet/send/token-select/wallet-token-select.module.scss` - -Add styles for tabs and NFT grid: - -```scss -.tabSwitcher { - display: flex; - gap: 8px; - margin-bottom: 16px; -} - -.tab { - flex: 1; - padding: 12px; - border: 1px solid var(--color-border); - background: transparent; - color: var(--color-text-secondary); - cursor: pointer; - border-radius: 8px; - transition: all 0.2s; - - &:hover { - background: var(--color-background-secondary); - } -} - -.tabActive { - background: var(--color-primary); - color: var(--color-text-primary); - border-color: var(--color-primary); -} - -.nftGrid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 12px; -} - -.nftSelectItem { - display: flex; - flex-direction: column; - padding: 8px; - border: 1px solid var(--color-border); - border-radius: 8px; - background: transparent; - cursor: pointer; - text-align: left; - - &:hover { - background: var(--color-background-secondary); - } -} - -.nftImage { - width: 100%; - aspect-ratio: 1; - object-fit: cover; - border-radius: 4px; -} - -.nftImagePlaceholder { - width: 100%; - aspect-ratio: 1; - background: var(--color-background-secondary); - border-radius: 4px; -} - -.nftInfo { - margin-top: 8px; -} - -.nftName { - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.nftCollection { - font-size: 12px; - color: var(--color-text-secondary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.emptyState { - padding: 24px; - text-align: center; - color: var(--color-text-secondary); -} -``` - ---- - -### Step 4: Review Transfer Component - -**File:** `src/apps/wallet/send/review-transfer/wallet-review-transfer.tsx` - -Update to handle both Token and NFT: - -```typescript -import { useDispatch, useSelector } from 'react-redux'; -import { previousStage, transferToken, transferNft } from '../../../../store/wallet'; -import styles from './wallet-review-transfer.module.scss'; -import { SendHeader } from '../components/send-header'; -import { amountSelector, recipientSelector, tokenSelector, nftSelector } from '../../../../store/wallet/selectors'; -import { MatrixAvatar } from '../../../../components/matrix-avatar'; -import { TokenIcon } from '../../components/token-icon/token-icon'; -import { FormattedNumber } from '../../components/formatted-number/formatted-number'; -import { Button } from '../../components/button/button'; -import { IconChevronRightDouble } from '@zero-tech/zui/icons'; - -export const WalletReviewTransfer = () => { - const dispatch = useDispatch(); - const recipient = useSelector(recipientSelector); - const token = useSelector(tokenSelector); - const amount = useSelector(amountSelector); - const nft = useSelector(nftSelector); - - const isNftTransfer = nft !== null; - - const handleBack = () => { - dispatch(previousStage()); - }; - - const handleConfirm = () => { - if (isNftTransfer) { - dispatch(transferNft()); - } else { - dispatch(transferToken()); - } - }; - - return ( -
- - -
-
-
Confirm transaction with
- -
{recipient?.primaryZid || recipient?.name}
-
{recipient?.publicAddress}
-
- - {isNftTransfer ? ( - // NFT Transfer Details -
-
- {nft.imageUrl ? ( - {nft.metadata?.name} - ) : ( -
- )} -
-
-
{nft.metadata?.name || 'Unnamed NFT'}
-
{nft.collectionName}
-
Token ID: {nft.id}
-
-
- ) : ( - // Token Transfer Details (existing) -
-
- -
{token.name}
-
- -
-
- -
- -
- -
- -
{token.name}
-
- -
-
-
- )} -
- -
-
Review the above before confirming.
-
Once made, your transaction is irreversible.
- -
-
- ); -}; -``` - -Add NFT-specific styles to `wallet-review-transfer.module.scss`. - ---- - -### Step 5: Success Screen - -**File:** `src/apps/wallet/send/success/wallet-transfer-success.tsx` - -Update to handle both Token and NFT: - -```typescript -// Add to imports -import { nftSelector } from '../../../../store/wallet/selectors'; - -// In component -const nft = useSelector(nftSelector); -const isNftTransfer = nft !== null; - -// In JSX, conditionally render token hero or NFT hero -{ - isNftTransfer ? ( -
- {nft.imageUrl && {nft.metadata?.name}} -
{nft.metadata?.name || 'NFT'}
-
{nft.collectionName}
-
- ) : ( - // Existing token hero -
{/* ... existing token code ... */}
- ); -} -``` - ---- - -## Implementation Checklist - -### Phase 1: ERC-721 - -- [x] **Step 1: Redux State** ✅ - - - [x] Add `nft: NFT | null` to WalletState in `src/store/wallet/index.ts` - - [x] Add `setNft` reducer - - [x] Add `TransferNft` to SagaActionTypes - - [x] Add `transferNft` saga action - - [x] Update `reset` reducer to clear nft - - [x] Add `nftSelector` to `src/store/wallet/selectors.ts` - -- [x] **Step 2: Saga** ✅ - - - [x] Update `handleNext` to skip Amount stage for NFTs - - [x] Update `handlePrevious` to handle NFT flow - - [x] Add `handleTransferNft` saga - - [x] Register `transferNft` in root saga - -- [x] **Step 3: Token Selection with Tabs** ✅ - - - [x] Add tab state and switcher UI - - [x] Import and use `useNFTsQuery` - - [x] Add NFT grid with clickable items - - [x] Wire up `handleNftClick` to dispatch `setNft` and `nextStage` - - [x] Add SCSS for tabs and NFT grid - -- [x] **Step 4: Review Screen** ✅ - - - [x] Import `nftSelector` - - [x] Add conditional rendering for NFT vs Token - - [x] Wire up confirm button to dispatch `transferNft` for NFTs - - [x] Add NFT-specific styles - -- [x] **Step 5: Success Screen** ✅ - - - [x] Import `nftSelector` - - [x] Add conditional rendering for NFT success view - - [x] Add NFT-specific styles - -- [x] **Testing** ✅ - - [x] Test token flow still works - - [x] Test NFT selection skips Amount stage - - [x] Test back navigation works correctly (NFT flow goes Confirm → Token) - - [ ] Test NFT transfer calls correct API (requires E2E/integration test) - - [ ] Test reset clears NFT state (covered by existing reset logic) - ---- - -## Phase 2: ERC-1155 (Future) - -### Backend Changes Required - -- Create `send-erc-1155.ts` transaction builder using `safeTransferFrom(from, to, id, amount, data)` -- Update endpoint to accept `amount` parameter for ERC-1155 - -### Frontend Changes Required - -- Add `nftAmount: string | null` to Redux state -- Show Amount stage for ERC-1155 tokens (check `nft.tokenType === 'ERC-1155'`) -- Validate amount <= nft.quantity -- Update `transferNFTRequest` to include amount parameter -- Update saga to pass amount for ERC-1155 - ---- - -## Questions Resolved - -1. **Entry Point:** Use existing Send flow with tabs - unified experience -2. **Amount for ERC-721:** Skip Amount stage (always transfer 1) -3. **Amount for ERC-1155:** Show Amount stage, validate against quantity (Phase 2) - -## Open Questions - -1. **Chain ID for NFTs:** Currently NFT type doesn't have chainId - may need to add or use default -2. **NFT loading state:** Show skeleton while NFTs load? -3. **Empty states:** What to show if user has no NFTs?