Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6318f70
feat: add address balance and transaction lookup pages
Gaubee Jan 4, 2026
dbf2a98
refactor: 配置驱动的链服务架构
Gaubee Jan 4, 2026
3000d68
feat: 添加数据库版本检测和迁移引导
Gaubee Jan 4, 2026
5427019
fix: 在 AppInitializer 中等待链配置初始化完成
Gaubee Jan 4, 2026
cc8e9e7
fix: MigrationRequiredView 移除对 useFlow 的依赖
Gaubee Jan 4, 2026
ddb9ab0
fix: 将 I18nextProvider 移到 AppInitializer 外层
Gaubee Jan 4, 2026
0b15822
feat: 添加外部数据类型安全验证
Gaubee Jan 4, 2026
293ed7e
feat: 钱包存储版本检测
Gaubee Jan 4, 2026
5942ce3
fix: MigrationRequiredView 直接跳转到 clear.html 进行清理
Gaubee Jan 4, 2026
9bd5804
fix: 区分全新安装和旧版升级
Gaubee Jan 4, 2026
f0467bd
fix: 修复 hasStoredData 检查 - enabledMap 是空对象而非 null
Gaubee Jan 4, 2026
e9834cb
feat(bioforest): 使用 Zod Schema 验证 API 响应
Gaubee Jan 4, 2026
944c406
fix(bioforest): 修复 AssetService 构造函数类型不匹配
Gaubee Jan 4, 2026
6932e44
fix: 导出 chainConfigService 并修正方法名
Gaubee Jan 4, 2026
abc7bd9
Merge remote-tracking branch 'origin/feat/address-lookup' into refact…
Gaubee Jan 4, 2026
5e34f92
feat: 地址交易查询使用内部 chainProvider
Gaubee Jan 4, 2026
1f8edd4
fix: useAddressBalanceQuery 使用 registerChainConfigs 替代已移除的 setChainCo…
Gaubee Jan 4, 2026
49f683f
fix: useAddressTransactionsQuery 同样需要初始化 adapter 和注册链配置
Gaubee Jan 4, 2026
1bbf901
fix: Schema 使用 nullish() 允许 result 为 null
Gaubee Jan 4, 2026
8f0cec5
feat: 添加 supportsTransactionHistory 到 ITransactionService
Gaubee Jan 4, 2026
753b4a5
feat: API 多 Provider 配置架构
Gaubee Jan 4, 2026
fbca40f
feat: ChainProvider 聚合模式实现
Gaubee Jan 4, 2026
6a61f1a
test: 添加 ChainProvider 单元测试
Gaubee Jan 4, 2026
365115c
refactor: 完成 ChainProvider 架构迁移
Gaubee Jan 4, 2026
d5495c1
test: 补充 WrappedProvider 单元测试
Gaubee Jan 4, 2026
f18304c
test: 添加地址交易查询页面测试
Gaubee Jan 4, 2026
68d9f56
test: 添加 ChainProvider 集成测试
Gaubee Jan 4, 2026
6350a92
fix: 使用 Blockscout API 替代 Etherscan v2
Gaubee Jan 4, 2026
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
418 changes: 230 additions & 188 deletions public/configs/default-chains.json

Large diffs are not rendered by default.

63 changes: 63 additions & 0 deletions src/components/common/migration-required-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { IconAlertTriangle, IconTrash } from '@tabler/icons-react'
import { Button } from '@/components/ui/button'

/**
* 数据库迁移引导组件
* 当检测到旧版数据需要迁移时显示
*
* 注意:此组件在 Stackflow context 外部渲染,不能使用 useFlow()
*/
export function MigrationRequiredView() {
const { t } = useTranslation(['settings', 'common'])
const [isClearing, setIsClearing] = useState(false)

const handleClearData = () => {
setIsClearing(true)
// 跳转到 clear.html 进行清理
const baseUri = import.meta.env.BASE_URL || '/'
window.location.href = `${baseUri}clear.html`
}

return (
<div className="flex min-h-screen flex-col items-center justify-center bg-background p-6">
<div className="flex flex-col items-center gap-6 text-center max-w-sm">
<div className="bg-destructive/10 flex size-20 items-center justify-center rounded-full">
<IconAlertTriangle className="text-destructive size-10" />
</div>

<div className="space-y-2">
<h1 className="text-xl font-bold">
{t('settings:storage.migrationRequired', '数据需要迁移')}
</h1>
<p className="text-muted-foreground text-sm">
{t(
'settings:storage.migrationDesc',
'检测到旧版本数据格式,需要清空本地数据库后才能继续使用。您的助记词和私钥不会受到影响,但需要重新导入钱包。'
)}
</p>
</div>

{/* Warning List */}
<div className="bg-destructive/5 rounded-lg p-4 w-full">
<ul className="text-destructive space-y-2 text-sm text-left">
<li>• {t('settings:clearData.item1', '所有钱包数据将被删除')}</li>
<li>• {t('settings:clearData.item2', '所有设置将恢复默认')}</li>
<li>• {t('settings:clearData.item3', '应用将重新启动')}</li>
</ul>
</div>

<Button
variant="destructive"
onClick={handleClearData}
className="w-full gap-2"
disabled={isClearing}
>
<IconTrash className="size-4" />
{t('settings:clearData.confirm', '确认清空')}
</Button>
</div>
</div>
)
}
55 changes: 45 additions & 10 deletions src/components/wallet/wallet-card-carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,6 +32,8 @@ interface WalletCardCarouselProps {
onOpenSettings?: (walletId: string) => void;
onOpenWalletList?: () => void;
onAddWallet?: () => void;
onOpenAddressBalance?: () => void;
onOpenAddressTransactions?: () => void;
className?: string;
}

Expand All @@ -45,6 +53,8 @@ export function WalletCardCarousel({
onOpenSettings,
onOpenWalletList,
onAddWallet,
onOpenAddressBalance,
onOpenAddressTransactions,
className,
}: WalletCardCarouselProps) {
const swiperRef = useRef<SwiperType | null>(null);
Expand Down Expand Up @@ -111,15 +121,40 @@ export function WalletCardCarousel({
</button>
)}

{/* 右上角:添加钱包 */}
{onAddWallet && (
<button
onClick={onAddWallet}
className="bg-primary text-primary-foreground hover:bg-primary/90 absolute top-0 right-4 z-10 flex items-center justify-center rounded-full p-1.5 backdrop-blur-sm transition-colors"
>
<IconPlus className="size-4" />
</button>
)}
{/* 右上角:添加钱包 + 更多菜单 */}
<div className="absolute top-0 right-4 z-10 flex items-center gap-1.5">
{onAddWallet && (
<button
onClick={onAddWallet}
className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center justify-center rounded-full p-1.5 backdrop-blur-sm transition-colors"
>
<IconPlus className="size-4" />
</button>
)}
{(onOpenAddressBalance || onOpenAddressTransactions) && (
<DropdownMenu>
<DropdownMenuTrigger
className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center justify-center rounded-full p-1.5 backdrop-blur-sm transition-colors"
>
<IconDotsVertical className="size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={8}>
{onOpenAddressBalance && (
<DropdownMenuItem onClick={onOpenAddressBalance}>
<IconSearch className="size-4" />
地址余额查询
</DropdownMenuItem>
)}
{onOpenAddressTransactions && (
<DropdownMenuItem onClick={onOpenAddressTransactions}>
<IconReceipt className="size-4" />
地址交易查询
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>

<Swiper
modules={[EffectCards]}
Expand Down
16 changes: 8 additions & 8 deletions src/frontend-main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ export function startFrontendMain(rootElement: HTMLElement): void {
createRoot(rootElement).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<ServiceProvider>
<MigrationProvider>
<AppInitializer>
<I18nextProvider i18n={i18n}>
<I18nextProvider i18n={i18n}>
<ServiceProvider>
<MigrationProvider>
<AppInitializer>
<IconProvidersWrapper>
<StackflowApp />
</IconProvidersWrapper>
Expand All @@ -79,10 +79,10 @@ export function startFrontendMain(rootElement: HTMLElement): void {
<MockDevTools />
</Suspense>
)}
</I18nextProvider>
</AppInitializer>
</MigrationProvider>
</ServiceProvider>
</AppInitializer>
</MigrationProvider>
</ServiceProvider>
</I18nextProvider>
</QueryClientProvider>
</StrictMode>,
)
Expand Down
63 changes: 19 additions & 44 deletions src/hooks/use-send.web3.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,29 @@
/**
* Web3 Transfer Implementation
*
* Handles transfers for EVM, Tron, and Bitcoin chains using chain adapters.
* Handles transfers for EVM, Tron, and Bitcoin chains using ChainProvider.
*/

import type { AssetInfo } from '@/types/asset'
import type { ChainConfig } from '@/services/chain-config'
import { Amount } from '@/types/amount'
import { walletStorageService, WalletStorageError, WalletStorageErrorCode } from '@/services/wallet-storage'
import { getAdapterRegistry, setupAdapters } from '@/services/chain-adapter'
import { getChainProvider } from '@/services/chain-adapter/providers'
import { mnemonicToSeedSync } from '@scure/bip39'

// Ensure adapters are registered
let adaptersInitialized = false
function ensureAdapters() {
if (!adaptersInitialized) {
setupAdapters()
adaptersInitialized = true
}
}

export interface Web3FeeResult {
amount: Amount
symbol: string
}

export async function fetchWeb3Fee(chainConfig: ChainConfig, fromAddress: string): Promise<Web3FeeResult> {
ensureAdapters()

const registry = getAdapterRegistry()
registry.setChainConfigs([chainConfig])
const chainProvider = getChainProvider(chainConfig.id)

const adapter = registry.getAdapter(chainConfig.id)
if (!adapter) {
throw new Error(`No adapter found for chain: ${chainConfig.id}`)
if (!chainProvider.supportsFeeEstimate) {
throw new Error(`Chain ${chainConfig.id} does not support fee estimation`)
}

const feeEstimate = await adapter.transaction.estimateFee({
const feeEstimate = await chainProvider.estimateFee!({
from: fromAddress,
to: fromAddress,
amount: Amount.fromRaw('1', chainConfig.decimals, chainConfig.symbol),
Expand All @@ -49,17 +36,13 @@ export async function fetchWeb3Fee(chainConfig: ChainConfig, fromAddress: string
}

export async function fetchWeb3Balance(chainConfig: ChainConfig, fromAddress: string): Promise<AssetInfo> {
ensureAdapters()
const chainProvider = getChainProvider(chainConfig.id)

const registry = getAdapterRegistry()
registry.setChainConfigs([chainConfig])

const adapter = registry.getAdapter(chainConfig.id)
if (!adapter) {
throw new Error(`No adapter found for chain: ${chainConfig.id}`)
if (!chainProvider.supportsNativeBalance) {
throw new Error(`Chain ${chainConfig.id} does not support balance queries`)
}

const balance = await adapter.asset.getNativeBalance(fromAddress)
const balance = await chainProvider.getNativeBalance!(fromAddress)

return {
assetType: balance.symbol,
Expand Down Expand Up @@ -91,8 +74,6 @@ export async function submitWeb3Transfer({
toAddress,
amount,
}: SubmitWeb3Params): Promise<SubmitWeb3Result> {
ensureAdapters()

// Get mnemonic from wallet storage
let mnemonic: string
try {
Expand All @@ -112,12 +93,10 @@ export async function submitWeb3Transfer({
}

try {
const registry = getAdapterRegistry()
registry.setChainConfigs([chainConfig])
const chainProvider = getChainProvider(chainConfig.id)

const adapter = registry.getAdapter(chainConfig.id)
if (!adapter) {
return { status: 'error', message: `不支持的链: ${chainConfig.id}` }
if (!chainProvider.supportsFullTransaction) {
return { status: 'error', message: `该链不支持完整交易流程: ${chainConfig.id}` }
}

console.log('[submitWeb3Transfer] Starting transfer:', {
Expand All @@ -133,19 +112,19 @@ export async function submitWeb3Transfer({

// Build unsigned transaction
console.log('[submitWeb3Transfer] Building transaction...')
const unsignedTx = await adapter.transaction.buildTransaction({
const unsignedTx = await chainProvider.buildTransaction!({
from: fromAddress,
to: toAddress,
amount,
})

// Sign transaction
console.log('[submitWeb3Transfer] Signing transaction...')
const signedTx = await adapter.transaction.signTransaction(unsignedTx, seed)
const signedTx = await chainProvider.signTransaction!(unsignedTx, seed)

// Broadcast transaction
console.log('[submitWeb3Transfer] Broadcasting transaction...')
const txHash = await adapter.transaction.broadcastTransaction(signedTx)
const txHash = await chainProvider.broadcastTransaction!(signedTx)

console.log('[submitWeb3Transfer] SUCCESS! txHash:', txHash)
return { status: 'ok', txHash }
Expand Down Expand Up @@ -178,21 +157,17 @@ export async function submitWeb3Transfer({
* Validate address for Web3 chains
*/
export function validateWeb3Address(chainConfig: ChainConfig, address: string): string | null {
ensureAdapters()

const registry = getAdapterRegistry()
registry.setChainConfigs([chainConfig])
const chainProvider = getChainProvider(chainConfig.id)

const adapter = registry.getAdapter(chainConfig.id)
if (!adapter) {
if (!chainProvider.supportsAddressValidation) {
return '不支持的链类型'
}

if (!address || address.trim() === '') {
return '请输入收款地址'
}

if (!adapter.identity.isValidAddress(address)) {
if (!chainProvider.isValidAddress!(address)) {
return '无效的地址格式'
}

Expand Down
18 changes: 18 additions & 0 deletions src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -352,5 +352,23 @@
"daysLater": "In {{count}} days",
"today": "Today",
"yesterday": "Yesterday"
},
"addressLookup": {
"balanceTitle": "Address Balance",
"transactionsTitle": "Address Transactions",
"chain": "Chain",
"address": "Address",
"addressPlaceholder": "Enter wallet address",
"addressOrHash": "Address or Transaction Hash",
"addressOrHashPlaceholder": "Enter address or tx hash",
"otherChains": "Other Chains",
"error": "Query Failed",
"queryError": "Failed to query transactions, please try again",
"noTransactions": "No transactions found",
"onChain": "on {{chain}}",
"explorerHint": "Transaction history requires block explorer",
"useExplorerHint": "This chain does not support direct history query, please use the explorer",
"openExplorer": "Open {{name}} Explorer",
"viewOnExplorer": "View on {{name}}"
}
}
5 changes: 4 additions & 1 deletion src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@
"available": "Available",
"unavailable": "Unable to get storage info",
"clearTitle": "Clear Data",
"clearDesc": "Clear all locally stored data including wallets, settings, and cache."
"clearDesc": "Clear all locally stored data including wallets, settings, and cache.",
"migrationRequired": "Data Migration Required",
"migrationDesc": "Detected old data format. Please clear the local database to continue. Your mnemonic and private keys are not affected, but you will need to re-import your wallet.",
"goToClear": "Go to Clear Data"
}
}
18 changes: 18 additions & 0 deletions src/i18n/locales/zh-CN/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -330,5 +330,23 @@
"daysLater": "{{count}} 天后",
"today": "今天",
"yesterday": "昨天"
},
"addressLookup": {
"balanceTitle": "地址余额查询",
"transactionsTitle": "地址交易查询",
"chain": "链",
"address": "地址",
"addressPlaceholder": "输入钱包地址",
"addressOrHash": "地址或交易哈希",
"addressOrHashPlaceholder": "输入地址或交易哈希",
"otherChains": "其他链",
"error": "查询失败",
"queryError": "查询交易失败,请稍后重试",
"noTransactions": "暂无交易记录",
"onChain": "在 {{chain}} 上",
"explorerHint": "交易记录需要通过区块浏览器查询",
"useExplorerHint": "该链不支持直接查询交易历史,请使用浏览器查看",
"openExplorer": "打开 {{name}} 浏览器",
"viewOnExplorer": "在 {{name}} 浏览器中查看"
}
}
5 changes: 4 additions & 1 deletion src/i18n/locales/zh-CN/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@
"available": "可用空间",
"unavailable": "无法获取存储信息",
"clearTitle": "清理数据",
"clearDesc": "清空所有本地存储的数据,包括钱包、设置和缓存。"
"clearDesc": "清空所有本地存储的数据,包括钱包、设置和缓存。",
"migrationRequired": "数据需要迁移",
"migrationDesc": "检测到旧版本数据格式,需要清空本地数据库后才能继续使用。您的助记词和私钥不会受到影响,但需要重新导入钱包。",
"goToClear": "前往清理数据"
}
}
Loading