diff --git a/src/components/wallet/wallet-card-carousel.tsx b/src/components/wallet/wallet-card-carousel.tsx index 3da8b3e44..40b95161c 100644 --- a/src/components/wallet/wallet-card-carousel.tsx +++ b/src/components/wallet/wallet-card-carousel.tsx @@ -8,7 +8,13 @@ import { useWalletTheme } from '@/hooks/useWalletTheme'; import { useChainIconUrls } from '@/hooks/useChainIconUrls'; import { cn } from '@/lib/utils'; import type { Wallet, ChainType } from '@/stores'; -import { IconWallet, IconPlus } from '@tabler/icons-react'; +import { IconWallet, IconPlus, IconDotsVertical, IconSearch, IconReceipt } from '@tabler/icons-react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import 'swiper/css'; import 'swiper/css/effect-cards'; @@ -26,6 +32,8 @@ interface WalletCardCarouselProps { onOpenSettings?: (walletId: string) => void; onOpenWalletList?: () => void; onAddWallet?: () => void; + onOpenAddressBalance?: () => void; + onOpenAddressTransactions?: () => void; className?: string; } @@ -45,6 +53,8 @@ export function WalletCardCarousel({ onOpenSettings, onOpenWalletList, onAddWallet, + onOpenAddressBalance, + onOpenAddressTransactions, className, }: WalletCardCarouselProps) { const swiperRef = useRef(null); @@ -111,15 +121,40 @@ export function WalletCardCarousel({ )} - {/* 右上角:添加钱包 */} - {onAddWallet && ( - - )} + {/* 右上角:添加钱包 + 更多菜单 */} +
+ {onAddWallet && ( + + )} + {(onOpenAddressBalance || onOpenAddressTransactions) && ( + + + + + + {onOpenAddressBalance && ( + + + 地址余额查询 + + )} + {onOpenAddressTransactions && ( + + + 地址交易查询 + + )} + + + )} +
{ + if (address.trim()) { + setQueryAddress(address.trim()) + setQueryChain(selectedChain) + } + }, [address, selectedChain]) + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSearch() + } + }, + [handleSearch] + ) + + const evmChains = enabledChains.filter((c) => c.type === 'evm') + const otherChains = enabledChains.filter((c) => c.type !== 'evm') + + return ( +
+ + +
+ {/* Chain Selector */} +
+ + +
+ + {/* Address Input */} +
+ +
+ setAddress(e.target.value)} + onKeyDown={handleKeyDown} + className="flex-1 font-mono text-sm" + /> + +
+
+ + {/* Result */} + {queryAddress && ( + + + {data?.error ? ( +
+ +
+
{t('common:addressLookup.error')}
+
{data.error}
+
+
+ ) : data?.balance ? ( +
+
+ +
+
+
+ {data.balance.amount.toFormatted()} {data.balance.symbol} +
+
+ {t('common:addressLookup.onChain', { + chain: enabledChains.find((c) => c.id === queryChain)?.name ?? queryChain, + })} +
+
+
+ ) : isLoading ? ( +
+ +
+ ) : null} +
+
+ )} + + {/* Debug Info (DEV only) */} + {import.meta.env.DEV && queryAddress && ( +
+
Chain: {queryChain}
+
Address: {queryAddress}
+
+ )} +
+
+ ) +} diff --git a/src/pages/address-transactions/index.tsx b/src/pages/address-transactions/index.tsx new file mode 100644 index 000000000..1c8c1b8fa --- /dev/null +++ b/src/pages/address-transactions/index.tsx @@ -0,0 +1,165 @@ +import { useState, useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useNavigation } from '@/stackflow' +import { PageHeader } from '@/components/layout/page-header' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Card, CardContent } from '@/components/ui/card' +import { useEnabledChains } from '@/stores' +import { IconSearch, IconExternalLink, IconInfoCircle } from '@tabler/icons-react' + +export function AddressTransactionsPage() { + const { t } = useTranslation(['common', 'wallet']) + const { goBack } = useNavigation() + const enabledChains = useEnabledChains() + + const [selectedChain, setSelectedChain] = useState('ethereum') + const [address, setAddress] = useState('') + + const selectedChainConfig = useMemo( + () => enabledChains.find((c) => c.id === selectedChain), + [enabledChains, selectedChain] + ) + + const explorerUrl = useMemo(() => { + if (!selectedChainConfig?.explorer || !address.trim()) return null + + const { queryAddress, url } = selectedChainConfig.explorer + if (queryAddress) { + return queryAddress.replace(':address', address.trim()) + } + // Fallback: common explorer patterns + if (url.includes('etherscan')) { + return `${url}/address/${address.trim()}` + } + if (url.includes('bscscan')) { + return `${url}/address/${address.trim()}` + } + if (url.includes('tronscan')) { + return `${url}/#/address/${address.trim()}` + } + return `${url}/address/${address.trim()}` + }, [selectedChainConfig, address]) + + const handleOpenExplorer = useCallback(() => { + if (explorerUrl) { + window.open(explorerUrl, '_blank', 'noopener,noreferrer') + } + }, [explorerUrl]) + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && explorerUrl) { + handleOpenExplorer() + } + }, + [explorerUrl, handleOpenExplorer] + ) + + const evmChains = enabledChains.filter((c) => c.type === 'evm') + const otherChains = enabledChains.filter((c) => c.type !== 'evm') + + return ( +
+ + +
+ {/* Chain Selector */} +
+ + +
+ + {/* Address Input */} +
+ +
+ setAddress(e.target.value)} + onKeyDown={handleKeyDown} + className="flex-1 font-mono text-sm" + /> + +
+
+ + {/* Info Card */} + + +
+ +
+

+ {t('common:addressLookup.explorerHint')} +

+ {selectedChainConfig?.explorer?.url && ( + + )} +
+
+
+
+ + {/* Quick Links */} + {address.trim() && explorerUrl && ( + + + + + + )} +
+
+ ) +} diff --git a/src/queries/index.ts b/src/queries/index.ts index b8dcf3d0f..458077d41 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -58,3 +58,9 @@ export { securityPasswordQueryKeys, type SecurityPasswordQueryResult, } from './use-security-password-query' + +export { + useAddressBalanceQuery, + addressBalanceKeys, + type AddressBalanceResult, +} from './use-address-balance-query' diff --git a/src/queries/use-address-balance-query.ts b/src/queries/use-address-balance-query.ts new file mode 100644 index 000000000..cc87c9032 --- /dev/null +++ b/src/queries/use-address-balance-query.ts @@ -0,0 +1,63 @@ +import { useQuery } from '@tanstack/react-query' +import { getAdapterRegistry, setupAdapters } from '@/services/chain-adapter' +import { chainConfigStore, chainConfigSelectors } from '@/stores' +import type { Balance } from '@/services/chain-adapter/types' + +let adaptersInitialized = false +function ensureAdapters() { + if (!adaptersInitialized) { + setupAdapters() + adaptersInitialized = true + } +} + +export const addressBalanceKeys = { + all: ['addressBalance'] as const, + query: (chainId: string, address: string) => ['addressBalance', chainId, address] as const, +} + +export interface AddressBalanceResult { + balance: Balance | null + error: string | null +} + +/** + * Query hook for fetching balance of any address on any chain + */ +export function useAddressBalanceQuery(chainId: string, address: string, enabled = true) { + return useQuery({ + queryKey: addressBalanceKeys.query(chainId, address), + queryFn: async (): Promise => { + if (!chainId || !address) { + return { balance: null, error: 'Missing chain or address' } + } + + try { + ensureAdapters() + + const state = chainConfigStore.state + const chainConfig = chainConfigSelectors.getChainById(state, chainId) + if (!chainConfig) { + return { balance: null, error: `Unknown chain: ${chainId}` } + } + + const registry = getAdapterRegistry() + registry.setChainConfigs([chainConfig]) + + const adapter = registry.getAdapter(chainId) + if (!adapter) { + return { balance: null, error: `No adapter for chain: ${chainId}` } + } + + const balance = await adapter.asset.getNativeBalance(address) + return { balance, error: null } + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + return { balance: null, error: message } + } + }, + enabled: enabled && !!chainId && !!address, + staleTime: 30 * 1000, + gcTime: 5 * 60 * 1000, + }) +} diff --git a/src/stackflow/activities/AddressBalanceActivity.tsx b/src/stackflow/activities/AddressBalanceActivity.tsx new file mode 100644 index 000000000..eb6a87444 --- /dev/null +++ b/src/stackflow/activities/AddressBalanceActivity.tsx @@ -0,0 +1,11 @@ +import type { ActivityComponentType } from '@stackflow/react' +import { AppScreen } from '@stackflow/plugin-basic-ui' +import { AddressBalancePage } from '@/pages/address-balance' + +export const AddressBalanceActivity: ActivityComponentType = () => { + return ( + + + + ) +} diff --git a/src/stackflow/activities/AddressTransactionsActivity.tsx b/src/stackflow/activities/AddressTransactionsActivity.tsx new file mode 100644 index 000000000..f6cb53b3f --- /dev/null +++ b/src/stackflow/activities/AddressTransactionsActivity.tsx @@ -0,0 +1,11 @@ +import type { ActivityComponentType } from '@stackflow/react' +import { AppScreen } from '@stackflow/plugin-basic-ui' +import { AddressTransactionsPage } from '@/pages/address-transactions' + +export const AddressTransactionsActivity: ActivityComponentType = () => { + return ( + + + + ) +} diff --git a/src/stackflow/activities/tabs/WalletTab.tsx b/src/stackflow/activities/tabs/WalletTab.tsx index a9dec5738..9f6dc4f5b 100644 --- a/src/stackflow/activities/tabs/WalletTab.tsx +++ b/src/stackflow/activities/tabs/WalletTab.tsx @@ -128,6 +128,16 @@ export function WalletTab() { push("WalletListJob", {}); }, [push]); + // 地址余额查询 + const handleOpenAddressBalance = useCallback(() => { + push("AddressBalanceActivity", {}); + }, [push]); + + // 地址交易查询 + const handleOpenAddressTransactions = useCallback(() => { + push("AddressTransactionsActivity", {}); + }, [push]); + // 交易点击 const handleTransactionClick = useCallback( (tx: TransactionInfo) => { @@ -166,6 +176,8 @@ export function WalletTab() { onOpenSettings={handleOpenWalletSettings} onOpenWalletList={handleOpenWalletList} onAddWallet={handleAddWallet} + onOpenAddressBalance={handleOpenAddressBalance} + onOpenAddressTransactions={handleOpenAddressTransactions} /> {/* 快捷操作按钮 - 颜色跟随主题 */} diff --git a/src/stackflow/stackflow.ts b/src/stackflow/stackflow.ts index cd42dd29e..065ec9f45 100644 --- a/src/stackflow/stackflow.ts +++ b/src/stackflow/stackflow.ts @@ -31,6 +31,8 @@ import { SettingsWalletChainsActivity } from './activities/SettingsWalletChainsA import { SettingsStorageActivity } from './activities/SettingsStorageActivity'; import { SettingsSourcesActivity } from './activities/SettingsSourcesActivity'; import { MiniappDetailActivity } from './activities/MiniappDetailActivity'; +import { AddressBalanceActivity } from './activities/AddressBalanceActivity'; +import { AddressTransactionsActivity } from './activities/AddressTransactionsActivity'; import { ChainSelectorJob, WalletRenameJob, @@ -94,6 +96,8 @@ export const { Stack, useFlow, useStepFlow, activities } = stackflow({ WelcomeActivity: '/welcome', SettingsSourcesActivity: '/settings/sources', MiniappDetailActivity: '/miniapp/:appId/detail', + AddressBalanceActivity: '/address-balance', + AddressTransactionsActivity: '/address-transactions', ChainSelectorJob: '/job/chain-selector', WalletRenameJob: '/job/wallet-rename/:walletId', WalletDeleteJob: '/job/wallet-delete/:walletId', @@ -155,6 +159,8 @@ export const { Stack, useFlow, useStepFlow, activities } = stackflow({ WelcomeActivity, SettingsSourcesActivity, MiniappDetailActivity, + AddressBalanceActivity, + AddressTransactionsActivity, ChainSelectorJob, WalletRenameJob, WalletDeleteJob,