Quantum Fusion Network (QFN) asset management app - React 19 + TypeScript + polkadot-api
State Management:
- NEVER use
useReducer- useuseStateor context - Component state with
useState, shared state via Context - Server state via TanStack Query only
TypeScript:
- NEVER use
any- useunknownand narrow types - NEVER use type assertions (
as) - let types prove correctness - NEVER create redundant type definitions - leverage inference
- Prefer narrow types (literals, discriminated unions) over broad types
Architecture:
- Components are presentational - minimal logic
- Business logic in
lib/folder and custom hooks - Pure functions, immutability, early returns
- ALL exports through barrel files (
index.ts) - import from@/lib,@/hooks,@/components
Wallet State: WalletContext + useWallet hook
- Extension connection, account selection, auto-reconnect
- Persists to localStorage
Connection State: ConnectionContext + useConnectionStatus hook
- Creates polkadot-api client (NO separate chain.ts file)
- Connection status, auto-reconnect with query invalidation
Transaction State: TransactionContext + useTransaction hook
- Tracks: signing → broadcasting → inBlock → finalized/error
useTransactionToastsobserves and displays notifications
Server State: TanStack Query (30s stale, 5min GC)
lib/ - Business logic and utilities (all exported via lib/index.ts)
balance/- Balance conversion and formatting utilitiestoPlanck.ts- Convert human-readable to Planck units (bigint)fromPlanck.ts- Convert Planck units to human-readable stringformat.ts- formatBalance with locale, rounding, symbol supportconfig.ts- Balance constants and error codes
utils.ts- cn() for Tailwind class mergingwalletStorage.ts- localStorage persistence for wallet connectiontoastConfigs.ts- Transaction toast configurations (ToastConfig type)queryClient.ts- TanStack Query setuperrorParsing.ts/errorMessages.ts/transactionErrors.ts- Error handling
hooks/ - Custom React hooks (all exported via hooks/index.ts)
useConnectionStatus.ts- Creates polkadot-api client, manages connectionuseConnectionContext.ts- Access connection state and API clientuseWallet.ts- Core wallet connection logicuseWalletContext.ts- Access wallet stateuseTransactionManager.ts- Internal transaction lifecycle manager (used by TransactionProvider)useTransaction.ts- Execute transactions with lifecycle trackinguseTransactionContext.ts- Access transaction stateuseTransactionToasts.ts- Display transaction notificationsuseFee.ts- Calculate transaction fees with real-time estimation
contexts/ - React Context providers (exported via contexts/index.ts)
WalletContext.tsx- Wallet state provider (usesuseWallet)ConnectionContext.tsx- Connection state provider (usesuseConnectionStatus)TransactionContext.tsx- Transaction state provider (usesuseTransactionManager)
components/ - UI components (all exported via components/index.ts)
WalletConnector.tsx- Wallet connection UIAccountSelector.tsx- Account selection dropdownAccountDashboard.tsx- Account balance and faucet linkConnectionBanner.tsx- Connection status bannerFeeDisplay.tsx- Display transaction feeTransactionFormFooter.tsx- Common form footer with fee displayTransactionReview.tsx- Review transaction before signing (JSON preview)MutationError.tsx- Display mutation errorserror-boundaries/- Error boundary componentsAppErrorBoundary.tsx- Top-level error boundaryFeatureErrorBoundary.tsx- Feature-level error boundaryComponentErrorBoundary.tsx- Component-level error boundary
- Component uses mutation hook (or custom logic) with operation function
- On mutation trigger,
useTransaction().executeTransaction()is called - TransactionManager tracks lifecycle: idle → signing → broadcasting → inBlock → finalized
useTransactionToastsobserves transaction state and displays notifications- Cleanup after 500ms, queries invalidated on success
Conversion:
toPlanck(value: string, decimals: number): bigint- Convert "1.5" to 1500000000000000000nfromPlanck(value: bigint, decimals?: number): string- Convert 1500000000000000000n to "1.5"
Formatting:
formatBalance(value: string, options?: FormatBalanceOptions): string- Options:
symbol,displayDecimals,locale,roundingMode - Handles locale-specific formatting (commas, periods)
- Supports rounding modes: 'round', 'floor', 'ceil'
- Example:
formatBalance("1234.5678", { symbol: "QF", displayDecimals: 2 })→ "1,234.57 QF"
- Options:
Import from main barrel:
import { toPlanck, fromPlanck, formatBalance } from '@/lib'useFee Hook:
- Real-time fee estimation with 500ms debounce (uses
useDeferredValue) - Call
transaction.getPaymentInfo(address, { at: 'best' }) - Returns:
{ fee, isLoading, error }
Usage:
import { useFee } from '@/hooks'
const { mutation, transaction } = useMyMutation({ ... })
const feeState = useFee(transaction, selectedAccount?.address)
// Use in footer
<TransactionFormFooter feeState={feeState} {...} />TransactionFormFooter - Consistent footer for all transaction forms:
<TransactionFormFooter
feeState={{ fee, isLoading, error }}
isDisabled={!isFormValid}
isPending={mutation.isPending}
actionText="Submit Transaction"
variant="default" // or "destructive"
/>TransactionReview - JSON preview for forms:
<TransactionReview
data={formData}
variant="default" // or "destructive"
/>AccountDashboard - Native balance + faucet link:
<AccountDashboard /> // Shows QF balance, faucet buttonStandard Form Layout:
<div className="space-y-6">
<AccountDashboard />
<Card>
<form onSubmit={handleSubmit}>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div className="lg:col-span-2">{/* Form fields */}</div>
<div className="lg:col-span-1">
<TransactionReview data={formData} />
</div>
</div>
<TransactionFormFooter feeState={feeState} {...} />
</form>
</Card>
</div>Network: QF Network testnet wss://test.qfnetwork.xyz
polkadot-api:
- Descriptors in
.papi/descriptors(auto-generated viapapicommand) - Metadata in
.papi/metadata/*.scalefiles - Client created in
useConnectionStatushook
- Toast duration: 30s (Sonner)
- TanStack Query: 30s stale time, 5min garbage collection
- Wallet auto-reconnects on page load if previously connected
- Error boundaries at app/feature/component levels
- All exports through barrel files for clean imports
- Balance decimals: Default 18 for QF native token
- Query refetch: 10s for balances, 5min stale time for metadata
Pre-built infrastructure:
- ✅ Wallet connection (WalletContext + useWallet)
- ✅ Chain connection (ConnectionContext + useConnectionStatus)
- ✅ Transaction management (TransactionContext + useTransaction)
- ✅ Fee estimation (useFee + FeeDisplay)
- ✅ Balance utilities (toPlanck, fromPlanck, formatBalance)
- ✅ Shared form components (TransactionFormFooter, TransactionReview, AccountDashboard)
- ✅ Error handling (errorParsing, transactionErrors, error boundaries)
- ✅ UI components (shadcn/ui + generic components)
To generate features:
- Create wizard-config.json (or use wizard)
- Open Cursor Composer
- Type: "Generate my app based on wizard-config.json"
- Run:
pnpm install && pnpm papi && pnpm dev
Never regenerate infrastructure files. They're tested and follow established patterns.
Generate from config:
# Generate prompts from wizard config
pnpm tsx workflow/generate-prompts.ts
# Or ask Claude directly
"Add [feature] based on wizard-config.json"Add features iteratively:
"Add NFT minting capability"
"Add freeze/thaw operations for assets"
Claude will:
- Load relevant skills from
.claude/skills/ - Generate operation functions and components
- Update App.tsx navigation
- Use template infrastructure (balance utilities, shared components, hooks)