- {isComplete
- ? t('progress.title.complete', { defaultValue: '迁移完成' })
- : t('progress.title.migrating', { defaultValue: '正在迁移...' })}
+ {isComplete ? t('progress.title.complete') : t('progress.title.migrating')}
{stepLabel}
@@ -63,7 +49,6 @@ export function MigrationProgressStep({ progress }: MigrationProgressStepProps)
{progress.currentWallet && (
{t('progress.currentWallet', {
- defaultValue: '当前: {{name}}',
name: progress.currentWallet,
})}
@@ -72,7 +57,6 @@ export function MigrationProgressStep({ progress }: MigrationProgressStepProps)
{progress.totalWallets !== undefined && progress.processedWallets !== undefined && (
{t('progress.walletCount', {
- defaultValue: '{{processed}} / {{total}} 个钱包',
processed: progress.processedWallets,
total: progress.totalWallets,
})}
diff --git a/src/components/onboarding/chain-selector.tsx b/src/components/onboarding/chain-selector.tsx
index 9305acf40..7cdc631ae 100644
--- a/src/components/onboarding/chain-selector.tsx
+++ b/src/components/onboarding/chain-selector.tsx
@@ -38,33 +38,15 @@ export interface ChainSelectorProps {
'data-testid'?: string;
}
-/** 链类型分组配置(基于 chainKind) */
-const CHAIN_KIND_GROUPS: Record = {
- bioforest: {
- name: '生物链林',
- description: 'BioForest 生态链',
- },
- evm: {
- name: 'EVM 兼容链',
- description: '以太坊虚拟机兼容链',
- },
- bitcoin: {
- name: 'Bitcoin',
- description: 'Bitcoin 网络',
- },
- tron: {
- name: 'Tron',
- description: 'Tron 网络',
- },
- custom: {
- name: '自定义链',
- description: '用户添加的自定义链',
- },
-};
+/** 链类型分组配置(基于 chainKind) - 返回翻译键 */
+const getChainKindConfig = (kind: string, t: (key: string) => string): { name: string; description: string } => ({
+ name: t(`common:chainKind.${kind}.name`),
+ description: t(`common:chainKind.${kind}.description`),
+});
/**
* 区块链网络选择器
- *
+ *
* 二级结构:
* - 第一级:技术类型(生物链林、EVM、BIP39)
* - 第二级:具体网络
@@ -87,7 +69,7 @@ export function ChainSelector({
// 按 chainKind 分组
const chainGroups = useMemo(() => {
const grouped = new Map();
-
+
for (const chain of chains) {
const kind = chain.chainKind || 'custom';
if (!grouped.has(kind)) {
@@ -99,14 +81,17 @@ export function ChainSelector({
// 按预定义顺序返回
const orderedKinds = ['bioforest', 'evm', 'bitcoin', 'tron', 'custom'];
return orderedKinds
- .filter(kind => grouped.has(kind))
- .map(kind => ({
- id: kind,
- name: CHAIN_KIND_GROUPS[kind]?.name || kind,
- description: CHAIN_KIND_GROUPS[kind]?.description,
- chains: grouped.get(kind)!,
- }));
- }, [chains]);
+ .filter((kind) => grouped.has(kind))
+ .map((kind) => {
+ const config = getChainKindConfig(kind, t);
+ return {
+ id: kind,
+ name: config.name || kind,
+ description: config.description,
+ chains: grouped.get(kind)!,
+ };
+ });
+ }, [chains, t]);
// 过滤搜索结果
const filteredGroups = useMemo(() => {
@@ -114,21 +99,22 @@ export function ChainSelector({
const query = searchQuery.toLowerCase();
return chainGroups
- .map(group => ({
+ .map((group) => ({
...group,
- chains: group.chains.filter(chain =>
- chain.name.toLowerCase().includes(query) ||
- chain.symbol.toLowerCase().includes(query) ||
- chain.id.toLowerCase().includes(query)
+ chains: group.chains.filter(
+ (chain) =>
+ chain.name.toLowerCase().includes(query) ||
+ chain.symbol.toLowerCase().includes(query) ||
+ chain.id.toLowerCase().includes(query),
),
}))
- .filter(group => group.chains.length > 0);
+ .filter((group) => group.chains.length > 0);
}, [chainGroups, searchQuery]);
const favoriteSet = useMemo(() => new Set(favoriteChains), [favoriteChains]);
const sortedGroups = useMemo(() => {
- return filteredGroups.map(group => ({
+ return filteredGroups.map((group) => ({
...group,
chains: [...group.chains].toSorted((a, b) => {
const aFav = favoriteSet.has(a.id);
@@ -141,7 +127,7 @@ export function ChainSelector({
// 切换组展开/折叠
const toggleGroup = useCallback((groupId: string) => {
- setExpandedGroups(prev => {
+ setExpandedGroups((prev) => {
const next = new Set(prev);
if (next.has(groupId)) {
next.delete(groupId);
@@ -153,59 +139,71 @@ export function ChainSelector({
}, []);
// 选择/取消选择单个链
- const toggleChain = useCallback((chainId: string) => {
- const isSelected = selectedChains.includes(chainId);
- if (isSelected) {
- onSelectionChange(selectedChains.filter(id => id !== chainId));
- } else {
- onSelectionChange([...selectedChains, chainId]);
- }
- }, [selectedChains, onSelectionChange]);
+ const toggleChain = useCallback(
+ (chainId: string) => {
+ const isSelected = selectedChains.includes(chainId);
+ if (isSelected) {
+ onSelectionChange(selectedChains.filter((id) => id !== chainId));
+ } else {
+ onSelectionChange([...selectedChains, chainId]);
+ }
+ },
+ [selectedChains, onSelectionChange],
+ );
// 选择/取消选择整个组
- const toggleGroup选择 = useCallback((group: ChainGroup) => {
- const groupChainIds = group.chains.map(c => c.id);
- const allSelected = groupChainIds.every(id => selectedChains.includes(id));
-
- if (allSelected) {
- // 取消选择整个组
- onSelectionChange(selectedChains.filter(id => !groupChainIds.includes(id)));
- } else {
- // 选择整个组
- const newSelection = new Set(selectedChains);
- groupChainIds.forEach(id => newSelection.add(id));
- onSelectionChange(Array.from(newSelection));
- }
- }, [selectedChains, onSelectionChange]);
+ const toggleGroup选择 = useCallback(
+ (group: ChainGroup) => {
+ const groupChainIds = group.chains.map((c) => c.id);
+ const allSelected = groupChainIds.every((id) => selectedChains.includes(id));
+
+ if (allSelected) {
+ // 取消选择整个组
+ onSelectionChange(selectedChains.filter((id) => !groupChainIds.includes(id)));
+ } else {
+ // 选择整个组
+ const newSelection = new Set(selectedChains);
+ groupChainIds.forEach((id) => newSelection.add(id));
+ onSelectionChange(Array.from(newSelection));
+ }
+ },
+ [selectedChains, onSelectionChange],
+ );
// 切换收藏
- const toggleFavorite = useCallback((chainId: string) => {
- if (!onFavoriteChange) return;
-
- const isFavorite = favoriteChains.includes(chainId);
- if (isFavorite) {
- onFavoriteChange(favoriteChains.filter(id => id !== chainId));
- } else {
- onFavoriteChange([...favoriteChains, chainId]);
- }
- }, [favoriteChains, onFavoriteChange]);
+ const toggleFavorite = useCallback(
+ (chainId: string) => {
+ if (!onFavoriteChange) return;
+
+ const isFavorite = favoriteChains.includes(chainId);
+ if (isFavorite) {
+ onFavoriteChange(favoriteChains.filter((id) => id !== chainId));
+ } else {
+ onFavoriteChange([...favoriteChains, chainId]);
+ }
+ },
+ [favoriteChains, onFavoriteChange],
+ );
// 检查组的选择状态
- const getGroupSelectionState = useCallback((group: ChainGroup) => {
- const groupChainIds = group.chains.map(c => c.id);
- const selectedCount = groupChainIds.filter(id => selectedChains.includes(id)).length;
-
- if (selectedCount === 0) return 'none';
- if (selectedCount === groupChainIds.length) return 'all';
- return 'partial';
- }, [selectedChains]);
+ const getGroupSelectionState = useCallback(
+ (group: ChainGroup) => {
+ const groupChainIds = group.chains.map((c) => c.id);
+ const selectedCount = groupChainIds.filter((id) => selectedChains.includes(id)).length;
+
+ if (selectedCount === 0) return 'none';
+ if (selectedCount === groupChainIds.length) return 'all';
+ return 'partial';
+ },
+ [selectedChains],
+ );
return (
{/* 搜索框 */}
{showSearch && (
-
+
setSearchQuery(e.target.value)}
@@ -218,18 +216,22 @@ export function ChainSelector({
{/* 链组列表 */}
- {sortedGroups.map(group => {
+ {sortedGroups.map((group) => {
const isExpanded = searchQuery.trim().length > 0 ? true : expandedGroups.has(group.id);
const selectionState = getGroupSelectionState(group);
-
+
return (
-
+
{/* 组标题 */}
{/* 组内链列表 */}
{isExpanded && (
-
- {group.chains.map(chain => {
+
+ {group.chains.map((chain) => {
const isSelected = selectedChains.includes(chain.id);
const isFavorite = favoriteChains.includes(chain.id);
-
+
return (
{/* 链图标 (placeholder) */}
-
+
{chain.symbol.slice(0, 2)}
{/* 链信息 */}
-
-
{chain.name}
-
{chain.symbol}
+
+
{chain.name}
+
{chain.symbol}
{/* 收藏按钮 */}
@@ -320,14 +323,14 @@ export function ChainSelector({
e.stopPropagation();
toggleFavorite(chain.id);
}}
- className="p-1 hover:bg-muted rounded transition-colors"
+ className="hover:bg-muted rounded p-1 transition-colors"
aria-label={isFavorite ? t('common:unfavorite') : t('common:favorite')}
data-testid={baseTestId ? `${baseTestId}-favorite-${chain.id}` : undefined}
>
{isFavorite ? (
) : (
-
+
)}
)}
@@ -343,7 +346,10 @@ export function ChainSelector({
{/* 空状态 */}
{sortedGroups.length === 0 && (
-
+
{t('chainSelector.noResults')}
)}
@@ -355,5 +361,5 @@ export function ChainSelector({
* 获取默认选择的链(全选)
*/
export function getDefaultSelectedChains(chains: ChainConfig[]): string[] {
- return chains.map(chain => chain.id);
+ return chains.map((chain) => chain.id);
}
diff --git a/src/components/security/crypto-authorize.tsx b/src/components/security/crypto-authorize.tsx
index 725bb3371..9a29dce9f 100644
--- a/src/components/security/crypto-authorize.tsx
+++ b/src/components/security/crypto-authorize.tsx
@@ -4,206 +4,172 @@
* 显示授权请求信息,让用户输入手势密码确认授权
*/
-import { useState, useCallback } from 'react'
-import { useTranslation } from 'react-i18next'
-import { PatternLock, patternToString } from './pattern-lock'
-import { walletStorageService } from '@/services/wallet-storage'
+import { useState, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PatternLock, patternToString } from './pattern-lock';
+import { walletStorageService } from '@/services/wallet-storage';
import {
- type CryptoAction,
- type TokenDuration,
- CRYPTO_ACTION_LABELS,
- TOKEN_DURATION_LABELS,
- TOKEN_DURATION_OPTIONS,
-} from '@/services/crypto-box'
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select'
+ type CryptoAction,
+ type TokenDuration,
+ CRYPTO_ACTION_LABELS,
+ TOKEN_DURATION_LABELS,
+ TOKEN_DURATION_OPTIONS,
+} from '@/services/crypto-box';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
export interface CryptoAuthorizeProps {
- /** 请求的操作权限 */
- actions: CryptoAction[]
- /** 默认授权时长 */
- duration: TokenDuration
- /** 使用的地址 */
- address: string
- /** 请求来源应用 */
- app: {
- name: string
- icon?: string
- }
- /** 确认回调,返回选择的时长 */
- onConfirm: (patternKey: string, selectedDuration: TokenDuration) => void
- /** 取消回调 */
- onCancel: () => void
+ /** 请求的操作权限 */
+ actions: CryptoAction[];
+ /** 默认授权时长 */
+ duration: TokenDuration;
+ /** 使用的地址 */
+ address: string;
+ /** 请求来源应用 */
+ app: {
+ name: string;
+ icon?: string;
+ };
+ /** 确认回调,返回选择的时长 */
+ onConfirm: (patternKey: string, selectedDuration: TokenDuration) => void;
+ /** 取消回调 */
+ onCancel: () => void;
}
/**
* Crypto 授权对话框
*/
-export function CryptoAuthorize({
- actions,
- duration,
- address,
- app,
- onConfirm,
- onCancel,
-}: CryptoAuthorizeProps) {
- const { t } = useTranslation()
- const [pattern, setPattern] = useState
([])
- const [error, setError] = useState(false)
- const [verifying, setVerifying] = useState(false)
- const [selectedDuration, setSelectedDuration] = useState(duration)
-
- const handlePatternComplete = useCallback(
- async (nodes: number[]) => {
- setVerifying(true)
- setError(false)
+export function CryptoAuthorize({ actions, duration, address, app, onConfirm, onCancel }: CryptoAuthorizeProps) {
+ const { t } = useTranslation();
+ const [pattern, setPattern] = useState([]);
+ const [error, setError] = useState(false);
+ const [verifying, setVerifying] = useState(false);
+ const [selectedDuration, setSelectedDuration] = useState(duration);
- try {
- const patternKey = patternToString(nodes)
+ const handlePatternComplete = useCallback(
+ async (nodes: number[]) => {
+ setVerifying(true);
+ setError(false);
- // 验证手势密码是否正确(尝试解密任意钱包的 mnemonic)
- const wallets = await walletStorageService.getAllWallets()
- if (wallets.length === 0) {
- setError(true)
- setPattern([])
- return
- }
+ try {
+ const patternKey = patternToString(nodes);
- let isValid = false
- for (const wallet of wallets) {
- try {
- await walletStorageService.getMnemonic(wallet.id, patternKey)
- isValid = true
- break
- } catch {
- // 继续尝试下一个钱包
- }
- }
+ // 验证手势密码是否正确(尝试解密任意钱包的 mnemonic)
+ const wallets = await walletStorageService.getAllWallets();
+ if (wallets.length === 0) {
+ setError(true);
+ setPattern([]);
+ return;
+ }
- if (isValid) {
- onConfirm(patternKey, selectedDuration)
- } else {
- setError(true)
- setPattern([])
- }
- } catch {
- setError(true)
- setPattern([])
- } finally {
- setVerifying(false)
- }
- },
- [onConfirm, selectedDuration]
- )
+ let isValid = false;
+ for (const wallet of wallets) {
+ try {
+ await walletStorageService.getMnemonic(wallet.id, patternKey);
+ isValid = true;
+ break;
+ } catch {
+ // 继续尝试下一个钱包
+ }
+ }
- return (
-
-
- {/* Header */}
-
-
- {app.icon && (
-

- )}
-
{app.name}
-
-
- {t('crypto.authorize.title', { defaultValue: '请求加密授权' })}
-
-
+ if (isValid) {
+ onConfirm(patternKey, selectedDuration);
+ } else {
+ setError(true);
+ setPattern([]);
+ }
+ } catch {
+ setError(true);
+ setPattern([]);
+ } finally {
+ setVerifying(false);
+ }
+ },
+ [onConfirm, selectedDuration],
+ );
- {/* 请求的权限 */}
-
-
- {t('crypto.authorize.permissions', { defaultValue: '请求权限' })}
-
-
- {actions.map((action) => (
- -
- ✓
-
-
{CRYPTO_ACTION_LABELS[action].name}
-
- {CRYPTO_ACTION_LABELS[action].description}
-
-
-
- ))}
-
-
+ return (
+
+
+ {/* Header */}
+
+
+ {app.icon &&

}
+
{app.name}
+
+
{t('crypto.authorize.title')}
+
- {/* 授权时长和地址 */}
-
-
-
- {t('crypto.authorize.duration', { defaultValue: '授权时长' })}
-
-
-
-
-
- {t('crypto.authorize.address', { defaultValue: '使用地址' })}
-
-
- {address.slice(0, 8)}...{address.slice(-6)}
-
-
+ {/* 请求的权限 */}
+
+
{t('crypto.authorize.permissions')}
+
+ {actions.map((action) => (
+ -
+ ✓
+
+
{CRYPTO_ACTION_LABELS[action].name}
+
{CRYPTO_ACTION_LABELS[action].description}
+
+ ))}
+
+
- {/* 手势密码 */}
-
-
- {t('crypto.authorize.pattern', { defaultValue: '请输入手势密码确认' })}
-
-
- {error && (
-
- {t('crypto.authorize.error', { defaultValue: '手势密码错误,请重试' })}
-
- )}
-
+ {/* 授权时长和地址 */}
+
+
+ {t('crypto.authorize.duration')}
+
+
+
+ {t('crypto.authorize.address')}
+
+ {address.slice(0, 8)}...{address.slice(-6)}
+
+
+
- {/* 取消按钮 */}
-
-
+ {/* 手势密码 */}
+
+
{t('crypto.authorize.pattern')}
+
+ {error &&
{t('crypto.authorize.error')}
}
- )
+
+ {/* 取消按钮 */}
+
+
+
+ );
}
diff --git a/src/components/security/password-input.tsx b/src/components/security/password-input.tsx
index d91b64cd8..029faa541 100644
--- a/src/components/security/password-input.tsx
+++ b/src/components/security/password-input.tsx
@@ -27,9 +27,9 @@ function calculateStrength(password: string): PasswordStrength {
}
const strengthConfig = {
- weak: { label: '弱', color: 'bg-destructive', width: 'w-1/3' },
- medium: { label: '中', color: 'bg-yellow-500', width: 'w-2/3' },
- strong: { label: '强', color: 'bg-secondary', width: 'w-full' },
+ weak: { label: '弱', color: 'bg-destructive', width: 'w-1/3' }, // i18n-ignore: visual indicator
+ medium: { label: '中', color: 'bg-yellow-500', width: 'w-2/3' }, // i18n-ignore: visual indicator
+ strong: { label: '强', color: 'bg-secondary', width: 'w-full' }, // i18n-ignore: visual indicator
};
const PasswordInput = forwardRef
(
diff --git a/src/components/token/token-item.tsx b/src/components/token/token-item.tsx
index c30a1b3b0..16223a748 100644
--- a/src/components/token/token-item.tsx
+++ b/src/components/token/token-item.tsx
@@ -180,12 +180,7 @@ export function TokenItem({
{/* Balance and Actions */}
-
+
{displayFiatValue && !loading && (
≈ {fiatSymbol}
@@ -204,7 +199,7 @@ export function TokenItem({
// Get menu items if provided
const items = menuItems?.(token, context).filter((item) => item.show !== false) ?? [];
-
+
// When there are menu items (which render as buttons), Item cannot be a button
// to avoid button nesting hydration errors
const hasMenuItems = items.length > 0;
@@ -246,14 +241,8 @@ export function TokenItem({
{items.length > 0 && (
- }
+ aria-label={t('common:a11y.more')}
+ render={}
>
diff --git a/src/components/token/token-list.tsx b/src/components/token/token-list.tsx
index b36f07c82..c0d3f3980 100644
--- a/src/components/token/token-list.tsx
+++ b/src/components/token/token-list.tsx
@@ -1,28 +1,28 @@
-import { cn } from '@/lib/utils'
-import { TokenItem, type TokenInfo, type TokenItemContext, type TokenMenuItem } from './token-item'
-import { EmptyState, SkeletonList } from '@biochain/key-ui'
+import { cn } from '@/lib/utils';
+import { TokenItem, type TokenInfo, type TokenItemContext, type TokenMenuItem } from './token-item';
+import { EmptyState, SkeletonList } from '@biochain/key-ui';
interface TokenListProps {
- tokens: TokenInfo[]
+ tokens: TokenInfo[];
/** Show skeleton loading state (no tokens yet) */
- loading?: boolean | undefined
+ loading?: boolean | undefined;
/** Show balance refresh animation (tokens exist but refreshing) */
- refreshing?: boolean | undefined
- showChange?: boolean | undefined
- onTokenClick?: ((token: TokenInfo) => void) | undefined
- emptyTitle?: string | undefined
- emptyDescription?: string | undefined
- emptyAction?: React.ReactNode | undefined
- className?: string | undefined
- testId?: string | undefined
+ refreshing?: boolean | undefined;
+ showChange?: boolean | undefined;
+ onTokenClick?: ((token: TokenInfo) => void) | undefined;
+ emptyTitle?: string | undefined;
+ emptyDescription?: string | undefined;
+ emptyAction?: React.ReactNode | undefined;
+ className?: string | undefined;
+ testId?: string | undefined;
/** Render prop for custom actions per token item (deprecated: use menuItems) */
- renderActions?: ((token: TokenInfo, context: TokenItemContext) => React.ReactNode) | undefined
+ renderActions?: ((token: TokenInfo, context: TokenItemContext) => React.ReactNode) | undefined;
/** Main asset symbol for the chain (used by renderActions context) */
- mainAssetSymbol?: string | undefined
+ mainAssetSymbol?: string | undefined;
/** Context menu handler for token items (matches TokenItem signature) */
- onContextMenu?: ((e: React.MouseEvent, token: TokenInfo) => void) | undefined
+ onContextMenu?: ((e: React.MouseEvent, token: TokenInfo) => void) | undefined;
/** Menu items for dropdown menu (recommended approach) */
- menuItems?: ((token: TokenInfo, context: TokenItemContext) => TokenMenuItem[]) | undefined
+ menuItems?: ((token: TokenInfo, context: TokenItemContext) => TokenMenuItem[]) | undefined;
}
export function TokenList({
@@ -31,8 +31,8 @@ export function TokenList({
refreshing = false,
showChange = false,
onTokenClick,
- emptyTitle = '暂无资产',
- emptyDescription = '转入资产后将显示在这里',
+ emptyTitle = '暂无资产', // i18n-ignore: default prop
+ emptyDescription = '转入资产后将显示在这里', // i18n-ignore: default prop
emptyAction,
className,
testId,
@@ -42,7 +42,7 @@ export function TokenList({
menuItems,
}: TokenListProps) {
if (loading) {
- return
+ return ;
}
if (tokens.length === 0) {
@@ -54,12 +54,16 @@ export function TokenList({
{...(emptyAction && { action: emptyAction })}
icon={
}
{...(className && { className })}
/>
- )
+ );
}
return (
@@ -79,5 +83,5 @@ export function TokenList({
/>
))}
- )
+ );
}
diff --git a/src/components/transaction/transaction-list.tsx b/src/components/transaction/transaction-list.tsx
index 03a111417..71b5a5572 100644
--- a/src/components/transaction/transaction-list.tsx
+++ b/src/components/transaction/transaction-list.tsx
@@ -23,16 +23,14 @@ function groupByDate(transactions: TransactionInfo[]): Map {
// timestamp 可能是 number (毫秒), string, 或 Date 对象
- const date = tx.timestamp instanceof Date
- ? tx.timestamp
- : new Date(tx.timestamp);
+ const date = tx.timestamp instanceof Date ? tx.timestamp : new Date(tx.timestamp);
const dateStr = date.toDateString();
let key: string;
if (dateStr === today) {
- key = '今天';
+ key = '今天'; // i18n-ignore: date grouping
} else if (dateStr === yesterday) {
- key = '昨天';
+ key = '昨天'; // i18n-ignore: date grouping
} else {
key = date.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric' });
}
@@ -50,8 +48,8 @@ export function TransactionList({
transactions,
loading = false,
onTransactionClick,
- emptyTitle = '暂无交易记录',
- emptyDescription = '您的交易记录将显示在这里',
+ emptyTitle = '暂无交易记录', // i18n-ignore: default prop
+ emptyDescription = '您的交易记录将显示在这里', // i18n-ignore: default prop
emptyAction,
className,
showChainIcon = false,
diff --git a/src/components/wallet/address-display.tsx b/src/components/wallet/address-display.tsx
index 6156c42fa..d694fb27b 100644
--- a/src/components/wallet/address-display.tsx
+++ b/src/components/wallet/address-display.tsx
@@ -73,9 +73,9 @@ function truncateAddress(address: string, maxWidth: number, font: string): strin
}
function truncateAddressByChars(address: string, startChars: number, endChars: number): string {
- const ellipsis = '...'
- if (address.length <= startChars + endChars + ellipsis.length) return address
- return `${address.slice(0, startChars)}${ellipsis}${address.slice(-endChars)}`
+ const ellipsis = '...';
+ if (address.length <= startChars + endChars + ellipsis.length) return address;
+ return `${address.slice(0, startChars)}${ellipsis}${address.slice(-endChars)}`;
}
export function AddressDisplay({
@@ -141,9 +141,7 @@ export function AddressDisplay({
setCopied(true);
onCopy?.();
setTimeout(() => setCopied(false), 2000);
- } catch {
-
- }
+ } catch {}
};
// 绝对定位:文字不参与容器宽度计算
@@ -185,7 +183,7 @@ export function AddressDisplay({
className,
)}
title={address}
- aria-label={copied ? `已复制 ${address}` : `复制 ${address}`}
+ aria-label={copied ? `已复制 ${address}` : `复制 ${address}`} // i18n-ignore: a11y
>
{/* 文字容器:flex-1 获取剩余空间 */}
@@ -201,7 +199,7 @@ export function AddressDisplay({
{copied ? (
-
+
) : (
)}
diff --git a/src/hooks/use-burn.ts b/src/hooks/use-burn.ts
index 6cb84b03a..1f58b65de 100644
--- a/src/hooks/use-burn.ts
+++ b/src/hooks/use-burn.ts
@@ -221,9 +221,9 @@ export function useBurn(options: UseBurnOptions = {}): UseBurnReturn {
step: 'result',
isSubmitting: false,
resultStatus: 'failed',
- errorMessage: '仅支持 BioForest 链的资产销毁',
+ errorMessage: t('error:burn.bioforestOnly'),
}));
- return { status: 'error', message: '仅支持 BioForest 链' };
+ return { status: 'error', message: t('error:burn.bioforestOnly') };
}
if (!walletId || !fromAddress || !state.asset || !state.amount || !state.recipientAddress) {
@@ -232,9 +232,9 @@ export function useBurn(options: UseBurnOptions = {}): UseBurnReturn {
step: 'result',
isSubmitting: false,
resultStatus: 'failed',
- errorMessage: '参数不完整',
+ errorMessage: t('error:transaction.paramsIncomplete'),
}));
- return { status: 'error', message: '参数不完整' };
+ return { status: 'error', message: t('error:transaction.paramsIncomplete') };
}
setState((prev) => ({
@@ -301,7 +301,7 @@ export function useBurn(options: UseBurnOptions = {}): UseBurnReturn {
const submitWithTwoStepSecret = useCallback(
async (password: string, twoStepSecret: string): Promise => {
if (!chainConfig || !walletId || !fromAddress || !state.asset || !state.amount || !state.recipientAddress) {
- return { status: 'error', message: '参数不完整' };
+ return { status: 'error', message: t('error:transaction.paramsIncomplete') };
}
setState((prev) => ({
diff --git a/src/hooks/use-duplicate-detection.ts b/src/hooks/use-duplicate-detection.ts
index 5f09d70ae..e16c408cb 100644
--- a/src/hooks/use-duplicate-detection.ts
+++ b/src/hooks/use-duplicate-detection.ts
@@ -1,27 +1,27 @@
-import { useState, useCallback } from 'react'
-import { generateAllAddresses, getAddressSet } from './use-multi-chain-address-generation'
-import type {
- DuplicateCheckResult,
- IWalletQuery,
-} from '@/services/wallet/types'
+import { useState, useCallback } from 'react';
+import { generateAllAddresses, getAddressSet } from './use-multi-chain-address-generation';
+import type { DuplicateCheckResult, IWalletQuery } from '@/services/wallet/types';
+import i18n from '@/i18n';
+
+const t = i18n.t.bind(i18n);
export interface DuplicateDetectionResult {
/** 检测结果 */
- result: DuplicateCheckResult
+ result: DuplicateCheckResult;
/** 是否正在检测 */
- isChecking: boolean
+ isChecking: boolean;
/** 错误信息 */
- error: string | null
+ error: string | null;
/** 执行检测 */
- check: (mnemonic: string[]) => Promise
+ check: (mnemonic: string[]) => Promise;
/** 重置状态 */
- reset: () => void
+ reset: () => void;
}
const INITIAL_RESULT: DuplicateCheckResult = {
isDuplicate: false,
type: 'none',
-}
+};
/**
* 三级重复检测 Hook
@@ -31,22 +31,22 @@ const INITIAL_RESULT: DuplicateCheckResult = {
* Level 3: 私钥碰撞检查 - 检查是否与私钥导入的钱包冲突
*/
export function useDuplicateDetection(walletQuery: IWalletQuery): DuplicateDetectionResult {
- const [result, setResult] = useState(INITIAL_RESULT)
- const [isChecking, setIsChecking] = useState(false)
- const [error, setError] = useState(null)
+ const [result, setResult] = useState(INITIAL_RESULT);
+ const [isChecking, setIsChecking] = useState(false);
+ const [error, setError] = useState(null);
const check = useCallback(
async (mnemonic: string[]): Promise => {
- setIsChecking(true)
- setError(null)
+ setIsChecking(true);
+ setError(null);
try {
// Generate all addresses from mnemonic
- const derivedKeys = generateAllAddresses(mnemonic)
- const newAddressSet = getAddressSet(derivedKeys)
+ const derivedKeys = generateAllAddresses(mnemonic);
+ const newAddressSet = getAddressSet(derivedKeys);
// === Level 1: Simple address lookup ===
- const existingAddresses = await walletQuery.getAllAddresses()
+ const existingAddresses = await walletQuery.getAllAddresses();
for (const existing of existingAddresses) {
if (newAddressSet.has(existing.address.toLowerCase())) {
@@ -59,9 +59,9 @@ export function useDuplicateDetection(walletQuery: IWalletQuery): DuplicateDetec
importType: 'mnemonic', // Default, will be refined in Level 3
matchedAddress: existing.address,
},
- }
- setResult(result)
- return result
+ };
+ setResult(result);
+ return result;
}
}
@@ -69,8 +69,8 @@ export function useDuplicateDetection(walletQuery: IWalletQuery): DuplicateDetec
// Already covered by Level 1 since we generate all addresses upfront
// === Level 3: Private key collision check ===
- const allWallets = await walletQuery.getAllMainWallets()
- const privateKeyWallets = allWallets.filter((w) => w.importType === 'privateKey')
+ const allWallets = await walletQuery.getAllMainWallets();
+ const privateKeyWallets = allWallets.filter((w) => w.importType === 'privateKey');
for (const pkWallet of privateKeyWallets) {
for (const pkAddr of pkWallet.addresses) {
@@ -84,9 +84,9 @@ export function useDuplicateDetection(walletQuery: IWalletQuery): DuplicateDetec
importType: 'privateKey',
matchedAddress: pkAddr.address,
},
- }
- setResult(result)
- return result
+ };
+ setResult(result);
+ return result;
}
}
}
@@ -95,24 +95,24 @@ export function useDuplicateDetection(walletQuery: IWalletQuery): DuplicateDetec
const noMatch: DuplicateCheckResult = {
isDuplicate: false,
type: 'none',
- }
- setResult(noMatch)
- return noMatch
+ };
+ setResult(noMatch);
+ return noMatch;
} catch (err) {
- const message = err instanceof Error ? err.message : '重复检测失败'
- setError(message)
- return INITIAL_RESULT
+ const message = err instanceof Error ? err.message : t('error:duplicate.detectionFailed');
+ setError(message);
+ return INITIAL_RESULT;
} finally {
- setIsChecking(false)
+ setIsChecking(false);
}
},
[walletQuery],
- )
+ );
const reset = useCallback(() => {
- setResult(INITIAL_RESULT)
- setError(null)
- }, [])
+ setResult(INITIAL_RESULT);
+ setError(null);
+ }, []);
return {
result,
@@ -120,21 +120,18 @@ export function useDuplicateDetection(walletQuery: IWalletQuery): DuplicateDetec
error,
check,
reset,
- }
+ };
}
/**
* 同步重复检测(用于非 React 上下文)
*/
-export async function checkDuplicates(
- mnemonic: string[],
- walletQuery: IWalletQuery,
-): Promise {
- const derivedKeys = generateAllAddresses(mnemonic)
- const newAddressSet = getAddressSet(derivedKeys)
+export async function checkDuplicates(mnemonic: string[], walletQuery: IWalletQuery): Promise {
+ const derivedKeys = generateAllAddresses(mnemonic);
+ const newAddressSet = getAddressSet(derivedKeys);
// Level 1 & 2: Address check
- const existingAddresses = await walletQuery.getAllAddresses()
+ const existingAddresses = await walletQuery.getAllAddresses();
for (const existing of existingAddresses) {
if (newAddressSet.has(existing.address.toLowerCase())) {
return {
@@ -146,13 +143,13 @@ export async function checkDuplicates(
importType: 'mnemonic',
matchedAddress: existing.address,
},
- }
+ };
}
}
// Level 3: Private key collision
- const allWallets = await walletQuery.getAllMainWallets()
- const privateKeyWallets = allWallets.filter((w) => w.importType === 'privateKey')
+ const allWallets = await walletQuery.getAllMainWallets();
+ const privateKeyWallets = allWallets.filter((w) => w.importType === 'privateKey');
for (const pkWallet of privateKeyWallets) {
for (const pkAddr of pkWallet.addresses) {
@@ -166,10 +163,10 @@ export async function checkDuplicates(
importType: 'privateKey',
matchedAddress: pkAddr.address,
},
- }
+ };
}
}
}
- return { isDuplicate: false, type: 'none' }
+ return { isDuplicate: false, type: 'none' };
}
diff --git a/src/hooks/use-multi-chain-address-generation.ts b/src/hooks/use-multi-chain-address-generation.ts
index 77542d718..4b387a81f 100644
--- a/src/hooks/use-multi-chain-address-generation.ts
+++ b/src/hooks/use-multi-chain-address-generation.ts
@@ -1,18 +1,21 @@
-import { useState, useCallback } from 'react'
-import { deriveAllAddresses, type DerivedKey } from '@/lib/crypto/derivation'
-import { validateMnemonic } from '@/lib/crypto/mnemonic'
+import { useState, useCallback } from 'react';
+import { deriveAllAddresses, type DerivedKey } from '@/lib/crypto/derivation';
+import { validateMnemonic } from '@/lib/crypto/mnemonic';
+import i18n from '@/i18n';
+
+const t = i18n.t.bind(i18n);
export interface MultiChainAddressResult {
/** All generated addresses */
- addresses: DerivedKey[]
+ addresses: DerivedKey[];
/** Whether generation is in progress */
- isGenerating: boolean
+ isGenerating: boolean;
/** Error message if generation failed */
- error: string | null
+ error: string | null;
/** Generate addresses from mnemonic */
- generate: (mnemonic: string[]) => Promise
+ generate: (mnemonic: string[]) => Promise;
/** Clear results */
- clear: () => void
+ clear: () => void;
}
/**
@@ -20,41 +23,41 @@ export interface MultiChainAddressResult {
* Used for duplicate detection during wallet recovery
*/
export function useMultiChainAddressGeneration(): MultiChainAddressResult {
- const [addresses, setAddresses] = useState([])
- const [isGenerating, setIsGenerating] = useState(false)
- const [error, setError] = useState(null)
+ const [addresses, setAddresses] = useState([]);
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [error, setError] = useState(null);
const generate = useCallback(async (mnemonic: string[]): Promise => {
- setIsGenerating(true)
- setError(null)
+ setIsGenerating(true);
+ setError(null);
try {
// Validate mnemonic first
if (!validateMnemonic(mnemonic)) {
- throw new Error('Invalid mnemonic')
+ throw new Error('Invalid mnemonic');
}
- const mnemonicString = mnemonic.join(' ')
+ const mnemonicString = mnemonic.join(' ');
// Generate addresses for all chains
// This is done synchronously but wrapped in promise for consistency
- const results = deriveAllAddresses(mnemonicString)
+ const results = deriveAllAddresses(mnemonicString);
- setAddresses(results)
- return results
+ setAddresses(results);
+ return results;
} catch (err) {
- const message = err instanceof Error ? err.message : '地址生成失败'
- setError(message)
- return []
+ const message = err instanceof Error ? err.message : t('error:address.generationFailed');
+ setError(message);
+ return [];
} finally {
- setIsGenerating(false)
+ setIsGenerating(false);
}
- }, [])
+ }, []);
const clear = useCallback(() => {
- setAddresses([])
- setError(null)
- }, [])
+ setAddresses([]);
+ setError(null);
+ }, []);
return {
addresses,
@@ -62,7 +65,7 @@ export function useMultiChainAddressGeneration(): MultiChainAddressResult {
error,
generate,
clear,
- }
+ };
}
/**
@@ -70,14 +73,14 @@ export function useMultiChainAddressGeneration(): MultiChainAddressResult {
*/
export function generateAllAddresses(mnemonic: string[]): DerivedKey[] {
if (!validateMnemonic(mnemonic)) {
- throw new Error('Invalid mnemonic')
+ throw new Error('Invalid mnemonic');
}
- return deriveAllAddresses(mnemonic.join(' '))
+ return deriveAllAddresses(mnemonic.join(' '));
}
/**
* Get all addresses as a Set for efficient lookup
*/
export function getAddressSet(keys: DerivedKey[]): Set {
- return new Set(keys.map((k) => k.address.toLowerCase()))
+ return new Set(keys.map((k) => k.address.toLowerCase()));
}
diff --git a/src/hooks/use-security-password.ts b/src/hooks/use-security-password.ts
index 186dc5afa..2036011cd 100644
--- a/src/hooks/use-security-password.ts
+++ b/src/hooks/use-security-password.ts
@@ -1,54 +1,53 @@
/**
* 安全密码 Hook
- *
+ *
* 提供安全密码的查询、缓存、验证功能
*/
-import { useCallback, useEffect, useMemo, useState } from 'react'
-import { useStore } from '@tanstack/react-store'
-import {
- securityPasswordStore,
- securityPasswordActions,
- securityPasswordSelectors,
-} from '@/stores/security-password'
-import { getChainProvider } from '@/services/chain-adapter/providers'
-import type { ChainConfig } from '@/services/chain-config'
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useStore } from '@tanstack/react-store';
+import { securityPasswordStore, securityPasswordActions, securityPasswordSelectors } from '@/stores/security-password';
+import { getChainProvider } from '@/services/chain-adapter/providers';
+import type { ChainConfig } from '@/services/chain-config';
+import i18n from '@/i18n';
+
+const t = i18n.t.bind(i18n);
interface UseSecurityPasswordOptions {
/** 链配置 */
- chainConfig: ChainConfig | null | undefined
+ chainConfig: ChainConfig | null | undefined;
/** 地址 */
- address: string | null | undefined
+ address: string | null | undefined;
/** 是否自动加载 */
- autoLoad?: boolean
+ autoLoad?: boolean;
}
interface UseSecurityPasswordResult {
/** 安全密码公钥 */
- publicKey: string | null | undefined
+ publicKey: string | null | undefined;
/** 是否设置了安全密码 */
- hasSecurityPassword: boolean
+ hasSecurityPassword: boolean;
/** 是否正在加载 */
- isLoading: boolean
+ isLoading: boolean;
/** 是否已加载完成 */
- isLoaded: boolean
+ isLoaded: boolean;
/** 错误信息 */
- error: string | null
+ error: string | null;
/** 刷新(重新查询) */
- refresh: () => Promise
+ refresh: () => Promise;
/** 验证安全密码 */
- verify: (mainSecret: string, paySecret: string) => Promise
+ verify: (mainSecret: string, paySecret: string) => Promise;
}
/**
* 安全密码 Hook
- *
+ *
* @example
* ```tsx
* const { hasSecurityPassword, verify, isLoading } = useSecurityPassword({
* chainConfig,
* address,
* })
- *
+ *
* // 签名前检查
* if (hasSecurityPassword) {
* // 显示安全密码输入
@@ -64,78 +63,81 @@ export function useSecurityPassword({
address,
autoLoad = true,
}: UseSecurityPasswordOptions): UseSecurityPasswordResult {
- const state = useStore(securityPasswordStore)
+ const state = useStore(securityPasswordStore);
const publicKey = useMemo(() => {
- if (!address) return undefined
- return securityPasswordSelectors.getPublicKey(state, address)
- }, [state, address])
+ if (!address) return undefined;
+ return securityPasswordSelectors.getPublicKey(state, address);
+ }, [state, address]);
const hasSecurityPassword = useMemo(() => {
- if (!address) return false
- return securityPasswordSelectors.hasSecurityPassword(state, address)
- }, [state, address])
+ if (!address) return false;
+ return securityPasswordSelectors.hasSecurityPassword(state, address);
+ }, [state, address]);
const isLoading = useMemo(() => {
- if (!address) return false
- return securityPasswordSelectors.isLoading(state, address)
- }, [state, address])
+ if (!address) return false;
+ return securityPasswordSelectors.isLoading(state, address);
+ }, [state, address]);
const isLoaded = useMemo(() => {
- if (!address) return false
- return securityPasswordSelectors.isLoaded(state, address)
- }, [state, address])
+ if (!address) return false;
+ return securityPasswordSelectors.isLoaded(state, address);
+ }, [state, address]);
const error = useMemo(() => {
- if (!address) return null
- return securityPasswordSelectors.getError(state, address)
- }, [state, address])
+ if (!address) return null;
+ return securityPasswordSelectors.getError(state, address);
+ }, [state, address]);
// 查询安全密码公钥
const refresh = useCallback(async () => {
- if (!chainConfig || !address) return
+ if (!chainConfig || !address) return;
- const provider = getChainProvider(chainConfig.id)
+ const provider = getChainProvider(chainConfig.id);
if (!provider.supportsBioAccountInfo) {
- securityPasswordActions.setError(address, '该链不支持安全密码')
- return
+ securityPasswordActions.setError(address, t('error:chain.notSupportSecurityPassword'));
+ return;
}
- securityPasswordActions.setLoading(address, true)
+ securityPasswordActions.setLoading(address, true);
try {
- const info = await provider.bioGetAccountInfo!(address)
- securityPasswordActions.setPublicKey(address, info.secondPublicKey ?? null)
+ const info = await provider.bioGetAccountInfo!(address);
+ securityPasswordActions.setPublicKey(address, info.secondPublicKey ?? null);
} catch (err) {
- const message = err instanceof Error ? err.message : '查询失败'
- securityPasswordActions.setError(address, message)
+ const message = err instanceof Error ? err.message : t('error:query.failed');
+ securityPasswordActions.setError(address, message);
}
- }, [chainConfig, address])
+ }, [chainConfig, address]);
// 验证安全密码
- const verify = useCallback(async (mainSecret: string, paySecret: string): Promise => {
- if (!chainConfig || !publicKey) return false
+ const verify = useCallback(
+ async (mainSecret: string, paySecret: string): Promise => {
+ if (!chainConfig || !publicKey) return false;
- const provider = getChainProvider(chainConfig.id)
- if (!provider.bioVerifyPayPassword) return false
+ const provider = getChainProvider(chainConfig.id);
+ if (!provider.bioVerifyPayPassword) return false;
- try {
- return await provider.bioVerifyPayPassword({
- mainSecret,
- paySecret,
- publicKey,
- })
- } catch {
- return false
- }
- }, [chainConfig, publicKey])
+ try {
+ return await provider.bioVerifyPayPassword({
+ mainSecret,
+ paySecret,
+ publicKey,
+ });
+ } catch {
+ return false;
+ }
+ },
+ [chainConfig, publicKey],
+ );
// 自动加载
useEffect(() => {
if (autoLoad && chainConfig && address && !isLoaded && !isLoading) {
- refresh()
+ refresh();
}
- }, [autoLoad, chainConfig, address, isLoaded, isLoading, refresh])
+ }, [autoLoad, chainConfig, address, isLoaded, isLoading, refresh]);
return {
publicKey,
@@ -145,12 +147,12 @@ export function useSecurityPassword({
error,
refresh,
verify,
- }
+ };
}
/**
* 实时验证安全密码 Hook
- *
+ *
* 用于输入时实时验证安全密码是否正确
*/
export function useSecurityPasswordValidation({
@@ -160,44 +162,44 @@ export function useSecurityPasswordValidation({
paySecret,
debounceMs = 300,
}: {
- chainConfig: ChainConfig | null | undefined
- publicKey: string | null | undefined
- mainSecret: string
- paySecret: string
- debounceMs?: number
+ chainConfig: ChainConfig | null | undefined;
+ publicKey: string | null | undefined;
+ mainSecret: string;
+ paySecret: string;
+ debounceMs?: number;
}) {
- const [isValid, setIsValid] = useState(null)
- const [isValidating, setIsValidating] = useState(false)
+ const [isValid, setIsValid] = useState(null);
+ const [isValidating, setIsValidating] = useState(false);
useEffect(() => {
if (!chainConfig || !publicKey || !mainSecret || !paySecret) {
- setIsValid(null)
- return
+ setIsValid(null);
+ return;
}
- setIsValidating(true)
+ setIsValidating(true);
const timer = setTimeout(async () => {
try {
- const provider = getChainProvider(chainConfig.id)
+ const provider = getChainProvider(chainConfig.id);
if (!provider.bioVerifyPayPassword) {
- setIsValid(false)
- return
+ setIsValid(false);
+ return;
}
const result = await provider.bioVerifyPayPassword({
mainSecret,
paySecret,
publicKey,
- })
- setIsValid(result)
+ });
+ setIsValid(result);
} catch {
- setIsValid(false)
+ setIsValid(false);
} finally {
- setIsValidating(false)
+ setIsValidating(false);
}
- }, debounceMs)
+ }, debounceMs);
- return () => clearTimeout(timer)
- }, [chainConfig, publicKey, mainSecret, paySecret, debounceMs])
+ return () => clearTimeout(timer);
+ }, [chainConfig, publicKey, mainSecret, paySecret, debounceMs]);
- return { isValid, isValidating }
+ return { isValid, isValidating };
}
diff --git a/src/hooks/use-send.mock.ts b/src/hooks/use-send.mock.ts
index 62a61be0b..efe2c600c 100644
--- a/src/hooks/use-send.mock.ts
+++ b/src/hooks/use-send.mock.ts
@@ -1,5 +1,8 @@
-import type { Dispatch, SetStateAction } from 'react'
-import type { SendState } from './use-send.types'
+import type { Dispatch, SetStateAction } from 'react';
+import type { SendState } from './use-send.types';
+import i18n from '@/i18n';
+
+const t = i18n.t.bind(i18n);
export async function submitMockTransfer(
setState: Dispatch>,
@@ -8,12 +11,12 @@ export async function submitMockTransfer(
...prev,
step: 'sending',
isSubmitting: true,
- }))
+ }));
try {
- await new Promise((resolve) => setTimeout(resolve, 2000))
+ await new Promise((resolve) => setTimeout(resolve, 2000));
- const isSuccess = Math.random() > 0.2 // 80% success rate
+ const isSuccess = Math.random() > 0.2; // 80% success rate
if (isSuccess) {
setState((prev) => ({
@@ -23,11 +26,11 @@ export async function submitMockTransfer(
resultStatus: 'success',
txHash: `0x${Math.random().toString(16).slice(2)}${Math.random().toString(16).slice(2)}`,
errorMessage: null,
- }))
- return { status: 'ok' }
+ }));
+ return { status: 'ok' };
}
- throw new Error('交易失败,请稍后重试')
+ throw new Error(t('error:transaction.transactionFailed'));
} catch (error) {
setState((prev) => ({
...prev,
@@ -35,8 +38,8 @@ export async function submitMockTransfer(
isSubmitting: false,
resultStatus: 'failed',
txHash: null,
- errorMessage: error instanceof Error ? error.message : '未知错误',
- }))
- return { status: 'error' }
+ errorMessage: error instanceof Error ? error.message : t('error:transaction.unknownError'),
+ }));
+ return { status: 'error' };
}
}
diff --git a/src/hooks/use-send.ts b/src/hooks/use-send.ts
index a83cce5de..06544d4c3 100644
--- a/src/hooks/use-send.ts
+++ b/src/hooks/use-send.ts
@@ -1,40 +1,47 @@
-import { useState, useCallback, useMemo } from 'react'
-import type { AssetInfo } from '@/types/asset'
-import { Amount } from '@/types/amount'
-import { initialState, MOCK_FEES } from './use-send.constants'
-import type { SendState, UseSendOptions, UseSendReturn } from './use-send.types'
-import { fetchBioforestFee, submitBioforestTransfer } from './use-send.bioforest'
-import { fetchWeb3Fee, submitWeb3Transfer, validateWeb3Address } from './use-send.web3'
-import { adjustAmountForFee, canProceedToConfirm, validateAddressInput, validateAmountInput } from './use-send.logic'
-import { submitMockTransfer } from './use-send.mock'
+import { useState, useCallback, useMemo } from 'react';
+import type { AssetInfo } from '@/types/asset';
+import { Amount } from '@/types/amount';
+import { initialState, MOCK_FEES } from './use-send.constants';
+import type { SendState, UseSendOptions, UseSendReturn } from './use-send.types';
+import { fetchBioforestFee, submitBioforestTransfer } from './use-send.bioforest';
+import { fetchWeb3Fee, submitWeb3Transfer, validateWeb3Address } from './use-send.web3';
+import { adjustAmountForFee, canProceedToConfirm, validateAddressInput, validateAmountInput } from './use-send.logic';
+import { submitMockTransfer } from './use-send.mock';
+import i18n from '@/i18n';
+
+const t = i18n.t.bind(i18n);
/**
* Hook for managing send flow state
*/
export function useSend(options: UseSendOptions = {}): UseSendReturn {
- const { initialAsset, useMock = true, walletId, fromAddress, chainConfig, getBalance } = options
+ const { initialAsset, useMock = true, walletId, fromAddress, chainConfig, getBalance } = options;
const [state, setState] = useState({
...initialState,
asset: initialAsset ?? null,
- })
+ });
- const isBioforestChain = chainConfig?.chainKind === 'bioforest'
- const isWeb3Chain = chainConfig?.chainKind === 'evm' || chainConfig?.chainKind === 'tron' || chainConfig?.chainKind === 'bitcoin'
+ const isBioforestChain = chainConfig?.chainKind === 'bioforest';
+ const isWeb3Chain =
+ chainConfig?.chainKind === 'evm' || chainConfig?.chainKind === 'tron' || chainConfig?.chainKind === 'bitcoin';
// Validate address
- const validateAddress = useCallback((address: string): string | null => {
- // Use chain adapter validation for Web3 chains
- if (isWeb3Chain && chainConfig) {
- return validateWeb3Address(chainConfig, address)
- }
- return validateAddressInput(address, isBioforestChain, fromAddress)
- }, [isBioforestChain, isWeb3Chain, chainConfig, fromAddress])
+ const validateAddress = useCallback(
+ (address: string): string | null => {
+ // Use chain adapter validation for Web3 chains
+ if (isWeb3Chain && chainConfig) {
+ return validateWeb3Address(chainConfig, address);
+ }
+ return validateAddressInput(address, isBioforestChain, fromAddress);
+ },
+ [isBioforestChain, isWeb3Chain, chainConfig, fromAddress],
+ );
// Validate amount
const validateAmount = useCallback((amount: Amount | null, asset: AssetInfo | null): string | null => {
- return validateAmountInput(amount, asset)
- }, [])
+ return validateAmountInput(amount, asset);
+ }, []);
// Set recipient address
const setToAddress = useCallback((address: string) => {
@@ -42,8 +49,8 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn {
...prev,
toAddress: address,
addressError: null, // Clear error on change
- }))
- }, [])
+ }));
+ }, []);
// Set amount
const setAmount = useCallback((amount: Amount | null) => {
@@ -51,82 +58,85 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn {
...prev,
amount,
amountError: null, // Clear error on change
- }))
- }, [])
+ }));
+ }, []);
// Set custom fee (from FeeEditJob modal)
const setFee = useCallback((formattedFee: string) => {
setState((prev) => {
- if (!prev.feeAmount) return prev
- const newFeeAmount = Amount.fromFormatted(formattedFee, prev.feeAmount.decimals, prev.feeAmount.symbol)
+ if (!prev.feeAmount) return prev;
+ const newFeeAmount = Amount.fromFormatted(formattedFee, prev.feeAmount.decimals, prev.feeAmount.symbol);
return {
...prev,
feeAmount: newFeeAmount,
- }
- })
- }, [])
+ };
+ });
+ }, []);
// Set asset and estimate fee
- const setAsset = useCallback((asset: AssetInfo) => {
- setState((prev) => ({
- ...prev,
- asset,
- feeLoading: true,
- }))
-
- const shouldUseMock = useMock || (!isBioforestChain && !isWeb3Chain) || !chainConfig || !fromAddress
-
- if (shouldUseMock) {
- // Mock fee estimation delay
- setTimeout(() => {
- const fee = MOCK_FEES[asset.assetType] ?? { amount: '0.001', symbol: asset.assetType }
- const feeAmount = Amount.fromFormatted(fee.amount, asset.decimals, fee.symbol)
- setState((prev) => ({
- ...prev,
- feeAmount: feeAmount,
- feeMinAmount: feeAmount,
- feeSymbol: fee.symbol,
- feeLoading: false,
- }))
- }, 300)
- return
- }
-
- void (async () => {
- try {
- // Use appropriate fee fetcher based on chain type
- const feeEstimate = isWeb3Chain
- ? await fetchWeb3Fee(chainConfig, fromAddress)
- : await fetchBioforestFee(chainConfig, fromAddress)
-
- setState((prev) => ({
- ...prev,
- feeAmount: feeEstimate.amount,
- feeMinAmount: feeEstimate.amount,
- feeSymbol: feeEstimate.symbol,
- feeLoading: false,
- }))
- } catch (error) {
- setState((prev) => ({
- ...prev,
- feeLoading: false,
- errorMessage: error instanceof Error ? error.message : '获取手续费失败',
- }))
+ const setAsset = useCallback(
+ (asset: AssetInfo) => {
+ setState((prev) => ({
+ ...prev,
+ asset,
+ feeLoading: true,
+ }));
+
+ const shouldUseMock = useMock || (!isBioforestChain && !isWeb3Chain) || !chainConfig || !fromAddress;
+
+ if (shouldUseMock) {
+ // Mock fee estimation delay
+ setTimeout(() => {
+ const fee = MOCK_FEES[asset.assetType] ?? { amount: '0.001', symbol: asset.assetType };
+ const feeAmount = Amount.fromFormatted(fee.amount, asset.decimals, fee.symbol);
+ setState((prev) => ({
+ ...prev,
+ feeAmount: feeAmount,
+ feeMinAmount: feeAmount,
+ feeSymbol: fee.symbol,
+ feeLoading: false,
+ }));
+ }, 300);
+ return;
}
- })()
- }, [chainConfig, fromAddress, isBioforestChain, isWeb3Chain, useMock])
+
+ void (async () => {
+ try {
+ // Use appropriate fee fetcher based on chain type
+ const feeEstimate = isWeb3Chain
+ ? await fetchWeb3Fee(chainConfig, fromAddress)
+ : await fetchBioforestFee(chainConfig, fromAddress);
+
+ setState((prev) => ({
+ ...prev,
+ feeAmount: feeEstimate.amount,
+ feeMinAmount: feeEstimate.amount,
+ feeSymbol: feeEstimate.symbol,
+ feeLoading: false,
+ }));
+ } catch (error) {
+ setState((prev) => ({
+ ...prev,
+ feeLoading: false,
+ errorMessage: error instanceof Error ? error.message : t('error:transaction.feeEstimateFailed'),
+ }));
+ }
+ })();
+ },
+ [chainConfig, fromAddress, isBioforestChain, isWeb3Chain, useMock],
+ );
// Get current balance from external source (single source of truth)
const currentBalance = useMemo(() => {
- if (!state.asset?.assetType || !getBalance) return state.asset?.amount ?? null
- return getBalance(state.asset.assetType) ?? state.asset?.amount ?? null
- }, [state.asset?.assetType, state.asset?.amount, getBalance])
+ if (!state.asset?.assetType || !getBalance) return state.asset?.amount ?? null;
+ return getBalance(state.asset.assetType) ?? state.asset?.amount ?? null;
+ }, [state.asset?.assetType, state.asset?.amount, getBalance]);
// Create asset with current balance for validation
const assetWithCurrentBalance = useMemo((): AssetInfo | null => {
- if (!state.asset || !currentBalance) return state.asset
- return { ...state.asset, amount: currentBalance }
- }, [state.asset, currentBalance])
+ if (!state.asset || !currentBalance) return state.asset;
+ return { ...state.asset, amount: currentBalance };
+ }, [state.asset, currentBalance]);
// Check if can proceed
const canProceed = useMemo(() => {
@@ -136,37 +146,39 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn {
asset: assetWithCurrentBalance,
isBioforestChain,
feeLoading: state.feeLoading,
- })
- }, [isBioforestChain, state.amount, assetWithCurrentBalance, state.toAddress, state.feeLoading])
+ });
+ }, [isBioforestChain, state.amount, assetWithCurrentBalance, state.toAddress, state.feeLoading]);
// Validate and go to confirm
const goToConfirm = useCallback((): boolean => {
- const addressError = validateAddress(state.toAddress)
- const amountError = assetWithCurrentBalance ? validateAmount(state.amount, assetWithCurrentBalance) : '请选择资产'
+ const addressError = validateAddress(state.toAddress);
+ const amountError = assetWithCurrentBalance
+ ? validateAmount(state.amount, assetWithCurrentBalance)
+ : t('error:validation.selectAsset');
if (addressError || amountError) {
setState((prev) => ({
...prev,
addressError,
amountError,
- }))
- return false
+ }));
+ return false;
}
if (assetWithCurrentBalance && state.feeAmount && state.amount) {
- const adjustResult = adjustAmountForFee(state.amount, assetWithCurrentBalance, state.feeAmount)
+ const adjustResult = adjustAmountForFee(state.amount, assetWithCurrentBalance, state.feeAmount);
if (adjustResult.status === 'error') {
setState((prev) => ({
...prev,
amountError: adjustResult.message,
- }))
- return false
+ }));
+ return false;
}
if (adjustResult.adjustedAmount) {
setState((prev) => ({
...prev,
amount: adjustResult.adjustedAmount ?? prev.amount,
- }))
+ }));
}
}
@@ -175,55 +187,135 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn {
step: 'confirm',
addressError: null,
amountError: null,
- }))
- return true
- }, [state.toAddress, state.amount, assetWithCurrentBalance, state.feeAmount, validateAddress, validateAmount])
+ }));
+ return true;
+ }, [state.toAddress, state.amount, assetWithCurrentBalance, state.feeAmount, validateAddress, validateAmount]);
// Go back to input
const goBack = useCallback(() => {
setState((prev) => ({
...prev,
step: 'input',
- }))
- }, [])
+ }));
+ }, []);
// Submit transaction
- const submit = useCallback(async (password: string) => {
-
+ const submit = useCallback(
+ async (password: string) => {
+ if (useMock) {
+ const result = await submitMockTransfer(setState);
+ return result.status === 'ok' ? { status: 'ok' as const } : { status: 'error' as const };
+ }
- if (useMock) {
+ if (!chainConfig) {
+ setState((prev) => ({
+ ...prev,
+ step: 'result',
+ isSubmitting: false,
+ resultStatus: 'failed',
+ txHash: null,
+ errorMessage: t('error:transaction.chainConfigMissing'),
+ }));
+ return { status: 'error' as const };
+ }
- const result = await submitMockTransfer(setState)
- return result.status === 'ok' ? { status: 'ok' as const } : { status: 'error' as const }
- }
+ // Handle Web3 chains (EVM, Tron, Bitcoin)
+ if (chainConfig.chainKind === 'evm' || chainConfig.chainKind === 'tron' || chainConfig.chainKind === 'bitcoin') {
+ if (!walletId || !fromAddress || !state.asset || !state.amount) {
+ setState((prev) => ({
+ ...prev,
+ step: 'result',
+ isSubmitting: false,
+ resultStatus: 'failed',
+ txHash: null,
+ errorMessage: t('error:transaction.walletInfoIncomplete'),
+ }));
+ return { status: 'error' as const };
+ }
- if (!chainConfig) {
+ setState((prev) => ({
+ ...prev,
+ step: 'sending',
+ isSubmitting: true,
+ errorMessage: null,
+ }));
+
+ const result = await submitWeb3Transfer({
+ chainConfig,
+ walletId,
+ password,
+ fromAddress,
+ toAddress: state.toAddress,
+ amount: state.amount,
+ });
+
+ if (result.status === 'password') {
+ setState((prev) => ({
+ ...prev,
+ step: 'confirm',
+ isSubmitting: false,
+ }));
+ return { status: 'password' as const };
+ }
+
+ if (result.status === 'error') {
+ setState((prev) => ({
+ ...prev,
+ step: 'result',
+ isSubmitting: false,
+ resultStatus: 'failed',
+ txHash: null,
+ errorMessage: result.message,
+ }));
+ return { status: 'error' as const };
+ }
- setState((prev) => ({
- ...prev,
- step: 'result',
- isSubmitting: false,
- resultStatus: 'failed',
- txHash: null,
- errorMessage: '链配置缺失',
- }))
- return { status: 'error' as const }
- }
+ setState((prev) => ({
+ ...prev,
+ step: 'result',
+ isSubmitting: false,
+ resultStatus: 'success',
+ txHash: result.txHash,
+ errorMessage: null,
+ }));
- // Handle Web3 chains (EVM, Tron, Bitcoin)
- if (chainConfig.chainKind === 'evm' || chainConfig.chainKind === 'tron' || chainConfig.chainKind === 'bitcoin') {
+ return { status: 'ok' as const, txHash: result.txHash };
+ }
+ // Unsupported chain type
+ if (chainConfig.chainKind !== 'bioforest') {
+ setState((prev) => ({
+ ...prev,
+ step: 'result',
+ isSubmitting: false,
+ resultStatus: 'failed',
+ txHash: null,
+ errorMessage: t('error:transaction.unsupportedChainType', { chainType: chainConfig.chainKind }),
+ }));
+ return { status: 'error' as const };
+ }
- if (!walletId || !fromAddress || !state.asset || !state.amount) {
+ if (!walletId || !fromAddress || !state.asset) {
setState((prev) => ({
...prev,
step: 'result',
isSubmitting: false,
resultStatus: 'failed',
txHash: null,
- errorMessage: '钱包信息不完整',
- }))
- return { status: 'error' as const }
+ errorMessage: t('error:wallet.incompleteInfo'),
+ }));
+ return { status: 'error' as const };
+ }
+
+ const addressError = validateAddress(state.toAddress);
+ const amountError = validateAmount(state.amount, state.asset);
+ if (addressError || amountError) {
+ setState((prev) => ({
+ ...prev,
+ addressError,
+ amountError,
+ }));
+ return { status: 'error' as const };
}
setState((prev) => ({
@@ -231,24 +323,49 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn {
step: 'sending',
isSubmitting: true,
errorMessage: null,
- }))
+ }));
+
+ // Amount should never be null here (validated above)
+ if (!state.amount) {
+ setState((prev) => ({
+ ...prev,
+ step: 'result',
+ isSubmitting: false,
+ resultStatus: 'failed',
+ txHash: null,
+ errorMessage: t('error:transaction.invalidAmount'),
+ }));
+ return { status: 'error' as const };
+ }
- const result = await submitWeb3Transfer({
+ const result = await submitBioforestTransfer({
chainConfig,
walletId,
password,
fromAddress,
toAddress: state.toAddress,
amount: state.amount,
- })
+ assetType: state.asset.assetType,
+ fee: state.feeAmount ?? undefined,
+ });
if (result.status === 'password') {
setState((prev) => ({
...prev,
step: 'confirm',
isSubmitting: false,
- }))
- return { status: 'password' as const }
+ }));
+ return { status: 'password' as const };
+ }
+
+ if (result.status === 'password_required') {
+ // Pay password is required - return status so UI can prompt for pay password
+ setState((prev) => ({
+ ...prev,
+ step: 'confirm',
+ isSubmitting: false,
+ }));
+ return { status: 'two_step_secret_required' as const, secondPublicKey: result.secondPublicKey };
}
if (result.status === 'error') {
@@ -259,8 +376,8 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn {
resultStatus: 'failed',
txHash: null,
errorMessage: result.message,
- }))
- return { status: 'error' as const }
+ }));
+ return { status: 'error' as const, message: result.message, pendingTxId: result.pendingTxId };
}
setState((prev) => ({
@@ -270,214 +387,118 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn {
resultStatus: 'success',
txHash: result.txHash,
errorMessage: null,
- }))
-
- return { status: 'ok' as const, txHash: result.txHash }
- }
-
- // Unsupported chain type
- if (chainConfig.chainKind !== 'bioforest') {
-
- setState((prev) => ({
- ...prev,
- step: 'result',
- isSubmitting: false,
- resultStatus: 'failed',
- txHash: null,
- errorMessage: `不支持的链类型: ${chainConfig.chainKind}`,
- }))
- return { status: 'error' as const }
- }
-
- if (!walletId || !fromAddress || !state.asset) {
- setState((prev) => ({
- ...prev,
- step: 'result',
- isSubmitting: false,
- resultStatus: 'failed',
- txHash: null,
- errorMessage: '钱包信息不完整',
- }))
- return { status: 'error' as const }
- }
-
- const addressError = validateAddress(state.toAddress)
- const amountError = validateAmount(state.amount, state.asset)
- if (addressError || amountError) {
- setState((prev) => ({
- ...prev,
- addressError,
- amountError,
- }))
- return { status: 'error' as const }
- }
-
- setState((prev) => ({
- ...prev,
- step: 'sending',
- isSubmitting: true,
- errorMessage: null,
- }))
-
- // Amount should never be null here (validated above)
- if (!state.amount) {
- setState((prev) => ({
- ...prev,
- step: 'result',
- isSubmitting: false,
- resultStatus: 'failed',
- txHash: null,
- errorMessage: '无效的金额',
- }))
- return { status: 'error' as const }
- }
+ }));
- const result = await submitBioforestTransfer({
+ return { status: 'ok' as const, txHash: result.txHash, pendingTxId: result.pendingTxId };
+ },
+ [
chainConfig,
- walletId,
- password,
fromAddress,
- toAddress: state.toAddress,
- amount: state.amount,
- assetType: state.asset.assetType,
- fee: state.feeAmount ?? undefined,
- })
-
- if (result.status === 'password') {
- setState((prev) => ({
- ...prev,
- step: 'confirm',
- isSubmitting: false,
- }))
- return { status: 'password' as const }
- }
+ state.amount,
+ state.asset,
+ state.toAddress,
+ useMock,
+ validateAddress,
+ validateAmount,
+ walletId,
+ ],
+ );
- if (result.status === 'password_required') {
- // Pay password is required - return status so UI can prompt for pay password
- setState((prev) => ({
- ...prev,
- step: 'confirm',
- isSubmitting: false,
- }))
- return { status: 'two_step_secret_required' as const, secondPublicKey: result.secondPublicKey }
- }
+ // Submit with pay password (for addresses with secondPublicKey)
+ const submitWithTwoStepSecret = useCallback(
+ async (password: string, twoStepSecret: string) => {
+ if (!chainConfig || !walletId || !fromAddress) {
+ return { status: 'error' as const };
+ }
- if (result.status === 'error') {
setState((prev) => ({
...prev,
- step: 'result',
- isSubmitting: false,
- resultStatus: 'failed',
+ step: 'sending',
+ isSubmitting: true,
+ resultStatus: null,
txHash: null,
- errorMessage: result.message,
- }))
- return { status: 'error' as const, message: result.message, pendingTxId: result.pendingTxId }
- }
-
- setState((prev) => ({
- ...prev,
- step: 'result',
- isSubmitting: false,
- resultStatus: 'success',
- txHash: result.txHash,
- errorMessage: null,
- }))
-
- return { status: 'ok' as const, txHash: result.txHash, pendingTxId: result.pendingTxId }
- }, [chainConfig, fromAddress, state.amount, state.asset, state.toAddress, useMock, validateAddress, validateAmount, walletId])
+ errorMessage: null,
+ }));
- // Submit with pay password (for addresses with secondPublicKey)
- const submitWithTwoStepSecret = useCallback(async (password: string, twoStepSecret: string) => {
- if (!chainConfig || !walletId || !fromAddress) {
- return { status: 'error' as const }
- }
+ if (!state.amount || !state.asset) {
+ setState((prev) => ({
+ ...prev,
+ step: 'result',
+ isSubmitting: false,
+ resultStatus: 'failed',
+ txHash: null,
+ errorMessage: t('error:transaction.invalidAmount'),
+ }));
+ return { status: 'error' as const };
+ }
- setState((prev) => ({
- ...prev,
- step: 'sending',
- isSubmitting: true,
- resultStatus: null,
- txHash: null,
- errorMessage: null,
- }))
-
- if (!state.amount || !state.asset) {
- setState((prev) => ({
- ...prev,
- step: 'result',
- isSubmitting: false,
- resultStatus: 'failed',
- txHash: null,
- errorMessage: '无效的金额',
- }))
- return { status: 'error' as const }
- }
+ const result = await submitBioforestTransfer({
+ chainConfig,
+ walletId,
+ password,
+ fromAddress,
+ toAddress: state.toAddress,
+ amount: state.amount,
+ assetType: state.asset.assetType,
+ fee: state.feeAmount ?? undefined,
+ twoStepSecret,
+ });
- const result = await submitBioforestTransfer({
- chainConfig,
- walletId,
- password,
- fromAddress,
- toAddress: state.toAddress,
- amount: state.amount,
- assetType: state.asset.assetType,
- fee: state.feeAmount ?? undefined,
- twoStepSecret,
- })
+ if (result.status === 'password') {
+ setState((prev) => ({
+ ...prev,
+ step: 'confirm',
+ isSubmitting: false,
+ }));
+ return { status: 'password' as const };
+ }
- if (result.status === 'password') {
- setState((prev) => ({
- ...prev,
- step: 'confirm',
- isSubmitting: false,
- }))
- return { status: 'password' as const }
- }
+ if (result.status === 'error') {
+ setState((prev) => ({
+ ...prev,
+ step: 'result',
+ isSubmitting: false,
+ resultStatus: 'failed',
+ txHash: null,
+ errorMessage: result.message,
+ }));
+ return { status: 'error' as const, message: result.message, pendingTxId: result.pendingTxId };
+ }
- if (result.status === 'error') {
- setState((prev) => ({
- ...prev,
- step: 'result',
- isSubmitting: false,
- resultStatus: 'failed',
- txHash: null,
- errorMessage: result.message,
- }))
- return { status: 'error' as const, message: result.message, pendingTxId: result.pendingTxId }
- }
+ // password_required should not happen when twoStepSecret is provided
+ if (result.status === 'password_required') {
+ setState((prev) => ({
+ ...prev,
+ step: 'result',
+ isSubmitting: false,
+ resultStatus: 'failed',
+ txHash: null,
+ errorMessage: t('error:transaction.securityPasswordFailed'),
+ }));
+ return { status: 'error' as const };
+ }
- // password_required should not happen when twoStepSecret is provided
- if (result.status === 'password_required') {
setState((prev) => ({
...prev,
step: 'result',
isSubmitting: false,
- resultStatus: 'failed',
- txHash: null,
- errorMessage: '安全密码验证失败',
- }))
- return { status: 'error' as const }
- }
-
- setState((prev) => ({
- ...prev,
- step: 'result',
- isSubmitting: false,
- resultStatus: 'success',
- txHash: result.txHash,
- errorMessage: null,
- }))
+ resultStatus: 'success',
+ txHash: result.txHash,
+ errorMessage: null,
+ }));
- return { status: 'ok' as const, txHash: result.txHash, pendingTxId: result.pendingTxId }
- }, [chainConfig, fromAddress, state.amount, state.toAddress, walletId])
+ return { status: 'ok' as const, txHash: result.txHash, pendingTxId: result.pendingTxId };
+ },
+ [chainConfig, fromAddress, state.amount, state.toAddress, walletId],
+ );
// Reset to initial state
const reset = useCallback(() => {
setState({
...initialState,
asset: initialAsset ?? null,
- })
- }, [initialAsset])
+ });
+ }, [initialAsset]);
return {
state,
@@ -491,5 +512,5 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn {
submitWithTwoStepSecret,
reset,
canProceed,
- }
+ };
}
diff --git a/src/hooks/use-send.web3.ts b/src/hooks/use-send.web3.ts
index 4e77e1b2a..ff5e9196b 100644
--- a/src/hooks/use-send.web3.ts
+++ b/src/hooks/use-send.web3.ts
@@ -1,26 +1,29 @@
/**
* Web3 Transfer Implementation
- *
+ *
* 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 { getChainProvider } from '@/services/chain-adapter/providers'
-import { mnemonicToSeedSync } from '@scure/bip39'
+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 { getChainProvider } from '@/services/chain-adapter/providers';
+import { mnemonicToSeedSync } from '@scure/bip39';
+import i18n from '@/i18n';
+
+const t = i18n.t.bind(i18n);
export interface Web3FeeResult {
- amount: Amount
- symbol: string
+ amount: Amount;
+ symbol: string;
}
export async function fetchWeb3Fee(chainConfig: ChainConfig, fromAddress: string): Promise {
- const chainProvider = getChainProvider(chainConfig.id)
+ const chainProvider = getChainProvider(chainConfig.id);
if (!chainProvider.supportsFeeEstimate || !chainProvider.supportsBuildTransaction) {
- throw new Error(`Chain ${chainConfig.id} does not support fee estimation`)
+ throw new Error(`Chain ${chainConfig.id} does not support fee estimation`);
}
// 新流程:先构建交易,再估算手续费
@@ -29,40 +32,40 @@ export async function fetchWeb3Fee(chainConfig: ChainConfig, fromAddress: string
from: fromAddress,
to: fromAddress,
amount: Amount.fromRaw('1', chainConfig.decimals, chainConfig.symbol),
- })
+ });
- const feeEstimate = await chainProvider.estimateFee!(unsignedTx)
+ const feeEstimate = await chainProvider.estimateFee!(unsignedTx);
return {
amount: feeEstimate.standard.amount,
symbol: chainConfig.symbol,
- }
+ };
}
export async function fetchWeb3Balance(chainConfig: ChainConfig, fromAddress: string): Promise {
- const chainProvider = getChainProvider(chainConfig.id)
- const balance = await chainProvider.nativeBalance.fetch({ address: fromAddress })
+ const chainProvider = getChainProvider(chainConfig.id);
+ const balance = await chainProvider.nativeBalance.fetch({ address: fromAddress });
return {
assetType: balance.symbol,
name: chainConfig.name,
amount: balance.amount,
decimals: balance.amount.decimals,
- }
+ };
}
export type SubmitWeb3Result =
| { status: 'ok'; txHash: string }
| { status: 'password' }
- | { status: 'error'; message: string }
+ | { status: 'error'; message: string };
export interface SubmitWeb3Params {
- chainConfig: ChainConfig
- walletId: string
- password: string
- fromAddress: string
- toAddress: string
- amount: Amount
+ chainConfig: ChainConfig;
+ walletId: string;
+ password: string;
+ fromAddress: string;
+ toAddress: string;
+ amount: Amount;
}
export async function submitWeb3Transfer({
@@ -74,34 +77,32 @@ export async function submitWeb3Transfer({
amount,
}: SubmitWeb3Params): Promise {
// Get mnemonic from wallet storage
- let mnemonic: string
+ let mnemonic: string;
try {
- mnemonic = await walletStorageService.getMnemonic(walletId, password)
+ mnemonic = await walletStorageService.getMnemonic(walletId, password);
} catch (error) {
if (error instanceof WalletStorageError && error.code === WalletStorageErrorCode.DECRYPTION_FAILED) {
- return { status: 'password' }
+ return { status: 'password' };
}
return {
status: 'error',
- message: error instanceof Error ? error.message : '未知错误',
- }
+ message: error instanceof Error ? error.message : t('error:transaction.unknownError'),
+ };
}
if (!amount.isPositive()) {
- return { status: 'error', message: '请输入有效金额' }
+ return { status: 'error', message: t('error:validation.enterValidAmount') };
}
try {
- const chainProvider = getChainProvider(chainConfig.id)
+ const chainProvider = getChainProvider(chainConfig.id);
if (!chainProvider.supportsFullTransaction) {
- return { status: 'error', message: `该链不支持完整交易流程: ${chainConfig.id}` }
+ return { status: 'error', message: t('error:transaction.chainNotSupported', { chainId: chainConfig.id }) };
}
-
-
// Derive private key from mnemonic
- const seed = mnemonicToSeedSync(mnemonic)
+ const seed = mnemonicToSeedSync(mnemonic);
// Build unsigned transaction
const unsignedTx = await chainProvider.buildTransaction!({
@@ -109,38 +110,49 @@ export async function submitWeb3Transfer({
from: fromAddress,
to: toAddress,
amount,
- })
+ });
// Sign transaction
- const signedTx = await chainProvider.signTransaction!(unsignedTx, { privateKey: seed })
+ const signedTx = await chainProvider.signTransaction!(unsignedTx, { privateKey: seed });
// Broadcast transaction
- const txHash = await chainProvider.broadcastTransaction!(signedTx)
+ const txHash = await chainProvider.broadcastTransaction!(signedTx);
-
- return { status: 'ok', txHash }
+ return { status: 'ok', txHash };
} catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ // Handle specific error cases
+ if (errorMessage.includes('insufficient') || errorMessage.includes('balance')) {
+ return { status: 'error', message: t('error:transaction.insufficientBalance') };
+ }
- const errorMessage = error instanceof Error ? error.message : String(error)
+ if (errorMessage.includes('fee') || errorMessage.includes('gas')) {
+ return { status: 'error', message: t('error:transaction.insufficientFee') };
+ }
- // Handle specific error cases
- if (errorMessage.includes('insufficient') || errorMessage.includes('余额不足')) {
- return { status: 'error', message: '余额不足' }
+ if (errorMessage.includes('not yet implemented') || errorMessage.includes('not supported')) {
+ return { status: 'error', message: t('error:transaction.featureNotImplemented') };
}
+ return {
+ status: 'error',
+ message: errorMessage || t('error:transaction.transactionFailed'),
+ };
+
if (errorMessage.includes('fee') || errorMessage.includes('手续费') || errorMessage.includes('gas')) {
- return { status: 'error', message: '手续费不足' }
+ // i18n-ignore
+ return { status: 'error', message: t('error:transaction.insufficientGas') };
}
if (errorMessage.includes('not yet implemented') || errorMessage.includes('not supported')) {
- return { status: 'error', message: '该链转账功能尚未完全实现' }
+ return { status: 'error', message: t('error:chain.transferNotImplemented') };
}
return {
status: 'error',
- message: errorMessage || '交易失败,请稍后重试',
- }
+ message: errorMessage || t('error:transaction.retryLater'),
+ };
}
}
@@ -148,19 +160,27 @@ export async function submitWeb3Transfer({
* Validate address for Web3 chains
*/
export function validateWeb3Address(chainConfig: ChainConfig, address: string): string | null {
- const chainProvider = getChainProvider(chainConfig.id)
+ const chainProvider = getChainProvider(chainConfig.id);
if (!chainProvider.supportsAddressValidation) {
- return '不支持的链类型'
+ return t('error:validation.unsupportedChainType');
+ }
+
+ if (!address || address.trim() === '') {
+ return t('error:validation.enterRecipientAddress');
+ }
+
+ if (!chainProvider.isValidAddress!(address)) {
+ return t('error:validation.invalidAddressFormat');
}
if (!address || address.trim() === '') {
- return '请输入收款地址'
+ return t('error:validation.enterReceiverAddress');
}
if (!chainProvider.isValidAddress!(address)) {
- return '无效的地址格式'
+ return t('error:validation.invalidAddress');
}
- return null
+ return null;
}
diff --git a/src/hooks/use-transaction-history.ts b/src/hooks/use-transaction-history.ts
index d03179293..d392d24d4 100644
--- a/src/hooks/use-transaction-history.ts
+++ b/src/hooks/use-transaction-history.ts
@@ -1,32 +1,39 @@
-import { useState, useCallback, useEffect } from 'react'
-import type { ChainType } from '@/stores'
-import type { TransactionInfo } from '@/components/transaction/transaction-item'
-import { Amount } from '@/types/amount'
-import { transactionService, type TransactionRecord as ServiceTransactionRecord, type TransactionFilter as ServiceFilter } from '@/services/transaction'
+import { useState, useCallback, useEffect } from 'react';
+import type { ChainType } from '@/stores';
+import type { TransactionInfo } from '@/components/transaction/transaction-item';
+import { Amount } from '@/types/amount';
+import {
+ transactionService,
+ type TransactionRecord as ServiceTransactionRecord,
+ type TransactionFilter as ServiceFilter,
+} from '@/services/transaction';
+import i18n from '@/i18n';
+
+const t = i18n.t.bind(i18n);
/** 交易历史过滤器 */
export interface TransactionFilter {
- chain?: ChainType | 'all' | undefined
- period?: '7d' | '30d' | '90d' | 'all' | undefined
+ chain?: ChainType | 'all' | undefined;
+ period?: '7d' | '30d' | '90d' | 'all' | undefined;
}
/** 扩展的交易信息(包含链类型)- 保持与组件兼容 */
export interface TransactionRecord extends TransactionInfo {
- chain: ChainType
- fee: Amount | undefined
- feeSymbol: string | undefined
- blockNumber: number | undefined
- confirmations: number | undefined
+ chain: ChainType;
+ fee: Amount | undefined;
+ feeSymbol: string | undefined;
+ blockNumber: number | undefined;
+ confirmations: number | undefined;
}
/** Hook 返回类型 */
export interface UseTransactionHistoryResult {
- transactions: TransactionRecord[]
- isLoading: boolean
- error: string | undefined
- filter: TransactionFilter
- setFilter: (filter: TransactionFilter) => void
- refresh: () => Promise
+ transactions: TransactionRecord[];
+ isLoading: boolean;
+ error: string | undefined;
+ filter: TransactionFilter;
+ setFilter: (filter: TransactionFilter) => void;
+ refresh: () => Promise;
}
/** 将 Service 记录转换为组件兼容格式 */
@@ -45,26 +52,26 @@ function convertToComponentFormat(record: ServiceTransactionRecord): Transaction
feeSymbol: record.feeSymbol,
blockNumber: record.blockNumber,
confirmations: record.confirmations,
- }
+ };
}
/** 交易历史 Hook */
export function useTransactionHistory(walletId?: string): UseTransactionHistoryResult {
- const [transactions, setTransactions] = useState([])
- const [isLoading, setIsLoading] = useState(true)
- const [error, setError] = useState()
- const [filter, setFilter] = useState({ chain: 'all', period: 'all' })
+ const [transactions, setTransactions] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState();
+ const [filter, setFilter] = useState({ chain: 'all', period: 'all' });
// 获取交易历史
const fetchTransactions = useCallback(async () => {
if (!walletId) {
- setTransactions([])
- setIsLoading(false)
- return
+ setTransactions([]);
+ setIsLoading(false);
+ return;
}
- setIsLoading(true)
- setError(undefined)
+ setIsLoading(true);
+ setError(undefined);
try {
const serviceFilter: ServiceFilter = {
@@ -72,26 +79,26 @@ export function useTransactionHistory(walletId?: string): UseTransactionHistoryR
period: filter.period ?? 'all',
type: undefined,
status: undefined,
- }
- const rawRecords = await transactionService.getHistory({ walletId, filter: serviceFilter })
+ };
+ const rawRecords = await transactionService.getHistory({ walletId, filter: serviceFilter });
// Records from service already have Amount objects
- setTransactions(rawRecords.map(convertToComponentFormat))
+ setTransactions(rawRecords.map(convertToComponentFormat));
} catch (e) {
- setError(e instanceof Error ? e.message : '加载交易历史失败')
+ setError(e instanceof Error ? e.message : t('error:history.loadFailed'));
} finally {
- setIsLoading(false)
+ setIsLoading(false);
}
- }, [walletId, filter])
+ }, [walletId, filter]);
// 初始加载和过滤器变化时重新获取
useEffect(() => {
- void fetchTransactions()
- }, [fetchTransactions])
+ void fetchTransactions();
+ }, [fetchTransactions]);
// 刷新
const refresh = useCallback(async () => {
- await fetchTransactions()
- }, [fetchTransactions])
+ await fetchTransactions();
+ }, [fetchTransactions]);
return {
transactions,
@@ -100,5 +107,5 @@ export function useTransactionHistory(walletId?: string): UseTransactionHistoryR
filter,
setFilter,
refresh,
- }
+ };
}
diff --git a/src/hooks/useWalletTheme.ts b/src/hooks/useWalletTheme.ts
index 0dc2dc1c6..d6bcd254a 100644
--- a/src/hooks/useWalletTheme.ts
+++ b/src/hooks/useWalletTheme.ts
@@ -1,58 +1,58 @@
-import { useCallback, useEffect } from 'react'
-import { useStore } from '@tanstack/react-store'
-import { walletStore } from '@/stores'
+import { useCallback, useEffect } from 'react';
+import { useStore } from '@tanstack/react-store';
+import { walletStore } from '@/stores';
-const THEME_HUE_CACHE_KEY = 'wallet_theme_hue_cache'
+const THEME_HUE_CACHE_KEY = 'wallet_theme_hue_cache';
/** 预设主题色 (oklch hue 角度) */
export const WALLET_THEME_PRESETS = {
- purple: 323, // 默认紫色
- blue: 250, // 蓝色
- cyan: 200, // 青色
- green: 145, // 绿色
- yellow: 85, // 黄色
- orange: 45, // 橙色
- red: 25, // 红色
- pink: 350, // 粉色
- magenta: 310, // 洋红
-} as const
-
-export type WalletThemePreset = keyof typeof WALLET_THEME_PRESETS
+ purple: 323, // 默认紫色
+ blue: 250, // 蓝色
+ cyan: 200, // 青色
+ green: 145, // 绿色
+ yellow: 85, // 黄色
+ orange: 45, // 橙色
+ red: 25, // 红色
+ pink: 350, // 粉色
+ magenta: 310, // 洋红
+} as const;
+
+export type WalletThemePreset = keyof typeof WALLET_THEME_PRESETS;
/** 主题色配置(包含名称和展示色) */
export const WALLET_THEME_COLORS = [
- { name: '紫色', hue: 323, color: 'oklch(0.6 0.25 323)' },
- { name: '蓝色', hue: 250, color: 'oklch(0.55 0.25 250)' },
- { name: '青色', hue: 200, color: 'oklch(0.65 0.2 200)' },
- { name: '绿色', hue: 145, color: 'oklch(0.6 0.2 145)' },
- { name: '黄色', hue: 85, color: 'oklch(0.75 0.18 85)' },
- { name: '橙色', hue: 45, color: 'oklch(0.7 0.2 45)' },
- { name: '红色', hue: 25, color: 'oklch(0.6 0.25 25)' },
- { name: '粉色', hue: 350, color: 'oklch(0.7 0.2 350)' },
- { name: '洋红', hue: 310, color: 'oklch(0.6 0.25 310)' },
-] as const
+ { nameKey: 'colors.purple', hue: 323, color: 'oklch(0.6 0.25 323)' },
+ { nameKey: 'colors.blue', hue: 250, color: 'oklch(0.55 0.25 250)' },
+ { nameKey: 'colors.cyan', hue: 200, color: 'oklch(0.65 0.2 200)' },
+ { nameKey: 'colors.green', hue: 145, color: 'oklch(0.6 0.2 145)' },
+ { nameKey: 'colors.yellow', hue: 85, color: 'oklch(0.75 0.18 85)' },
+ { nameKey: 'colors.orange', hue: 45, color: 'oklch(0.7 0.2 45)' },
+ { nameKey: 'colors.red', hue: 25, color: 'oklch(0.6 0.25 25)' },
+ { nameKey: 'colors.pink', hue: 350, color: 'oklch(0.7 0.2 350)' },
+ { nameKey: 'colors.magenta', hue: 310, color: 'oklch(0.6 0.25 310)' },
+] as const;
/**
* 基于助记词/密钥稳定派生主题色 hue
* 使用简单哈希算法生成 0-360 的色相值
*/
export function deriveThemeHue(secret: string): number {
- let hash = 0
+ let hash = 0;
for (let i = 0; i < secret.length; i++) {
- const char = secret.charCodeAt(i)
- hash = ((hash << 5) - hash) + char
- hash = hash & hash // Convert to 32bit integer
+ const char = secret.charCodeAt(i);
+ hash = (hash << 5) - hash + char;
+ hash = hash & hash; // Convert to 32bit integer
}
// Map to 0-360 range
- return Math.abs(hash) % 360
+ return Math.abs(hash) % 360;
}
/**
* 将主题色应用到 CSS 变量
*/
function applyThemeColor(hue: number) {
- const root = document.documentElement
- root.style.setProperty('--primary-hue', String(hue))
+ const root = document.documentElement;
+ root.style.setProperty('--primary-hue', String(hue));
}
/**
@@ -60,7 +60,7 @@ function applyThemeColor(hue: number) {
*/
function saveThemeHueCache(hue: number) {
try {
- localStorage.setItem(THEME_HUE_CACHE_KEY, String(hue))
+ localStorage.setItem(THEME_HUE_CACHE_KEY, String(hue));
} catch {
// ignore storage errors
}
@@ -71,17 +71,17 @@ function saveThemeHueCache(hue: number) {
*/
function loadThemeHueCache(): number | null {
try {
- const cached = localStorage.getItem(THEME_HUE_CACHE_KEY)
+ const cached = localStorage.getItem(THEME_HUE_CACHE_KEY);
if (cached) {
- const hue = parseInt(cached, 10)
+ const hue = parseInt(cached, 10);
if (!isNaN(hue) && hue >= 0 && hue <= 360) {
- return hue
+ return hue;
}
}
} catch {
// ignore
}
- return null
+ return null;
}
/**
@@ -89,12 +89,12 @@ function loadThemeHueCache(): number | null {
* 立即从缓存读取并应用,避免白屏闪烁
*/
export function initializeThemeHue() {
- const cached = loadThemeHueCache()
+ const cached = loadThemeHueCache();
if (cached !== null) {
- applyThemeColor(cached)
+ applyThemeColor(cached);
} else {
// 没有缓存时使用默认紫色
- applyThemeColor(WALLET_THEME_PRESETS.purple)
+ applyThemeColor(WALLET_THEME_PRESETS.purple);
}
}
@@ -102,10 +102,10 @@ export function initializeThemeHue() {
* 根据钱包ID获取主题色
*/
function getThemeHueForWallet(wallets: { id: string; themeHue: number }[], walletId: string | null): number {
- if (!walletId) return WALLET_THEME_PRESETS.purple
-
- const wallet = wallets.find((w) => w.id === walletId)
- return wallet?.themeHue ?? WALLET_THEME_PRESETS.purple
+ if (!walletId) return WALLET_THEME_PRESETS.purple;
+
+ const wallet = wallets.find((w) => w.id === walletId);
+ return wallet?.themeHue ?? WALLET_THEME_PRESETS.purple;
}
/**
@@ -113,40 +113,41 @@ function getThemeHueForWallet(wallets: { id: string; themeHue: number }[], walle
* 管理当前钱包的主题色,自动应用到全局CSS变量
*/
export function useWalletTheme() {
- const wallets = useStore(walletStore, (s) => s.wallets)
- const currentWalletId = useStore(walletStore, (s) => s.currentWalletId)
+ const wallets = useStore(walletStore, (s) => s.wallets);
+ const currentWalletId = useStore(walletStore, (s) => s.currentWalletId);
// 获取当前钱包的主题色
- const walletThemeHue = currentWalletId
+ const walletThemeHue = currentWalletId
? getThemeHueForWallet(wallets as { id: string; themeHue: number }[], currentWalletId)
- : null
+ : null;
// 实际使用的主题色:有钱包用钱包的,没有用缓存的,都没有用默认的
- const themeHue = walletThemeHue ?? loadThemeHueCache() ?? WALLET_THEME_PRESETS.purple
+ const themeHue = walletThemeHue ?? loadThemeHueCache() ?? WALLET_THEME_PRESETS.purple;
// 应用主题色并缓存(只有当钱包真正加载后才缓存)
useEffect(() => {
- applyThemeColor(themeHue)
+ applyThemeColor(themeHue);
// 只有当从钱包获取到主题色时才保存缓存
if (walletThemeHue !== null) {
- saveThemeHueCache(walletThemeHue)
+ saveThemeHueCache(walletThemeHue);
}
- }, [themeHue, walletThemeHue])
+ }, [themeHue, walletThemeHue]);
// 设置主题色
const setThemeColor = useCallback((walletId: string, hue: number) => {
walletStore.setState((state) => ({
...state,
- wallets: state.wallets.map((w) =>
- w.id === walletId ? { ...w, themeHue: hue } : w
- ),
- }))
- }, [])
+ wallets: state.wallets.map((w) => (w.id === walletId ? { ...w, themeHue: hue } : w)),
+ }));
+ }, []);
// 设置预设主题
- const setThemePreset = useCallback((walletId: string, preset: WalletThemePreset) => {
- setThemeColor(walletId, WALLET_THEME_PRESETS[preset])
- }, [setThemeColor])
+ const setThemePreset = useCallback(
+ (walletId: string, preset: WalletThemePreset) => {
+ setThemeColor(walletId, WALLET_THEME_PRESETS[preset]);
+ },
+ [setThemeColor],
+ );
return {
themeHue,
@@ -154,7 +155,6 @@ export function useWalletTheme() {
setThemeColor,
setThemePreset,
/** 获取指定钱包的主题色 */
- getWalletTheme: (walletId: string) =>
- getThemeHueForWallet(wallets as { id: string; themeHue: number }[], walletId),
- }
+ getWalletTheme: (walletId: string) => getThemeHueForWallet(wallets as { id: string; themeHue: number }[], walletId),
+ };
}
diff --git a/src/i18n/index.ts b/src/i18n/index.ts
index 8acd9d232..11918cc2c 100644
--- a/src/i18n/index.ts
+++ b/src/i18n/index.ts
@@ -1,105 +1,109 @@
-import i18n from 'i18next'
-import { initReactI18next } from 'react-i18next'
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
// Namespace imports - en
-import enCommon from './locales/en/common.json'
-import enWallet from './locales/en/wallet.json'
-import enTransaction from './locales/en/transaction.json'
-import enSecurity from './locales/en/security.json'
-import enStaking from './locales/en/staking.json'
-import enDweb from './locales/en/dweb.json'
-import enError from './locales/en/error.json'
-import enSettings from './locales/en/settings.json'
-import enToken from './locales/en/token.json'
-import enTime from './locales/en/time.json'
-import enEmpty from './locales/en/empty.json'
-import enScanner from './locales/en/scanner.json'
-import enGuide from './locales/en/guide.json'
-import enMigration from './locales/en/migration.json'
-import enAuthorize from './locales/en/authorize.json'
-import enCurrency from './locales/en/currency.json'
-import enOnboarding from './locales/en/onboarding.json'
-import enNotification from './locales/en/notification.json'
-import enHome from './locales/en/home.json'
-import enEcosystem from './locales/en/ecosystem.json'
+import enCommon from './locales/en/common.json';
+import enWallet from './locales/en/wallet.json';
+import enTransaction from './locales/en/transaction.json';
+import enSecurity from './locales/en/security.json';
+import enStaking from './locales/en/staking.json';
+import enDweb from './locales/en/dweb.json';
+import enError from './locales/en/error.json';
+import enSettings from './locales/en/settings.json';
+import enToken from './locales/en/token.json';
+import enTime from './locales/en/time.json';
+import enEmpty from './locales/en/empty.json';
+import enScanner from './locales/en/scanner.json';
+import enGuide from './locales/en/guide.json';
+import enMigration from './locales/en/migration.json';
+import enAuthorize from './locales/en/authorize.json';
+import enCurrency from './locales/en/currency.json';
+import enOnboarding from './locales/en/onboarding.json';
+import enNotification from './locales/en/notification.json';
+import enHome from './locales/en/home.json';
+import enEcosystem from './locales/en/ecosystem.json';
+import enPermission from './locales/en/permission.json';
// Namespace imports - zh-CN
-import zhCNCommon from './locales/zh-CN/common.json'
-import zhCNWallet from './locales/zh-CN/wallet.json'
-import zhCNTransaction from './locales/zh-CN/transaction.json'
-import zhCNSecurity from './locales/zh-CN/security.json'
-import zhCNStaking from './locales/zh-CN/staking.json'
-import zhCNDweb from './locales/zh-CN/dweb.json'
-import zhCNError from './locales/zh-CN/error.json'
-import zhCNSettings from './locales/zh-CN/settings.json'
-import zhCNToken from './locales/zh-CN/token.json'
-import zhCNTime from './locales/zh-CN/time.json'
-import zhCNEmpty from './locales/zh-CN/empty.json'
-import zhCNScanner from './locales/zh-CN/scanner.json'
-import zhCNGuide from './locales/zh-CN/guide.json'
-import zhCNMigration from './locales/zh-CN/migration.json'
-import zhCNAuthorize from './locales/zh-CN/authorize.json'
-import zhCNCurrency from './locales/zh-CN/currency.json'
-import zhCNOnboarding from './locales/zh-CN/onboarding.json'
-import zhCNNotification from './locales/zh-CN/notification.json'
-import zhCNHome from './locales/zh-CN/home.json'
-import zhCNEcosystem from './locales/zh-CN/ecosystem.json'
+import zhCNCommon from './locales/zh-CN/common.json';
+import zhCNWallet from './locales/zh-CN/wallet.json';
+import zhCNTransaction from './locales/zh-CN/transaction.json';
+import zhCNSecurity from './locales/zh-CN/security.json';
+import zhCNStaking from './locales/zh-CN/staking.json';
+import zhCNDweb from './locales/zh-CN/dweb.json';
+import zhCNError from './locales/zh-CN/error.json';
+import zhCNSettings from './locales/zh-CN/settings.json';
+import zhCNToken from './locales/zh-CN/token.json';
+import zhCNTime from './locales/zh-CN/time.json';
+import zhCNEmpty from './locales/zh-CN/empty.json';
+import zhCNScanner from './locales/zh-CN/scanner.json';
+import zhCNGuide from './locales/zh-CN/guide.json';
+import zhCNMigration from './locales/zh-CN/migration.json';
+import zhCNAuthorize from './locales/zh-CN/authorize.json';
+import zhCNCurrency from './locales/zh-CN/currency.json';
+import zhCNOnboarding from './locales/zh-CN/onboarding.json';
+import zhCNNotification from './locales/zh-CN/notification.json';
+import zhCNHome from './locales/zh-CN/home.json';
+import zhCNEcosystem from './locales/zh-CN/ecosystem.json';
+import zhCNPermission from './locales/zh-CN/permission.json';
// Namespace imports - zh-TW
-import zhTWCommon from './locales/zh-TW/common.json'
-import zhTWWallet from './locales/zh-TW/wallet.json'
-import zhTWTransaction from './locales/zh-TW/transaction.json'
-import zhTWSecurity from './locales/zh-TW/security.json'
-import zhTWStaking from './locales/zh-TW/staking.json'
-import zhTWDweb from './locales/zh-TW/dweb.json'
-import zhTWError from './locales/zh-TW/error.json'
-import zhTWSettings from './locales/zh-TW/settings.json'
-import zhTWToken from './locales/zh-TW/token.json'
-import zhTWTime from './locales/zh-TW/time.json'
-import zhTWEmpty from './locales/zh-TW/empty.json'
-import zhTWScanner from './locales/zh-TW/scanner.json'
-import zhTWGuide from './locales/zh-TW/guide.json'
-import zhTWMigration from './locales/zh-TW/migration.json'
-import zhTWAuthorize from './locales/zh-TW/authorize.json'
-import zhTWCurrency from './locales/zh-TW/currency.json'
-import zhTWOnboarding from './locales/zh-TW/onboarding.json'
-import zhTWNotification from './locales/zh-TW/notification.json'
-import zhTWHome from './locales/zh-TW/home.json'
-import zhTWEcosystem from './locales/zh-TW/ecosystem.json'
+import zhTWCommon from './locales/zh-TW/common.json';
+import zhTWWallet from './locales/zh-TW/wallet.json';
+import zhTWTransaction from './locales/zh-TW/transaction.json';
+import zhTWSecurity from './locales/zh-TW/security.json';
+import zhTWStaking from './locales/zh-TW/staking.json';
+import zhTWDweb from './locales/zh-TW/dweb.json';
+import zhTWError from './locales/zh-TW/error.json';
+import zhTWSettings from './locales/zh-TW/settings.json';
+import zhTWToken from './locales/zh-TW/token.json';
+import zhTWTime from './locales/zh-TW/time.json';
+import zhTWEmpty from './locales/zh-TW/empty.json';
+import zhTWScanner from './locales/zh-TW/scanner.json';
+import zhTWGuide from './locales/zh-TW/guide.json';
+import zhTWMigration from './locales/zh-TW/migration.json';
+import zhTWAuthorize from './locales/zh-TW/authorize.json';
+import zhTWCurrency from './locales/zh-TW/currency.json';
+import zhTWOnboarding from './locales/zh-TW/onboarding.json';
+import zhTWNotification from './locales/zh-TW/notification.json';
+import zhTWHome from './locales/zh-TW/home.json';
+import zhTWEcosystem from './locales/zh-TW/ecosystem.json';
+import zhTWPermission from './locales/zh-TW/permission.json';
// Namespace imports - ar
-import arCommon from './locales/ar/common.json'
-import arWallet from './locales/ar/wallet.json'
-import arTransaction from './locales/ar/transaction.json'
-import arSecurity from './locales/ar/security.json'
-import arStaking from './locales/ar/staking.json'
-import arDweb from './locales/ar/dweb.json'
-import arError from './locales/ar/error.json'
-import arSettings from './locales/ar/settings.json'
-import arToken from './locales/ar/token.json'
-import arTime from './locales/ar/time.json'
-import arEmpty from './locales/ar/empty.json'
-import arScanner from './locales/ar/scanner.json'
-import arGuide from './locales/ar/guide.json'
-import arMigration from './locales/ar/migration.json'
-import arAuthorize from './locales/ar/authorize.json'
-import arCurrency from './locales/ar/currency.json'
-import arOnboarding from './locales/ar/onboarding.json'
-import arNotification from './locales/ar/notification.json'
-import arHome from './locales/ar/home.json'
-import arEcosystem from './locales/ar/ecosystem.json'
+import arCommon from './locales/ar/common.json';
+import arWallet from './locales/ar/wallet.json';
+import arTransaction from './locales/ar/transaction.json';
+import arSecurity from './locales/ar/security.json';
+import arStaking from './locales/ar/staking.json';
+import arDweb from './locales/ar/dweb.json';
+import arError from './locales/ar/error.json';
+import arSettings from './locales/ar/settings.json';
+import arToken from './locales/ar/token.json';
+import arTime from './locales/ar/time.json';
+import arEmpty from './locales/ar/empty.json';
+import arScanner from './locales/ar/scanner.json';
+import arGuide from './locales/ar/guide.json';
+import arMigration from './locales/ar/migration.json';
+import arAuthorize from './locales/ar/authorize.json';
+import arCurrency from './locales/ar/currency.json';
+import arOnboarding from './locales/ar/onboarding.json';
+import arNotification from './locales/ar/notification.json';
+import arHome from './locales/ar/home.json';
+import arEcosystem from './locales/ar/ecosystem.json';
+import arPermission from './locales/ar/permission.json';
// 语言配置
export const languages = {
'zh-CN': { name: '简体中文', dir: 'ltr' as const },
'zh-TW': { name: '中文(繁體)', dir: 'ltr' as const },
- 'en': { name: 'English', dir: 'ltr' as const },
- 'ar': { name: 'العربية', dir: 'rtl' as const },
-} as const
+ en: { name: 'English', dir: 'ltr' as const },
+ ar: { name: 'العربية', dir: 'rtl' as const },
+} as const;
-export type LanguageCode = keyof typeof languages
+export type LanguageCode = keyof typeof languages;
-export const defaultLanguage: LanguageCode = 'zh-CN'
+export const defaultLanguage: LanguageCode = 'zh-CN';
// 命名空间定义
export const namespaces = [
@@ -123,20 +127,21 @@ export const namespaces = [
'notification',
'home',
'ecosystem',
-] as const
+ 'permission',
+] as const;
-export type Namespace = (typeof namespaces)[number]
+export type Namespace = (typeof namespaces)[number];
-export const defaultNS: Namespace = 'common'
+export const defaultNS: Namespace = 'common';
// 获取语言的文字方向
export function getLanguageDirection(lang: LanguageCode): 'ltr' | 'rtl' {
- return languages[lang]?.dir ?? 'ltr'
+ return languages[lang]?.dir ?? 'ltr';
}
// 是否为 RTL 语言
export function isRTL(lang: LanguageCode): boolean {
- return getLanguageDirection(lang) === 'rtl'
+ return getLanguageDirection(lang) === 'rtl';
}
i18n.use(initReactI18next).init({
@@ -162,6 +167,7 @@ i18n.use(initReactI18next).init({
notification: enNotification,
home: enHome,
ecosystem: enEcosystem,
+ permission: enPermission,
},
'zh-CN': {
common: zhCNCommon,
@@ -184,6 +190,7 @@ i18n.use(initReactI18next).init({
notification: zhCNNotification,
home: zhCNHome,
ecosystem: zhCNEcosystem,
+ permission: zhCNPermission,
},
'zh-TW': {
common: zhTWCommon,
@@ -206,6 +213,7 @@ i18n.use(initReactI18next).init({
notification: zhTWNotification,
home: zhTWHome,
ecosystem: zhTWEcosystem,
+ permission: zhTWPermission,
},
ar: {
common: arCommon,
@@ -228,6 +236,7 @@ i18n.use(initReactI18next).init({
notification: arNotification,
home: arHome,
ecosystem: arEcosystem,
+ permission: arPermission,
},
},
lng: defaultLanguage,
@@ -240,6 +249,6 @@ i18n.use(initReactI18next).init({
react: {
useSuspense: false,
},
-})
+});
-export default i18n
+export default i18n;
diff --git a/src/i18n/locales/ar/common.json b/src/i18n/locales/ar/common.json
index 990817b44..12d125a0e 100644
--- a/src/i18n/locales/ar/common.json
+++ b/src/i18n/locales/ar/common.json
@@ -36,14 +36,15 @@
"tabWallet": "المحفظة",
"tokenDetails": "عرض تفاصيل {{token}}",
"transactionDetails": "تفاصيل المعاملة",
- "unknownApp": "تطبيق غير معروف"
+ "unknownApp": "تطبيق غير معروف",
+ "more": "المزيد من الإجراءات"
},
"acceptExchange": "Accept exchange",
"account": "المستخدم",
"accountFromMime": "account from mime",
"accountWorks": "account works!",
"acquisitionTime": "Acquisition time",
- "add": "Add",
+ "add": "إضافة",
"addressBook": {
"addContact": "إضافة جهة اتصال",
"addresses": "العناوين",
@@ -102,7 +103,7 @@
"bgPrimary": "bg-primary",
"bip39Index": "Index",
"byContinuingToUseItYouAgreeToTheUserAgreement": "By continuing to use it, you agree to the 《User Agreement》",
- "cancel": "Cancel",
+ "cancel": "إلغاء",
"cardWorks": "card works!",
"certificate": "Certificate",
"chains": {
@@ -126,7 +127,7 @@
"codeOfThePrivateKey": "الشفرة السداسية للمفتاح الخاص",
"comingSoonStayTuned": "قريبًا",
"comprehensiveEncryption": "Comprehensive <br /> encryption",
- "confirm": "Confirm",
+ "confirm": "تأكيد",
"confirmBackedUp": "Confirm Backed Up",
"confirmToDelete": "Confirm to delete ?",
"confirmToTurnOffTouchId": "Confirm to turn off Touch ID?",
@@ -170,9 +171,11 @@
"couldBeWithin_30Seconds": "Could be within 30 seconds",
"creator": "Creator",
"date": {
- "format": "{{weekday}}، {{day}} {{month}}"
+ "format": "{{weekday}}، {{day}} {{month}}",
+ "today": "اليوم",
+ "yesterday": "أمس"
},
- "delete": "Delete",
+ "delete": "حذف",
"destroy": "Destroy",
"done": "Done",
"download": "تحميل",
@@ -191,7 +194,10 @@
},
"noAppsUsed": "لم تستخدم أي تطبيقات بعد",
"runningApps": "التطبيقات قيد التشغيل",
- "stackViewHints": "اسحب للتبديل · اضغط للفتح · اسحب للأعلى للإغلاق"
+ "stackViewHints": "اسحب للتبديل · اضغط للفتح · اسحب للأعلى للإغلاق",
+ "discover": "اكتشف",
+ "mine": "لي",
+ "stack": "مكدس"
},
"edit": "Edit",
"energy": "Energy",
@@ -215,7 +221,7 @@
"finish": "تم",
"firstTimeToUse": "First time to use",
"freeze": "Freeze",
- "from": "From",
+ "from": "من",
"frozen": "Frozen",
"get_(estimate)": "Get(estimate)",
"goHome": "الرئيسية",
@@ -239,7 +245,7 @@
"manage": "Manage",
"me": "Me",
"memo": "Memo",
- "message": "Message",
+ "message": "محتوى الرسالة",
"methodsParams": "Methods params",
"mimeInTab": "لي",
"mine": "لي",
@@ -263,7 +269,7 @@
},
"name": "Name",
"navOverview": "Overview",
- "network": "Network",
+ "network": "الشبكة",
"networkNotConnected": "Network not connected",
"newFinish": "Finish",
"next": "Next",
@@ -412,5 +418,106 @@
"{{item}}Words": "words",
"{{length}}Words": "words",
"中文(简体)": "中文(简体)",
- "中文(繁體)": "中文(繁體)"
+ "中文(繁體)": "中文(繁體)",
+ "tabs": {
+ "assets": "الأصول",
+ "history": "السجل"
+ },
+ "empty": {
+ "noTransactions": "لا توجد معاملات بعد",
+ "transactionsWillAppear": "ستظهر معاملاتك هنا",
+ "noAssets": "لا توجد أصول بعد",
+ "assetsWillAppear": "ستظهر أصولك هنا بعد التحويل"
+ },
+ "password": {
+ "weak": "ضعيف",
+ "medium": "متوسط",
+ "strong": "قوي"
+ },
+ "unknownWallet": "محفظة غير معروفة",
+ "unknownDApp": "تطبيق غير معروف",
+ "biometric": {
+ "verifyIdentity": "التحقق من الهوية",
+ "protectWallet": "حماية المحفظة بالقياسات الحيوية",
+ "accessMnemonic": "الوصول إلى عبارة الاسترداد"
+ },
+ "signTransaction": "توقيع المعاملة",
+ "invalidTransaction": "بيانات معاملة غير صالحة",
+ "signingAddressNotFound": "لم يتم العثور على عنوان التوقيع في المحافظ",
+ "signingAddress": "عنوان التوقيع",
+ "transaction": "المعاملة",
+ "signTxWarning": "يرجى التأكد من أنك تثق بهذا التطبيق والتحقق من المعاملة بعناية.",
+ "signing": "جاري التوقيع...",
+ "sign": "توقيع",
+ "signMessage": "طلب التوقيع",
+ "hexDataWarning": "تحتوي هذه الرسالة على بيانات سداسية عشرية. يرجى التأكد من أنك تثق بهذا التطبيق.",
+ "switchNetwork": "تبديل الشبكة",
+ "requestsNetworkSwitch": "يطلب تبديل الشبكة",
+ "chainSwitchWarning": "بعد تبديل الشبكات، ستكون معاملاتك على الشبكة الجديدة. يرجى التأكد من فهمك للتأثيرات.",
+ "confirmTransfer": "تأكيد التحويل",
+ "requestsTransfer": "يطلب التحويل",
+ "to": "إلى",
+ "amount": "المبلغ",
+ "cryptoAuthorize": "تفويض التشفير",
+ "permissions": "الأذونات",
+ "duration": "المدة",
+ "selectChainWallet": "اختر محفظة {{chain}}",
+ "requestsAccess": "يطلب الوصول",
+ "noWalletsForChain": "لا توجد محافظ تدعم {{chain}}",
+ "noWallets": "لا توجد محافظ",
+ "requestsDestroy": "يطلب التدمير",
+ "transferWarning": "يرجى التحقق من عنوان المستلم والمبلغ. لا يمكن إلغاء التحويلات.",
+ "confirming": "جاري التأكيد...",
+ "drawPatternToConfirm": "ارسم النمط للتأكيد",
+ "selectWallet": "اختر المحفظة",
+ "sources": {
+ "title": "إدارة المصادر الموثوقة",
+ "noSources": "لا توجد مصادر",
+ "urlPlaceholder": "عنوان URL للاشتراك (https://...)",
+ "namePlaceholder": "الاسم (اختياري)",
+ "addSource": "إضافة مصدر",
+ "official": "رسمي",
+ "updatedAt": "تم التحديث {{date}}",
+ "enterUrl": "يرجى إدخال عنوان URL للاشتراك",
+ "invalidUrl": "تنسيق URL غير صالح",
+ "alreadyExists": "هذا المصدر موجود بالفعل",
+ "customSource": "مصدر مخصص"
+ },
+ "enable": "تمكين",
+ "disable": "تعطيل",
+ "refresh": "تحديث",
+ "assetSelector": {
+ "selectAsset": "اختر الأصل",
+ "balance": "الرصيد"
+ },
+ "chainKind": {
+ "bioforest": {
+ "name": "BioForest",
+ "description": "نظام BioForest البيئي"
+ },
+ "evm": {
+ "name": "متوافق مع EVM",
+ "description": "سلاسل متوافقة مع آلة إيثريوم الافتراضية"
+ },
+ "bitcoin": {
+ "description": "شبكة Bitcoin"
+ },
+ "tron": {
+ "description": "شبكة Tron"
+ },
+ "custom": {
+ "name": "سلاسل مخصصة",
+ "description": "سلاسل مخصصة أضافها المستخدم"
+ }
+ },
+ "crypto": {
+ "authorize": {
+ "title": "طلب تفويض التشفير",
+ "permissions": "الأذونات المطلوبة",
+ "duration": "مدة التفويض",
+ "address": "العنوان المستخدم",
+ "pattern": "ارسم النمط للتأكيد",
+ "error": "النمط غير صحيح، يرجى المحاولة مرة أخرى"
+ }
+ }
}
diff --git a/src/i18n/locales/ar/error.json b/src/i18n/locales/ar/error.json
index cd0422c19..17a42ded6 100644
--- a/src/i18n/locales/ar/error.json
+++ b/src/i18n/locales/ar/error.json
@@ -11,14 +11,16 @@
"saveFailed": "Save Failed",
"scanErrorFromImage": "Scan error from image",
"shareFailed": "Sharing Failed",
- "unknown": "Unknown error",
+ "unknown": "خطأ غير معروف",
"validation": {
"enterRecipientAddress": "Please enter recipient address",
"invalidAddressFormat": "Invalid address format",
"cannotTransferToSelf": "Cannot transfer to yourself",
"enterAmount": "Please enter amount",
"enterValidAmount": "Please enter a valid amount",
- "exceedsBalance": "Amount exceeds balance"
+ "exceedsBalance": "Amount exceeds balance",
+ "enterReceiverAddress": "يرجى إدخال عنوان المستلم",
+ "invalidAddress": "تنسيق العنوان غير صالح"
},
"transaction": {
"failed": "Transaction failed, please try again later",
@@ -31,20 +33,28 @@
"chainConfigMissing": "Chain configuration missing",
"unsupportedChainType": "Unsupported chain type: {{chain}}",
"issuerAddressNotFound": "Unable to get asset issuer address",
- "issuerAddressNotReady": "Asset issuer address not ready"
+ "issuerAddressNotReady": "Asset issuer address not ready",
+ "insufficientGas": "رسوم الغاز غير كافية",
+ "retryLater": "فشلت المعاملة، يرجى المحاولة لاحقاً",
+ "transferFailed": "فشل التحويل",
+ "securityPasswordWrong": "كلمة مرور الأمان غير صحيحة",
+ "unknownError": "خطأ غير معروف"
},
"crypto": {
- "keyDerivationFailed": "Key derivation failed",
+ "keyDerivationFailed": "فشل اشتقاق المفتاح",
"decryptionFailed": "Decryption failed: wrong password or corrupted data",
"decryptionKeyFailed": "Decryption failed: wrong key or corrupted data",
"biometricNotAvailable": "Biometric not available",
"biometricVerificationFailed": "Biometric verification failed",
"webModeRequiresPassword": "Web mode requires password",
"dwebEnvironmentRequired": "DWEB environment required",
- "mpayDecryptionFailed": "mpay data decryption failed: wrong password or corrupted data"
+ "mpayDecryptionFailed": "mpay data decryption failed: wrong password or corrupted data",
+ "unsupportedBitcoinPurpose": "غرض Bitcoin غير مدعوم: {{purpose}}",
+ "decryptFailed": "فشل فك التشفير: كلمة مرور خاطئة أو بيانات تالفة",
+ "decryptKeyFailed": "فشل فك التشفير: مفتاح خاطئ أو بيانات تالفة"
},
"address": {
- "generationFailed": "Address generation failed"
+ "generationFailed": "فشل إنشاء العنوان"
},
"securityPassword": {
"chainNotSupported": "This chain does not support security password",
@@ -55,5 +65,55 @@
},
"duplicate": {
"detectionFailed": "Duplicate detection failed"
+ },
+ "chain": {
+ "unsupportedType": "نوع سلسلة غير مدعوم: {{chain}}",
+ "notSupportSecurityPassword": "هذه السلسلة لا تدعم كلمة المرور الأمنية",
+ "transferNotImplemented": "وظيفة التحويل لهذه السلسلة غير مطبقة بالكامل"
+ },
+ "mpay": {
+ "decryptFailed": "فشل فك تشفير بيانات mpay: كلمة مرور خاطئة أو بيانات تالفة"
+ },
+ "miniapp": {
+ "launchFailed": {
+ "stayOnDesktop": "فشل الإطلاق: يرجى البقاء على صفحة سطح المكتب المستهدفة وإعادة المحاولة",
+ "iconNotReady": "فشل الإطلاق: الأيقونة غير جاهزة، يرجى العودة إلى سطح المكتب وإعادة المحاولة",
+ "containerNotReady": "فشل الإطلاق: الحاوية غير جاهزة، يرجى إعادة المحاولة"
+ }
+ },
+ "biometric": {
+ "unavailable": "المصادقة البيومترية غير متوفرة",
+ "verificationFailed": "فشل التحقق البيومتري"
+ },
+ "query": {
+ "failed": "فشل الاستعلام"
+ },
+ "wallet": {
+ "incompleteInfo": "معلومات المحفظة غير مكتملة"
+ },
+ "destroy": {
+ "failed": "فشل التدمير"
+ },
+ "security": {
+ "passwordInvalid": "كلمة المرور الأمنية غير صحيحة"
+ },
+ "transfer": {
+ "failed": "فشل التحويل"
+ },
+ "secureStorage": {
+ "webPasswordRequired": "كلمة المرور مطلوبة لوضع الويب",
+ "dwebRequired": "مطلوب بيئة DWEB",
+ "passwordRequired": "كلمة المرور مطلوبة لفك التشفير",
+ "passwordRequiredForMnemonic": "كلمة المرور مطلوبة"
+ },
+ "signature": {
+ "missingField": "signaturedata حقل مفقود: {{field}}",
+ "unsupportedType": "نوع التوقيع غير مدعوم: {{type}}",
+ "typeMustBeStringOrNumber": "signaturedata.type يجب أن يكون سلسلة أو رقم",
+ "missingParam": "معلمة signaturedata مفقودة",
+ "invalidJson": "signaturedata ليس JSON صالحًا",
+ "mustBeArray": "signaturedata يجب أن يكون مصفوفة JSON",
+ "emptyArray": "signaturedata لا يمكن أن يكون مصفوفة فارغة",
+ "firstMustBeObject": "signaturedata[0] يجب أن يكون كائنًا"
}
}
diff --git a/src/i18n/locales/ar/migration.json b/src/i18n/locales/ar/migration.json
index 24b2622c1..ba1d4c2ed 100644
--- a/src/i18n/locales/ar/migration.json
+++ b/src/i18n/locales/ar/migration.json
@@ -53,5 +53,9 @@
"goHome": "دخول المحفظة",
"skip": "تخطي، إنشاء محفظة جديدة",
"retry": "إعادة المحاولة"
+ },
+ "pattern": {
+ "title": "التحقق من قفل المحفظة",
+ "description": "يرجى رسم نمط قفل المحفظة للمتابعة"
}
}
diff --git a/src/i18n/locales/ar/permission.json b/src/i18n/locales/ar/permission.json
new file mode 100644
index 000000000..c72c4652a
--- /dev/null
+++ b/src/i18n/locales/ar/permission.json
@@ -0,0 +1,30 @@
+{
+ "bio_requestAccounts": {
+ "label": "عرض الحسابات",
+ "description": "عرض عناوين محفظتك"
+ },
+ "bio_createTransaction": {
+ "label": "إنشاء معاملة",
+ "description": "إنشاء معاملة غير موقعة (بدون توقيع/بث)"
+ },
+ "bio_signMessage": {
+ "label": "توقيع رسالة",
+ "description": "طلب توقيع رسالة (يتطلب تأكيدك)"
+ },
+ "bio_signTypedData": {
+ "label": "توقيع البيانات",
+ "description": "طلب توقيع بيانات منظمة (يتطلب تأكيدك)"
+ },
+ "bio_signTransaction": {
+ "label": "توقيع معاملة",
+ "description": "طلب توقيع معاملة (يتطلب تأكيدك)"
+ },
+ "bio_sendTransaction": {
+ "label": "إرسال معاملة",
+ "description": "طلب إرسال تحويل (يتطلب تأكيدك)"
+ },
+ "requestsPermissions": "يطلب الأذونات التالية",
+ "permissionNote": "تتطلب العمليات الحساسة تأكيدك",
+ "reject": "رفض",
+ "approve": "سماح"
+}
diff --git a/src/i18n/locales/ar/settings.json b/src/i18n/locales/ar/settings.json
index 44e676282..bac522e26 100644
--- a/src/i18n/locales/ar/settings.json
+++ b/src/i18n/locales/ar/settings.json
@@ -98,7 +98,8 @@
"storage": "التخزين",
"viewMnemonic": "عرض العبارة السرية",
"walletChains": "شبكات المحفظة",
- "walletManagement": "إدارة المحفظة"
+ "walletManagement": "إدارة المحفظة",
+ "miniappSources": "مصادر التطبيقات المصغرة الموثوقة"
},
"language": "اللغة",
"languagePage": {
diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json
index fa7ebe7f0..92caa5ab5 100644
--- a/src/i18n/locales/en/common.json
+++ b/src/i18n/locales/en/common.json
@@ -36,7 +36,8 @@
"tabEcosystem": "Ecosystem",
"tokenDetails": "View {{token}} details",
"transactionDetails": "Transaction details",
- "unknownApp": "Unknown app"
+ "unknownApp": "Unknown app",
+ "more": "More actions"
},
"acceptExchange": "Accept exchange",
"account": "User",
@@ -249,7 +250,10 @@
"open": "Open",
"detail": "Details",
"remove": "Remove"
- }
+ },
+ "discover": "Discover",
+ "mine": "Mine",
+ "stack": "Stack"
},
"chains": {
"ethereum": "Ethereum",
@@ -275,7 +279,9 @@
"6": "Saturday"
},
"date": {
- "format": "{{weekday}}, {{month}} {{day}}"
+ "format": "{{weekday}}, {{month}} {{day}}",
+ "today": "Today",
+ "yesterday": "Yesterday"
},
"providerFallback": {
"queryFailed": "{{feature}} query failed",
@@ -412,5 +418,106 @@
"useExplorerHint": "This chain does not support direct history query, please use the explorer",
"openExplorer": "Open {{name}} Explorer",
"viewOnExplorer": "View on {{name}}"
+ },
+ "tabs": {
+ "assets": "Assets",
+ "history": "History"
+ },
+ "empty": {
+ "noTransactions": "No transactions yet",
+ "transactionsWillAppear": "Your transactions will appear here",
+ "noAssets": "No assets yet",
+ "assetsWillAppear": "Your assets will appear here after transfer"
+ },
+ "password": {
+ "weak": "Weak",
+ "medium": "Medium",
+ "strong": "Strong"
+ },
+ "unknownWallet": "Unknown Wallet",
+ "unknownDApp": "Unknown DApp",
+ "biometric": {
+ "verifyIdentity": "Verify Identity",
+ "protectWallet": "Protect wallet with biometrics",
+ "accessMnemonic": "Access wallet mnemonic"
+ },
+ "signTransaction": "Sign Transaction",
+ "invalidTransaction": "Invalid transaction data",
+ "signingAddressNotFound": "Signing address not found in wallets",
+ "signingAddress": "Signing Address",
+ "transaction": "Transaction",
+ "signTxWarning": "Please confirm you trust this app and carefully verify the transaction.",
+ "signing": "Signing...",
+ "sign": "Sign",
+ "signMessage": "Sign Request",
+ "hexDataWarning": "This message contains hex data. Please confirm you trust this app.",
+ "switchNetwork": "Switch Network",
+ "requestsNetworkSwitch": "requests network switch",
+ "chainSwitchWarning": "After switching networks, your transactions will be on the new network. Please make sure you understand the implications.",
+ "confirmTransfer": "Confirm Transfer",
+ "requestsTransfer": "requests transfer",
+ "to": "To",
+ "amount": "Amount",
+ "cryptoAuthorize": "Crypto Authorize",
+ "permissions": "Permissions",
+ "duration": "Duration",
+ "selectChainWallet": "Select {{chain}} Wallet",
+ "requestsAccess": "requests access",
+ "noWalletsForChain": "No wallets support {{chain}}",
+ "noWallets": "No wallets",
+ "requestsDestroy": "requests destruction",
+ "transferWarning": "Please verify the recipient address and amount. Transfers cannot be reversed.",
+ "confirming": "Confirming...",
+ "drawPatternToConfirm": "Draw pattern to confirm",
+ "selectWallet": "Select Wallet",
+ "sources": {
+ "title": "Trusted Sources",
+ "noSources": "No sources",
+ "urlPlaceholder": "Subscription URL (https://...)",
+ "namePlaceholder": "Name (optional)",
+ "addSource": "Add source",
+ "official": "Official",
+ "updatedAt": "Updated {{date}}",
+ "enterUrl": "Please enter subscription URL",
+ "invalidUrl": "Invalid URL format",
+ "alreadyExists": "This source already exists",
+ "customSource": "Custom source"
+ },
+ "enable": "Enable",
+ "disable": "Disable",
+ "refresh": "Refresh",
+ "assetSelector": {
+ "selectAsset": "Select asset",
+ "balance": "Balance"
+ },
+ "chainKind": {
+ "bioforest": {
+ "name": "BioForest",
+ "description": "BioForest ecosystem"
+ },
+ "evm": {
+ "name": "EVM Compatible",
+ "description": "Ethereum Virtual Machine compatible chains"
+ },
+ "bitcoin": {
+ "description": "Bitcoin network"
+ },
+ "tron": {
+ "description": "Tron network"
+ },
+ "custom": {
+ "name": "Custom chains",
+ "description": "User-added custom chains"
+ }
+ },
+ "crypto": {
+ "authorize": {
+ "title": "Request Crypto Authorization",
+ "permissions": "Requested Permissions",
+ "duration": "Authorization Duration",
+ "address": "Using Address",
+ "pattern": "Draw pattern to confirm",
+ "error": "Pattern incorrect, please try again"
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/i18n/locales/en/error.json b/src/i18n/locales/en/error.json
index cd0422c19..23752b34d 100644
--- a/src/i18n/locales/en/error.json
+++ b/src/i18n/locales/en/error.json
@@ -18,7 +18,9 @@
"cannotTransferToSelf": "Cannot transfer to yourself",
"enterAmount": "Please enter amount",
"enterValidAmount": "Please enter a valid amount",
- "exceedsBalance": "Amount exceeds balance"
+ "exceedsBalance": "Amount exceeds balance",
+ "enterReceiverAddress": "Please enter recipient address",
+ "invalidAddress": "Invalid address format"
},
"transaction": {
"failed": "Transaction failed, please try again later",
@@ -31,7 +33,12 @@
"chainConfigMissing": "Chain configuration missing",
"unsupportedChainType": "Unsupported chain type: {{chain}}",
"issuerAddressNotFound": "Unable to get asset issuer address",
- "issuerAddressNotReady": "Asset issuer address not ready"
+ "issuerAddressNotReady": "Asset issuer address not ready",
+ "insufficientGas": "Insufficient gas fee",
+ "retryLater": "Transaction failed, please try again later",
+ "transferFailed": "Transfer failed",
+ "securityPasswordWrong": "Security password incorrect",
+ "unknownError": "Unknown error"
},
"crypto": {
"keyDerivationFailed": "Key derivation failed",
@@ -41,7 +48,10 @@
"biometricVerificationFailed": "Biometric verification failed",
"webModeRequiresPassword": "Web mode requires password",
"dwebEnvironmentRequired": "DWEB environment required",
- "mpayDecryptionFailed": "mpay data decryption failed: wrong password or corrupted data"
+ "mpayDecryptionFailed": "mpay data decryption failed: wrong password or corrupted data",
+ "unsupportedBitcoinPurpose": "Unsupported Bitcoin purpose: {{purpose}}",
+ "decryptFailed": "Decryption failed: wrong password or corrupted data",
+ "decryptKeyFailed": "Decryption failed: wrong key or corrupted data"
},
"address": {
"generationFailed": "Address generation failed"
@@ -55,5 +65,55 @@
},
"duplicate": {
"detectionFailed": "Duplicate detection failed"
+ },
+ "chain": {
+ "unsupportedType": "Unsupported chain type: {{chain}}",
+ "notSupportSecurityPassword": "This chain does not support security password",
+ "transferNotImplemented": "Transfer function for this chain is not fully implemented"
+ },
+ "mpay": {
+ "decryptFailed": "Failed to decrypt mpay data: wrong password or corrupted data"
+ },
+ "miniapp": {
+ "launchFailed": {
+ "stayOnDesktop": "Launch failed: please stay on the target desktop page and retry",
+ "iconNotReady": "Launch failed: icon not ready, please return to desktop and retry",
+ "containerNotReady": "Launch failed: container not ready, please retry"
+ }
+ },
+ "biometric": {
+ "unavailable": "Biometric authentication unavailable",
+ "verificationFailed": "Biometric verification failed"
+ },
+ "query": {
+ "failed": "Query failed"
+ },
+ "wallet": {
+ "incompleteInfo": "Wallet information is incomplete"
+ },
+ "destroy": {
+ "failed": "Destroy failed"
+ },
+ "security": {
+ "passwordInvalid": "Security password is incorrect"
+ },
+ "transfer": {
+ "failed": "Transfer failed"
+ },
+ "secureStorage": {
+ "webPasswordRequired": "Password required for web mode",
+ "dwebRequired": "DWEB environment required",
+ "passwordRequired": "Password required for decryption",
+ "passwordRequiredForMnemonic": "Password required"
+ },
+ "signature": {
+ "missingField": "signaturedata missing field: {{field}}",
+ "unsupportedType": "Unsupported signature type: {{type}}",
+ "typeMustBeStringOrNumber": "signaturedata.type must be string or number",
+ "missingParam": "Missing signaturedata parameter",
+ "invalidJson": "signaturedata is not valid JSON",
+ "mustBeArray": "signaturedata must be a JSON array",
+ "emptyArray": "signaturedata cannot be an empty array",
+ "firstMustBeObject": "signaturedata[0] must be an object"
}
}
diff --git a/src/i18n/locales/en/migration.json b/src/i18n/locales/en/migration.json
index a1d54bffc..c43541ee8 100644
--- a/src/i18n/locales/en/migration.json
+++ b/src/i18n/locales/en/migration.json
@@ -53,5 +53,9 @@
"goHome": "Enter Wallet",
"skip": "Skip, create new wallet",
"retry": "Retry"
+ },
+ "pattern": {
+ "title": "Verify Wallet Lock",
+ "description": "Please draw the wallet lock pattern to continue migration"
}
}
diff --git a/src/i18n/locales/en/permission.json b/src/i18n/locales/en/permission.json
new file mode 100644
index 000000000..06f486e90
--- /dev/null
+++ b/src/i18n/locales/en/permission.json
@@ -0,0 +1,30 @@
+{
+ "bio_requestAccounts": {
+ "label": "View Accounts",
+ "description": "View your wallet addresses"
+ },
+ "bio_createTransaction": {
+ "label": "Create Transaction",
+ "description": "Create unsigned transaction (no signing/broadcasting)"
+ },
+ "bio_signMessage": {
+ "label": "Sign Message",
+ "description": "Request to sign a message (requires your confirmation)"
+ },
+ "bio_signTypedData": {
+ "label": "Sign Data",
+ "description": "Request to sign structured data (requires your confirmation)"
+ },
+ "bio_signTransaction": {
+ "label": "Sign Transaction",
+ "description": "Request to sign a transaction (requires your confirmation)"
+ },
+ "bio_sendTransaction": {
+ "label": "Send Transaction",
+ "description": "Request to send a transfer (requires your confirmation)"
+ },
+ "requestsPermissions": "Requests the following permissions",
+ "permissionNote": "Sensitive operations require your confirmation",
+ "reject": "Reject",
+ "approve": "Allow"
+}
diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json
index 814ad55ce..2ff89b8eb 100644
--- a/src/i18n/locales/en/settings.json
+++ b/src/i18n/locales/en/settings.json
@@ -22,7 +22,8 @@
"appearance": "Appearance",
"aboutApp": "About BFM Pay",
"clearData": "Clear App Data",
- "storage": "Storage"
+ "storage": "Storage",
+ "miniappSources": "Miniapp Trusted Sources"
},
"appearance": {
"title": "Appearance",
@@ -147,4 +148,4 @@
"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"
}
-}
\ No newline at end of file
+}
diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json
index 6424b41c4..47726884a 100644
--- a/src/i18n/locales/zh-CN/common.json
+++ b/src/i18n/locales/zh-CN/common.json
@@ -19,7 +19,10 @@
"open": "打开",
"detail": "详情",
"remove": "移除"
- }
+ },
+ "discover": "发现",
+ "mine": "我的",
+ "stack": "堆栈"
},
"chains": {
"ethereum": "Ethereum",
@@ -45,7 +48,9 @@
"6": "周六"
},
"date": {
- "format": "{{month}}月{{day}}日 {{weekday}}"
+ "format": "{{month}}月{{day}}日 {{weekday}}",
+ "today": "今天",
+ "yesterday": "昨天"
},
"paste": "粘贴",
"addressPlaceholder": "输入或粘贴地址",
@@ -92,7 +97,8 @@
"permissions": "权限列表",
"transactionDetails": "交易详情",
"selectWallet": "选择钱包",
- "addWallet": "添加钱包"
+ "addWallet": "添加钱包",
+ "more": "更多操作"
},
"contact": {
"addTitle": "添加联系人",
@@ -121,7 +127,7 @@
"accountFromMime": "account from mime",
"accountWorks": "account works!",
"acquisitionTime": "入手时间",
- "add": "新增",
+ "add": "添加",
"advancedEncryptionTechnologyDigitalWealthIsMoreSecure": "先进加密技术 <br /> 数字财富更安全",
"afterConfirmation_{appName}WillDeleteAllLocalDataTips": "确定后,将删除所有本地钱包数据,所有钱包将从本地移除,确定退出?",
"album": "相册",
@@ -215,7 +221,7 @@
"manage": "管理",
"me": "我",
"memo": "附言",
- "message": "信息",
+ "message": "消息内容",
"methodsParams": "合约参数",
"mimeInTab": "我的",
"mine": "我的",
@@ -412,5 +418,106 @@
"useExplorerHint": "该链不支持直接查询交易历史,请使用浏览器查看",
"openExplorer": "打开 {{name}} 浏览器",
"viewOnExplorer": "在 {{name}} 浏览器中查看"
+ },
+ "tabs": {
+ "assets": "资产",
+ "history": "交易"
+ },
+ "empty": {
+ "noTransactions": "暂无交易记录",
+ "transactionsWillAppear": "您的交易记录将显示在这里",
+ "noAssets": "暂无资产",
+ "assetsWillAppear": "转入资产后将显示在这里"
+ },
+ "password": {
+ "weak": "弱",
+ "medium": "中",
+ "strong": "强"
+ },
+ "unknownWallet": "未知钱包",
+ "unknownDApp": "未知 DApp",
+ "biometric": {
+ "verifyIdentity": "验证身份",
+ "protectWallet": "使用生物识别保护钱包",
+ "accessMnemonic": "访问钱包助记词"
+ },
+ "signTransaction": "签名交易",
+ "invalidTransaction": "无效的交易数据",
+ "signingAddressNotFound": "找不到对应的钱包地址,无法签名",
+ "signingAddress": "签名地址",
+ "transaction": "交易内容",
+ "signTxWarning": "请确认您信任此应用,并仔细核对交易内容。",
+ "signing": "签名中...",
+ "sign": "签名",
+ "signMessage": "签名请求",
+ "hexDataWarning": "此消息包含十六进制数据,请确认您信任此应用。",
+ "switchNetwork": "切换网络",
+ "requestsNetworkSwitch": "请求切换网络",
+ "chainSwitchWarning": "切换网络后,您的交易将在新网络上进行。请确保您了解此操作的影响。",
+ "confirmTransfer": "确认转账",
+ "requestsTransfer": "请求转账",
+ "to": "到",
+ "amount": "金额",
+ "cryptoAuthorize": "加密授权",
+ "permissions": "权限",
+ "duration": "时长",
+ "selectChainWallet": "选择 {{chain}} 钱包",
+ "requestsAccess": "请求访问",
+ "noWalletsForChain": "暂无支持 {{chain}} 的钱包",
+ "noWallets": "暂无钱包",
+ "requestsDestroy": "请求销毁",
+ "transferWarning": "请仔细核对收款地址和金额,转账后无法撤回。",
+ "confirming": "确认中...",
+ "drawPatternToConfirm": "请绘制手势密码确认",
+ "selectWallet": "选择钱包",
+ "sources": {
+ "title": "可信源管理",
+ "noSources": "暂无订阅源",
+ "urlPlaceholder": "订阅 URL (https://...)",
+ "namePlaceholder": "名称 (可选)",
+ "addSource": "添加订阅源",
+ "official": "官方",
+ "updatedAt": "更新于 {{date}}",
+ "enterUrl": "请输入订阅 URL",
+ "invalidUrl": "无效的 URL 格式",
+ "alreadyExists": "该订阅源已存在",
+ "customSource": "自定义源"
+ },
+ "enable": "启用",
+ "disable": "禁用",
+ "refresh": "刷新",
+ "assetSelector": {
+ "selectAsset": "选择资产",
+ "balance": "余额"
+ },
+ "chainKind": {
+ "bioforest": {
+ "name": "生物链林",
+ "description": "BioForest 生态链"
+ },
+ "evm": {
+ "name": "EVM 兼容链",
+ "description": "以太坊虚拟机兼容链"
+ },
+ "bitcoin": {
+ "description": "Bitcoin 网络"
+ },
+ "tron": {
+ "description": "Tron 网络"
+ },
+ "custom": {
+ "name": "自定义链",
+ "description": "用户添加的自定义链"
+ }
+ },
+ "crypto": {
+ "authorize": {
+ "title": "请求加密授权",
+ "permissions": "请求权限",
+ "duration": "授权时长",
+ "address": "使用地址",
+ "pattern": "请输入手势密码确认",
+ "error": "手势密码错误,请重试"
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/i18n/locales/zh-CN/error.json b/src/i18n/locales/zh-CN/error.json
index c93f8dfcb..1558f4f6a 100644
--- a/src/i18n/locales/zh-CN/error.json
+++ b/src/i18n/locales/zh-CN/error.json
@@ -18,7 +18,9 @@
"cannotTransferToSelf": "不能转账给自己",
"enterAmount": "请输入金额",
"enterValidAmount": "请输入有效金额",
- "exceedsBalance": "销毁数量不能大于余额"
+ "exceedsBalance": "销毁数量不能大于余额",
+ "enterReceiverAddress": "请输入收款地址",
+ "invalidAddress": "无效的地址格式"
},
"transaction": {
"failed": "交易失败,请稍后重试",
@@ -31,7 +33,12 @@
"chainConfigMissing": "链配置缺失",
"unsupportedChainType": "不支持的链类型: {{chain}}",
"issuerAddressNotFound": "无法获取资产发行地址",
- "issuerAddressNotReady": "资产发行地址未获取"
+ "issuerAddressNotReady": "资产发行地址未获取",
+ "insufficientGas": "手续费不足",
+ "retryLater": "交易失败,请稍后重试",
+ "transferFailed": "转账失败",
+ "securityPasswordWrong": "安全密码错误",
+ "unknownError": "未知错误"
},
"crypto": {
"keyDerivationFailed": "密钥派生失败",
@@ -41,7 +48,10 @@
"biometricVerificationFailed": "生物识别验证失败",
"webModeRequiresPassword": "Web 模式需要提供密码",
"dwebEnvironmentRequired": "需要在 DWEB 环境中访问",
- "mpayDecryptionFailed": "mpay 数据解密失败:密码错误或数据损坏"
+ "mpayDecryptionFailed": "mpay 数据解密失败:密码错误或数据损坏",
+ "unsupportedBitcoinPurpose": "不支持的 Bitcoin purpose: {{purpose}}",
+ "decryptFailed": "解密失败:密码错误或数据损坏",
+ "decryptKeyFailed": "解密失败:密钥错误或数据损坏"
},
"address": {
"generationFailed": "地址生成失败"
@@ -55,5 +65,55 @@
},
"duplicate": {
"detectionFailed": "重复检测失败"
+ },
+ "chain": {
+ "unsupportedType": "不支持的链类型: {{chain}}",
+ "notSupportSecurityPassword": "该链不支持安全密码",
+ "transferNotImplemented": "该链转账功能尚未完全实现"
+ },
+ "mpay": {
+ "decryptFailed": "mpay 数据解密失败:密码错误或数据损坏"
+ },
+ "miniapp": {
+ "launchFailed": {
+ "stayOnDesktop": "启动失败:请停留在目标桌面页后重试",
+ "iconNotReady": "启动失败:图标未就绪,请返回桌面重试",
+ "containerNotReady": "启动失败:加载容器未就绪,请重试"
+ }
+ },
+ "biometric": {
+ "unavailable": "生物识别不可用",
+ "verificationFailed": "生物识别验证失败"
+ },
+ "query": {
+ "failed": "查询失败"
+ },
+ "wallet": {
+ "incompleteInfo": "钱包信息不完整"
+ },
+ "destroy": {
+ "failed": "销毁失败"
+ },
+ "security": {
+ "passwordInvalid": "安全密码错误"
+ },
+ "transfer": {
+ "failed": "转账失败"
+ },
+ "secureStorage": {
+ "webPasswordRequired": "Web 模式需要提供密码",
+ "dwebRequired": "需要在 DWEB 环境中访问",
+ "passwordRequired": "需要提供密码解密",
+ "passwordRequiredForMnemonic": "需要提供密码"
+ },
+ "signature": {
+ "missingField": "signaturedata 缺少字段:{{field}}",
+ "unsupportedType": "不支持的签名类型:{{type}}",
+ "typeMustBeStringOrNumber": "signaturedata.type 必须是字符串或数字",
+ "missingParam": "缺少 signaturedata 参数",
+ "invalidJson": "signaturedata 不是合法的 JSON",
+ "mustBeArray": "signaturedata 必须是 JSON 数组",
+ "emptyArray": "signaturedata 不能为空数组",
+ "firstMustBeObject": "signaturedata[0] 必须是对象"
}
}
diff --git a/src/i18n/locales/zh-CN/migration.json b/src/i18n/locales/zh-CN/migration.json
index 5dc015def..eef8c2d99 100644
--- a/src/i18n/locales/zh-CN/migration.json
+++ b/src/i18n/locales/zh-CN/migration.json
@@ -53,5 +53,9 @@
"goHome": "进入钱包",
"skip": "跳过,创建新钱包",
"retry": "重试"
+ },
+ "pattern": {
+ "title": "验证钱包锁",
+ "description": "请绘制钱包锁图案以继续迁移"
}
}
diff --git a/src/i18n/locales/zh-CN/permission.json b/src/i18n/locales/zh-CN/permission.json
new file mode 100644
index 000000000..2481d5e12
--- /dev/null
+++ b/src/i18n/locales/zh-CN/permission.json
@@ -0,0 +1,30 @@
+{
+ "bio_requestAccounts": {
+ "label": "查看账户",
+ "description": "查看您的钱包地址"
+ },
+ "bio_createTransaction": {
+ "label": "创建交易",
+ "description": "构造未签名交易(不做签名/不做广播)"
+ },
+ "bio_signMessage": {
+ "label": "签名消息",
+ "description": "请求签名消息(需要您确认)"
+ },
+ "bio_signTypedData": {
+ "label": "签名数据",
+ "description": "请求签名结构化数据(需要您确认)"
+ },
+ "bio_signTransaction": {
+ "label": "签名交易",
+ "description": "请求签名交易(需要您确认)"
+ },
+ "bio_sendTransaction": {
+ "label": "发送交易",
+ "description": "请求发送转账(需要您确认)"
+ },
+ "requestsPermissions": "请求以下权限",
+ "permissionNote": "敏感操作需要您的确认",
+ "reject": "拒绝",
+ "approve": "允许"
+}
diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json
index 9696352be..5e87b75f8 100644
--- a/src/i18n/locales/zh-CN/settings.json
+++ b/src/i18n/locales/zh-CN/settings.json
@@ -22,7 +22,8 @@
"appearance": "外观",
"aboutApp": "关于 BFM Pay",
"clearData": "清空应用数据",
- "storage": "存储空间"
+ "storage": "存储空间",
+ "miniappSources": "小程序可信源"
},
"appearance": {
"title": "外观设置",
@@ -147,4 +148,4 @@
"migrationDesc": "检测到旧版本数据格式,需要清空本地数据库后才能继续使用。您的助记词和私钥不会受到影响,但需要重新导入钱包。",
"goToClear": "前往清理数据"
}
-}
\ No newline at end of file
+}
diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json
index 7ca5bdfcd..798fbfe18 100644
--- a/src/i18n/locales/zh-TW/common.json
+++ b/src/i18n/locales/zh-TW/common.json
@@ -36,14 +36,15 @@
"tabWallet": "錢包",
"tokenDetails": "查看 {{token}} 詳情",
"transactionDetails": "交易詳情",
- "unknownApp": "未知應用程式"
+ "unknownApp": "未知應用程式",
+ "more": "更多操作"
},
"acceptExchange": "接受交換",
"account": "賬戶",
"accountFromMime": "account from mime",
"accountWorks": "account works!",
"acquisitionTime": "入手時間",
- "add": "新增",
+ "add": "添加",
"addressBook": {
"addContact": "新增聯繫人",
"addresses": "地址",
@@ -126,7 +127,7 @@
"codeOfThePrivateKey": "code of the private key",
"comingSoonStayTuned": "即將上綫,敬請期待",
"comprehensiveEncryption": "全面加密",
- "confirm": "確定",
+ "confirm": "確認",
"confirmBackedUp": "確認已備份",
"confirmToDelete": "確認刪除?",
"confirmToTurnOffTouchId": "確定要關閉指紋鎖嗎?",
@@ -170,7 +171,9 @@
"couldBeWithin_30Seconds": "預計30秒",
"creator": "創作者",
"date": {
- "format": "{{month}}月{{day}}日 {{weekday}}"
+ "format": "{{month}}月{{day}}日 {{weekday}}",
+ "today": "今天",
+ "yesterday": "昨天"
},
"delete": "刪除",
"destroy": "銷毀",
@@ -191,7 +194,10 @@
},
"noAppsUsed": "還沒有使用過的應用",
"runningApps": "正在執行的應用",
- "stackViewHints": "左右滑動切換 · 點擊開啟 · 上滑關閉"
+ "stackViewHints": "左右滑動切換 · 點擊開啟 · 上滑關閉",
+ "discover": "發現",
+ "mine": "我的",
+ "stack": "堆棧"
},
"edit": "編輯",
"energy": "能量",
@@ -239,7 +245,7 @@
"manage": "管理",
"me": "我",
"memo": "附言",
- "message": "信息",
+ "message": "訊息內容",
"methodsParams": "合約參數",
"mimeInTab": "mime-in-tab",
"mine": "我的",
@@ -263,7 +269,7 @@
},
"name": "名稱",
"navOverview": "概覽",
- "network": "網絡",
+ "network": "網路",
"networkNotConnected": "當前的網絡未連接",
"newFinish": "完成",
"next": "下一步",
@@ -412,5 +418,106 @@
"{{item}}Words": "個字",
"{{length}}Words": "個字",
"中文(简体)": "中文(简体)",
- "中文(繁體)": "中文(繁體)"
+ "中文(繁體)": "中文(繁體)",
+ "tabs": {
+ "assets": "資產",
+ "history": "交易"
+ },
+ "empty": {
+ "noTransactions": "暫無交易記錄",
+ "transactionsWillAppear": "您的交易記錄將顯示在這裡",
+ "noAssets": "暫無資產",
+ "assetsWillAppear": "轉入資產後將顯示在這裡"
+ },
+ "password": {
+ "weak": "弱",
+ "medium": "中",
+ "strong": "強"
+ },
+ "unknownWallet": "未知錢包",
+ "unknownDApp": "未知 DApp",
+ "biometric": {
+ "verifyIdentity": "驗證身份",
+ "protectWallet": "使用生物識別保護錢包",
+ "accessMnemonic": "訪問錢包助記詞"
+ },
+ "signTransaction": "簽名交易",
+ "invalidTransaction": "無效的交易資料",
+ "signingAddressNotFound": "找不到對應的錢包地址,無法簽名",
+ "signingAddress": "簽名地址",
+ "transaction": "交易內容",
+ "signTxWarning": "請確認您信任此應用,並仔細核對交易內容。",
+ "signing": "簽名中...",
+ "sign": "簽名",
+ "signMessage": "簽名請求",
+ "hexDataWarning": "此訊息包含十六進位資料,請確認您信任此應用。",
+ "switchNetwork": "切換網路",
+ "requestsNetworkSwitch": "請求切換網路",
+ "chainSwitchWarning": "切換網路後,您的交易將在新網路上進行。請確保您了解此操作的影響。",
+ "confirmTransfer": "確認轉帳",
+ "requestsTransfer": "請求轉帳",
+ "to": "到",
+ "amount": "金額",
+ "cryptoAuthorize": "加密授權",
+ "permissions": "權限",
+ "duration": "時長",
+ "selectChainWallet": "選擇 {{chain}} 錢包",
+ "requestsAccess": "請求訪問",
+ "noWalletsForChain": "暫無支援 {{chain}} 的錢包",
+ "noWallets": "暫無錢包",
+ "requestsDestroy": "請求銷毀",
+ "transferWarning": "請仔細核對收款地址和金額,轉帳後無法撤回。",
+ "confirming": "確認中...",
+ "drawPatternToConfirm": "請繪製手勢密碼確認",
+ "selectWallet": "選擇錢包",
+ "sources": {
+ "title": "可信源管理",
+ "noSources": "暫無訂閱源",
+ "urlPlaceholder": "訂閱 URL (https://...)",
+ "namePlaceholder": "名稱 (可選)",
+ "addSource": "添加訂閱源",
+ "official": "官方",
+ "updatedAt": "更新於 {{date}}",
+ "enterUrl": "請輸入訂閱 URL",
+ "invalidUrl": "無效的 URL 格式",
+ "alreadyExists": "該訂閱源已存在",
+ "customSource": "自定義源"
+ },
+ "enable": "啟用",
+ "disable": "禁用",
+ "refresh": "刷新",
+ "assetSelector": {
+ "selectAsset": "選擇資產",
+ "balance": "餘額"
+ },
+ "chainKind": {
+ "bioforest": {
+ "name": "生物鏈林",
+ "description": "BioForest 生態鏈"
+ },
+ "evm": {
+ "name": "EVM 兼容鏈",
+ "description": "以太坊虛擬機兼容鏈"
+ },
+ "bitcoin": {
+ "description": "Bitcoin 網絡"
+ },
+ "tron": {
+ "description": "Tron 網絡"
+ },
+ "custom": {
+ "name": "自定義鏈",
+ "description": "用戶添加的自定義鏈"
+ }
+ },
+ "crypto": {
+ "authorize": {
+ "title": "請求加密授權",
+ "permissions": "請求權限",
+ "duration": "授權時長",
+ "address": "使用地址",
+ "pattern": "請輸入手勢密碼確認",
+ "error": "手勢密碼錯誤,請重試"
+ }
+ }
}
diff --git a/src/i18n/locales/zh-TW/error.json b/src/i18n/locales/zh-TW/error.json
index 3e34b611f..c2bbbbaa8 100644
--- a/src/i18n/locales/zh-TW/error.json
+++ b/src/i18n/locales/zh-TW/error.json
@@ -18,7 +18,9 @@
"cannotTransferToSelf": "不能轉帳給自己",
"enterAmount": "請輸入金額",
"enterValidAmount": "請輸入有效金額",
- "exceedsBalance": "銷毀數量不能大於餘額"
+ "exceedsBalance": "銷毀數量不能大於餘額",
+ "enterReceiverAddress": "請輸入收款地址",
+ "invalidAddress": "無效的地址格式"
},
"transaction": {
"failed": "交易失敗,請稍後重試",
@@ -31,7 +33,12 @@
"chainConfigMissing": "鏈配置缺失",
"unsupportedChainType": "不支持的鏈類型: {{chain}}",
"issuerAddressNotFound": "無法獲取資產發行地址",
- "issuerAddressNotReady": "資產發行地址未獲取"
+ "issuerAddressNotReady": "資產發行地址未獲取",
+ "insufficientGas": "手續費不足",
+ "retryLater": "交易失敗,請稍後重試",
+ "transferFailed": "轉帳失敗",
+ "securityPasswordWrong": "安全密碼錯誤",
+ "unknownError": "未知錯誤"
},
"crypto": {
"keyDerivationFailed": "密鑰派生失敗",
@@ -41,7 +48,10 @@
"biometricVerificationFailed": "生物識別驗證失敗",
"webModeRequiresPassword": "Web 模式需要提供密碼",
"dwebEnvironmentRequired": "需要在 DWEB 環境中訪問",
- "mpayDecryptionFailed": "mpay 資料解密失敗:密碼錯誤或資料損壞"
+ "mpayDecryptionFailed": "mpay 資料解密失敗:密碼錯誤或資料損壞",
+ "unsupportedBitcoinPurpose": "不支援的 Bitcoin purpose: {{purpose}}",
+ "decryptFailed": "解密失敗:密碼錯誤或資料損壞",
+ "decryptKeyFailed": "解密失敗:密鑰錯誤或資料損壞"
},
"address": {
"generationFailed": "地址生成失敗"
@@ -55,5 +65,55 @@
},
"duplicate": {
"detectionFailed": "重複檢測失敗"
+ },
+ "chain": {
+ "unsupportedType": "不支援的鏈類型: {{chain}}",
+ "notSupportSecurityPassword": "該鏈不支持安全密碼",
+ "transferNotImplemented": "該鏈轉賬功能尚未完全實現"
+ },
+ "mpay": {
+ "decryptFailed": "mpay 資料解密失敗:密碼錯誤或資料損壞"
+ },
+ "miniapp": {
+ "launchFailed": {
+ "stayOnDesktop": "啟動失敗:請停留在目標桌面頁後重試",
+ "iconNotReady": "啟動失敗:圖標未就緒,請返回桌面重試",
+ "containerNotReady": "啟動失敗:加載容器未就緒,請重試"
+ }
+ },
+ "biometric": {
+ "unavailable": "生物識別不可用",
+ "verificationFailed": "生物識別驗證失敗"
+ },
+ "query": {
+ "failed": "查詢失敗"
+ },
+ "wallet": {
+ "incompleteInfo": "錢包信息不完整"
+ },
+ "destroy": {
+ "failed": "銷毀失敗"
+ },
+ "security": {
+ "passwordInvalid": "安全密碼錯誤"
+ },
+ "transfer": {
+ "failed": "轉賬失敗"
+ },
+ "secureStorage": {
+ "webPasswordRequired": "Web 模式需要提供密碼",
+ "dwebRequired": "需要在 DWEB 環境中訪問",
+ "passwordRequired": "需要提供密碼解密",
+ "passwordRequiredForMnemonic": "需要提供密碼"
+ },
+ "signature": {
+ "missingField": "signaturedata 缺少字段:{{field}}",
+ "unsupportedType": "不支持的簽名類型:{{type}}",
+ "typeMustBeStringOrNumber": "signaturedata.type 必須是字符串或數字",
+ "missingParam": "缺少 signaturedata 參數",
+ "invalidJson": "signaturedata 不是合法的 JSON",
+ "mustBeArray": "signaturedata 必須是 JSON 數組",
+ "emptyArray": "signaturedata 不能為空數組",
+ "firstMustBeObject": "signaturedata[0] 必須是對象"
}
}
diff --git a/src/i18n/locales/zh-TW/migration.json b/src/i18n/locales/zh-TW/migration.json
index 9ee1446fd..ae916e90c 100644
--- a/src/i18n/locales/zh-TW/migration.json
+++ b/src/i18n/locales/zh-TW/migration.json
@@ -53,5 +53,9 @@
"goHome": "進入錢包",
"skip": "跳過,創建新錢包",
"retry": "重試"
+ },
+ "pattern": {
+ "title": "驗證錢包鎖",
+ "description": "請繪製錢包鎖圖案以繼續遷移"
}
}
diff --git a/src/i18n/locales/zh-TW/permission.json b/src/i18n/locales/zh-TW/permission.json
new file mode 100644
index 000000000..07477b5cb
--- /dev/null
+++ b/src/i18n/locales/zh-TW/permission.json
@@ -0,0 +1,30 @@
+{
+ "bio_requestAccounts": {
+ "label": "查看帳戶",
+ "description": "查看您的錢包地址"
+ },
+ "bio_createTransaction": {
+ "label": "建立交易",
+ "description": "建構未簽名交易(不做簽名/不做廣播)"
+ },
+ "bio_signMessage": {
+ "label": "簽名訊息",
+ "description": "請求簽名訊息(需要您確認)"
+ },
+ "bio_signTypedData": {
+ "label": "簽名資料",
+ "description": "請求簽名結構化資料(需要您確認)"
+ },
+ "bio_signTransaction": {
+ "label": "簽名交易",
+ "description": "請求簽名交易(需要您確認)"
+ },
+ "bio_sendTransaction": {
+ "label": "發送交易",
+ "description": "請求發送轉帳(需要您確認)"
+ },
+ "requestsPermissions": "請求以下權限",
+ "permissionNote": "敏感操作需要您的確認",
+ "reject": "拒絕",
+ "approve": "允許"
+}
diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json
index fbb010382..59dc8a04a 100644
--- a/src/i18n/locales/zh-TW/settings.json
+++ b/src/i18n/locales/zh-TW/settings.json
@@ -98,7 +98,8 @@
"storage": "儲存空間",
"viewMnemonic": "查看助記詞",
"walletChains": "錢包網路",
- "walletManagement": "錢包管理"
+ "walletManagement": "錢包管理",
+ "miniappSources": "小程序可信源"
},
"language": "語言",
"languagePage": {
diff --git a/src/lib/crypto/derivation.ts b/src/lib/crypto/derivation.ts
index 72ba8a8bc..62625622e 100644
--- a/src/lib/crypto/derivation.ts
+++ b/src/lib/crypto/derivation.ts
@@ -1,6 +1,6 @@
/**
* 密钥派生模块 - BIP32/BIP44
- *
+ *
* 支持的链:
* - Ethereum (m/44'/60'/0'/0/x)
* - Bitcoin (m/44'/0'/0'/0/x)
@@ -8,17 +8,20 @@
* - BFMeta (m/44'/9999'/0'/0/x)
*/
-import { HDKey } from '@scure/bip32'
-import { mnemonicToSeedSync } from '@scure/bip39'
-import { secp256k1 } from '@noble/curves/secp256k1.js'
-import { keccak_256 } from '@noble/hashes/sha3.js'
-import { sha256 } from '@noble/hashes/sha2.js'
-import { ripemd160 } from '@noble/hashes/legacy.js'
-import { bytesToHex } from '@noble/hashes/utils.js'
+import { HDKey } from '@scure/bip32';
+import { mnemonicToSeedSync } from '@scure/bip39';
+import { secp256k1 } from '@noble/curves/secp256k1.js';
+import { keccak_256 } from '@noble/hashes/sha3.js';
+import { sha256 } from '@noble/hashes/sha2.js';
+import { ripemd160 } from '@noble/hashes/legacy.js';
+import { bytesToHex } from '@noble/hashes/utils.js';
+import i18n from '@/i18n';
+
+const t = i18n.t.bind(i18n);
// ==================== 类型定义 ====================
-export type ChainType = 'ethereum' | 'bitcoin' | 'tron' | 'bfmeta'
+export type ChainType = 'ethereum' | 'bitcoin' | 'tron' | 'bfmeta';
/**
* Bitcoin BIP purpose codes
@@ -27,21 +30,21 @@ export type ChainType = 'ethereum' | 'bitcoin' | 'tron' | 'bfmeta'
* - 84: Native SegWit P2WPKH (starts with bc1q)
* - 86: Taproot P2TR (starts with bc1p)
*/
-export type BitcoinPurpose = 44 | 49 | 84 | 86
+export type BitcoinPurpose = 44 | 49 | 84 | 86;
export interface DerivedKey {
/** 私钥 (hex) */
- privateKey: string
+ privateKey: string;
/** 公钥 (hex, compressed) */
- publicKey: string
+ publicKey: string;
/** 地址 */
- address: string
+ address: string;
/** 派生路径 */
- path: string
+ path: string;
/** 链类型 */
- chain: ChainType
+ chain: ChainType;
/** Bitcoin purpose (only for bitcoin chain) */
- purpose?: BitcoinPurpose
+ purpose?: BitcoinPurpose;
}
// BIP44 coin types
@@ -50,7 +53,7 @@ const COIN_TYPES: Record = {
bitcoin: 0,
tron: 195,
bfmeta: 9999,
-}
+};
// ==================== 核心函数 ====================
@@ -58,43 +61,32 @@ const COIN_TYPES: Record = {
* 从助记词派生 HD 密钥
*/
export function deriveHDKey(mnemonic: string, password?: string): HDKey {
- const seed = mnemonicToSeedSync(mnemonic, password)
- return HDKey.fromMasterSeed(seed)
+ const seed = mnemonicToSeedSync(mnemonic, password);
+ return HDKey.fromMasterSeed(seed);
}
/**
* 获取 BIP44 路径
* m / purpose' / coin_type' / account' / change / address_index
*/
-export function getBIP44Path(
- chain: ChainType,
- account = 0,
- change = 0,
- index = 0
-): string {
- const coinType = COIN_TYPES[chain]
- return `m/44'/${coinType}'/${account}'/${change}/${index}`
+export function getBIP44Path(chain: ChainType, account = 0, change = 0, index = 0): string {
+ const coinType = COIN_TYPES[chain];
+ return `m/44'/${coinType}'/${account}'/${change}/${index}`;
}
/**
* 获取通用 BIP 路径(支持自定义 purpose)
* m / purpose' / coin_type' / account' / change / address_index
*/
-export function getBIPPath(
- purpose: number,
- coinType: number,
- account = 0,
- change = 0,
- index = 0
-): string {
- return `m/${purpose}'/${coinType}'/${account}'/${change}/${index}`
+export function getBIPPath(purpose: number, coinType: number, account = 0, change = 0, index = 0): string {
+ return `m/${purpose}'/${coinType}'/${account}'/${change}/${index}`;
}
/**
* 从 HD 密钥派生子密钥
*/
export function deriveChildKey(hdKey: HDKey, path: string): HDKey {
- return hdKey.derive(path)
+ return hdKey.derive(path);
}
// ==================== 地址生成 ====================
@@ -104,59 +96,56 @@ export function deriveChildKey(hdKey: HDKey, path: string): HDKey {
*/
export function privateKeyToEthereumAddress(privateKey: Uint8Array): string {
// 从私钥生成未压缩公钥(不带前缀 04)
- const uncompressedPubKey = secp256k1.getPublicKey(privateKey, false)
- const pubKeyWithoutPrefix = uncompressedPubKey.slice(1) // 去掉 04 前缀
+ const uncompressedPubKey = secp256k1.getPublicKey(privateKey, false);
+ const pubKeyWithoutPrefix = uncompressedPubKey.slice(1); // 去掉 04 前缀
// Keccak256 哈希,取后 20 字节
- const hash = keccak_256(pubKeyWithoutPrefix)
- const address = hash.slice(-20) as Uint8Array
+ const hash = keccak_256(pubKeyWithoutPrefix);
+ const address = hash.slice(-20) as Uint8Array;
- return '0x' + bytesToHex(address)
+ return '0x' + bytesToHex(address);
}
/**
* 以太坊地址校验和 (EIP-55)
*/
export function toChecksumAddress(address: string): string {
- const addr = address.toLowerCase().replace('0x', '')
+ const addr = address.toLowerCase().replace('0x', '');
// 将字符串转换为 Uint8Array
- const encoder = new TextEncoder()
- const hash = bytesToHex(keccak_256(encoder.encode(addr)))
+ const encoder = new TextEncoder();
+ const hash = bytesToHex(keccak_256(encoder.encode(addr)));
- let checksumAddress = '0x'
+ let checksumAddress = '0x';
for (let i = 0; i < addr.length; i++) {
if (parseInt(hash[i]!, 16) >= 8) {
- checksumAddress += addr[i]!.toUpperCase()
+ checksumAddress += addr[i]!.toUpperCase();
} else {
- checksumAddress += addr[i]!
+ checksumAddress += addr[i]!;
}
}
- return checksumAddress
+ return checksumAddress;
}
/**
* 从公钥生成比特币地址 (P2PKH, Legacy - BIP44)
* Addresses start with '1' on mainnet
*/
-export function publicKeyToBitcoinAddress(
- publicKey: Uint8Array,
- network: 'mainnet' | 'testnet' = 'mainnet'
-): string {
+export function publicKeyToBitcoinAddress(publicKey: Uint8Array, network: 'mainnet' | 'testnet' = 'mainnet'): string {
// SHA256 -> RIPEMD160
- const sha = sha256(publicKey)
- const hash160 = ripemd160(sha)
+ const sha = sha256(publicKey);
+ const hash160 = ripemd160(sha);
// 添加网络前缀
- const prefix = network === 'mainnet' ? 0x00 : 0x6f
- const prefixed = new Uint8Array([prefix, ...hash160])
+ const prefix = network === 'mainnet' ? 0x00 : 0x6f;
+ const prefixed = new Uint8Array([prefix, ...hash160]);
// 双 SHA256 校验和
- const checksum = sha256(sha256(prefixed)).slice(0, 4)
- const full = new Uint8Array([...prefixed, ...checksum])
+ const checksum = sha256(sha256(prefixed)).slice(0, 4);
+ const full = new Uint8Array([...prefixed, ...checksum]);
// Base58 编码
- return base58Encode(full)
+ return base58Encode(full);
}
/**
@@ -165,28 +154,28 @@ export function publicKeyToBitcoinAddress(
*/
export function publicKeyToNestedSegwitAddress(
publicKey: Uint8Array,
- network: 'mainnet' | 'testnet' = 'mainnet'
+ network: 'mainnet' | 'testnet' = 'mainnet',
): string {
// SHA256 -> RIPEMD160 of public key
- const sha = sha256(publicKey)
- const pubKeyHash = ripemd160(sha)
+ const sha = sha256(publicKey);
+ const pubKeyHash = ripemd160(sha);
// Create witness script: OP_0 <20-byte-pubkey-hash>
- const witnessScript = new Uint8Array([0x00, 0x14, ...pubKeyHash])
+ const witnessScript = new Uint8Array([0x00, 0x14, ...pubKeyHash]);
// Hash the witness script
- const scriptSha = sha256(witnessScript)
- const scriptHash = ripemd160(scriptSha)
+ const scriptSha = sha256(witnessScript);
+ const scriptHash = ripemd160(scriptSha);
// Add P2SH prefix
- const prefix = network === 'mainnet' ? 0x05 : 0xc4
- const prefixed = new Uint8Array([prefix, ...scriptHash])
+ const prefix = network === 'mainnet' ? 0x05 : 0xc4;
+ const prefixed = new Uint8Array([prefix, ...scriptHash]);
// Double SHA256 checksum
- const checksum = sha256(sha256(prefixed)).slice(0, 4)
- const full = new Uint8Array([...prefixed, ...checksum])
+ const checksum = sha256(sha256(prefixed)).slice(0, 4);
+ const full = new Uint8Array([...prefixed, ...checksum]);
- return base58Encode(full)
+ return base58Encode(full);
}
/**
@@ -195,15 +184,15 @@ export function publicKeyToNestedSegwitAddress(
*/
export function publicKeyToNativeSegwitAddress(
publicKey: Uint8Array,
- network: 'mainnet' | 'testnet' = 'mainnet'
+ network: 'mainnet' | 'testnet' = 'mainnet',
): string {
// SHA256 -> RIPEMD160 of public key
- const sha = sha256(publicKey)
- const pubKeyHash = ripemd160(sha)
+ const sha = sha256(publicKey);
+ const pubKeyHash = ripemd160(sha);
// Bech32 encode with witness version 0
- const hrp = network === 'mainnet' ? 'bc' : 'tb'
- return bech32Encode(hrp, 0, pubKeyHash)
+ const hrp = network === 'mainnet' ? 'bc' : 'tb';
+ return bech32Encode(hrp, 0, pubKeyHash);
}
/**
@@ -211,13 +200,10 @@ export function publicKeyToNativeSegwitAddress(
* Addresses start with 'bc1p' on mainnet
* Note: This is a simplified implementation that uses key-path only
*/
-export function publicKeyToTaprootAddress(
- publicKey: Uint8Array,
- network: 'mainnet' | 'testnet' = 'mainnet'
-): string {
+export function publicKeyToTaprootAddress(publicKey: Uint8Array, network: 'mainnet' | 'testnet' = 'mainnet'): string {
// For Taproot, we need to convert to x-only public key (32 bytes)
// and apply the taproot tweak
- const xOnlyPubKey = publicKey.length === 33 ? publicKey.slice(1) : publicKey.slice(1, 33)
+ const xOnlyPubKey = publicKey.length === 33 ? publicKey.slice(1) : publicKey.slice(1, 33);
// Apply taproot tweak (BIP341)
// tweakedPubKey = pubKey + hash_tag("TapTweak", pubKey) * G
@@ -228,8 +214,8 @@ export function publicKeyToTaprootAddress(
// This is a simplified version - full implementation needs secp256k1 point operations
// Bech32m encode with witness version 1
- const hrp = network === 'mainnet' ? 'bc' : 'tb'
- return bech32mEncode(hrp, 1, xOnlyPubKey)
+ const hrp = network === 'mainnet' ? 'bc' : 'tb';
+ return bech32mEncode(hrp, 1, xOnlyPubKey);
}
/**
@@ -237,146 +223,146 @@ export function publicKeyToTaprootAddress(
*/
export function privateKeyToTronAddress(privateKey: Uint8Array): string {
// 与以太坊类似,但前缀是 0x41
- const uncompressedPubKey = secp256k1.getPublicKey(privateKey, false)
- const pubKeyWithoutPrefix = uncompressedPubKey.slice(1)
-
- const hash = keccak_256(pubKeyWithoutPrefix)
- const addressBytes = hash.slice(-20)
-
+ const uncompressedPubKey = secp256k1.getPublicKey(privateKey, false);
+ const pubKeyWithoutPrefix = uncompressedPubKey.slice(1);
+
+ const hash = keccak_256(pubKeyWithoutPrefix);
+ const addressBytes = hash.slice(-20);
+
// 添加 Tron 前缀 0x41
- const prefixed = new Uint8Array([0x41, ...addressBytes])
-
+ const prefixed = new Uint8Array([0x41, ...addressBytes]);
+
// 双 SHA256 校验和
- const checksum = sha256(sha256(prefixed)).slice(0, 4)
- const full = new Uint8Array([...prefixed, ...checksum])
-
+ const checksum = sha256(sha256(prefixed)).slice(0, 4);
+ const full = new Uint8Array([...prefixed, ...checksum]);
+
// Base58 编码
- return base58Encode(full)
+ return base58Encode(full);
}
// ==================== Base58 编码 ====================
-const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
+const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
function base58Encode(bytes: Uint8Array): string {
- const digits = [0]
+ const digits = [0];
for (const byte of bytes) {
- let carry = byte
+ let carry = byte;
for (let i = 0; i < digits.length; i++) {
- carry += digits[i]! << 8
- digits[i] = carry % 58
- carry = (carry / 58) | 0
+ carry += digits[i]! << 8;
+ digits[i] = carry % 58;
+ carry = (carry / 58) | 0;
}
while (carry > 0) {
- digits.push(carry % 58)
- carry = (carry / 58) | 0
+ digits.push(carry % 58);
+ carry = (carry / 58) | 0;
}
}
// 处理前导零
- let result = ''
+ let result = '';
for (const byte of bytes) {
- if (byte === 0) result += BASE58_ALPHABET[0]!
- else break
+ if (byte === 0) result += BASE58_ALPHABET[0]!;
+ else break;
}
// 反转并转换
for (let i = digits.length - 1; i >= 0; i--) {
- result += BASE58_ALPHABET[digits[i]!]
+ result += BASE58_ALPHABET[digits[i]!];
}
- return result
+ return result;
}
// ==================== Bech32/Bech32m 编码 ====================
-const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
+const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
-const BECH32_GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
+const BECH32_GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
function bech32Polymod(values: number[]): number {
- let chk = 1
+ let chk = 1;
for (const v of values) {
- const top = chk >> 25
- chk = ((chk & 0x1ffffff) << 5) ^ v
+ const top = chk >> 25;
+ chk = ((chk & 0x1ffffff) << 5) ^ v;
for (let i = 0; i < 5; i++) {
if ((top >> i) & 1) {
- chk ^= BECH32_GENERATOR[i]!
+ chk ^= BECH32_GENERATOR[i]!;
}
}
}
- return chk
+ return chk;
}
function bech32HrpExpand(hrp: string): number[] {
- const ret: number[] = []
+ const ret: number[] = [];
for (const c of hrp) {
- ret.push(c.charCodeAt(0) >> 5)
+ ret.push(c.charCodeAt(0) >> 5);
}
- ret.push(0)
+ ret.push(0);
for (const c of hrp) {
- ret.push(c.charCodeAt(0) & 31)
+ ret.push(c.charCodeAt(0) & 31);
}
- return ret
+ return ret;
}
function bech32CreateChecksum(hrp: string, data: number[], isBech32m: boolean): number[] {
- const values = [...bech32HrpExpand(hrp), ...data]
+ const values = [...bech32HrpExpand(hrp), ...data];
// Bech32m uses 0x2bc830a3, Bech32 uses 1
- const constant = isBech32m ? 0x2bc830a3 : 1
- const polymod = bech32Polymod([...values, 0, 0, 0, 0, 0, 0]) ^ constant
- const ret: number[] = []
+ const constant = isBech32m ? 0x2bc830a3 : 1;
+ const polymod = bech32Polymod([...values, 0, 0, 0, 0, 0, 0]) ^ constant;
+ const ret: number[] = [];
for (let i = 0; i < 6; i++) {
- ret.push((polymod >> (5 * (5 - i))) & 31)
+ ret.push((polymod >> (5 * (5 - i))) & 31);
}
- return ret
+ return ret;
}
function convertBits(data: Uint8Array, fromBits: number, toBits: number, pad: boolean): number[] {
- let acc = 0
- let bits = 0
- const ret: number[] = []
- const maxv = (1 << toBits) - 1
+ let acc = 0;
+ let bits = 0;
+ const ret: number[] = [];
+ const maxv = (1 << toBits) - 1;
for (const value of data) {
- acc = (acc << fromBits) | value
- bits += fromBits
+ acc = (acc << fromBits) | value;
+ bits += fromBits;
while (bits >= toBits) {
- bits -= toBits
- ret.push((acc >> bits) & maxv)
+ bits -= toBits;
+ ret.push((acc >> bits) & maxv);
}
}
if (pad) {
if (bits > 0) {
- ret.push((acc << (toBits - bits)) & maxv)
+ ret.push((acc << (toBits - bits)) & maxv);
}
}
- return ret
+ return ret;
}
/**
* Bech32 编码 (用于 P2WPKH - witness version 0)
*/
function bech32Encode(hrp: string, witnessVersion: number, data: Uint8Array): string {
- const converted = convertBits(data, 8, 5, true)
- const combined = [witnessVersion, ...converted]
- const checksum = bech32CreateChecksum(hrp, combined, false)
- const encoded = [...combined, ...checksum].map((d) => BECH32_CHARSET[d]).join('')
- return `${hrp}1${encoded}`
+ const converted = convertBits(data, 8, 5, true);
+ const combined = [witnessVersion, ...converted];
+ const checksum = bech32CreateChecksum(hrp, combined, false);
+ const encoded = [...combined, ...checksum].map((d) => BECH32_CHARSET[d]).join('');
+ return `${hrp}1${encoded}`;
}
/**
* Bech32m 编码 (用于 P2TR - witness version 1+)
*/
function bech32mEncode(hrp: string, witnessVersion: number, data: Uint8Array): string {
- const converted = convertBits(data, 8, 5, true)
- const combined = [witnessVersion, ...converted]
- const checksum = bech32CreateChecksum(hrp, combined, true)
- const encoded = [...combined, ...checksum].map((d) => BECH32_CHARSET[d]).join('')
- return `${hrp}1${encoded}`
+ const converted = convertBits(data, 8, 5, true);
+ const combined = [witnessVersion, ...converted];
+ const checksum = bech32CreateChecksum(hrp, combined, true);
+ const encoded = [...combined, ...checksum].map((d) => BECH32_CHARSET[d]).join('');
+ return `${hrp}1${encoded}`;
}
// ==================== 主要 API ====================
@@ -384,40 +370,35 @@ function bech32mEncode(hrp: string, witnessVersion: number, data: Uint8Array): s
/**
* 从助记词派生指定链的密钥
*/
-export function deriveKey(
- mnemonic: string,
- chain: ChainType,
- index = 0,
- account = 0
-): DerivedKey {
- const hdKey = deriveHDKey(mnemonic)
- const path = getBIP44Path(chain, account, 0, index)
- const childKey = deriveChildKey(hdKey, path)
+export function deriveKey(mnemonic: string, chain: ChainType, index = 0, account = 0): DerivedKey {
+ const hdKey = deriveHDKey(mnemonic);
+ const path = getBIP44Path(chain, account, 0, index);
+ const childKey = deriveChildKey(hdKey, path);
if (!childKey.privateKey || !childKey.publicKey) {
- throw new Error('密钥派生失败')
+ throw new Error(t('error:crypto.keyDerivationFailed'));
}
- const privateKey = bytesToHex(childKey.privateKey)
- const publicKey = bytesToHex(childKey.publicKey)
+ const privateKey = bytesToHex(childKey.privateKey);
+ const publicKey = bytesToHex(childKey.publicKey);
- let address: string
+ let address: string;
switch (chain) {
case 'ethereum':
- address = toChecksumAddress(privateKeyToEthereumAddress(childKey.privateKey))
- break
+ address = toChecksumAddress(privateKeyToEthereumAddress(childKey.privateKey));
+ break;
case 'bitcoin':
- address = publicKeyToBitcoinAddress(childKey.publicKey)
- break
+ address = publicKeyToBitcoinAddress(childKey.publicKey);
+ break;
case 'tron':
- address = privateKeyToTronAddress(childKey.privateKey)
- break
+ address = privateKeyToTronAddress(childKey.privateKey);
+ break;
case 'bfmeta':
// BFMeta 使用与以太坊类似的地址格式
- address = toChecksumAddress(privateKeyToEthereumAddress(childKey.privateKey))
- break
+ address = toChecksumAddress(privateKeyToEthereumAddress(childKey.privateKey));
+ break;
default:
- throw new Error(`不支持的链类型: ${chain}`)
+ throw new Error(t('error:chain.unsupportedType', { chain }));
}
return {
@@ -426,7 +407,7 @@ export function deriveKey(
address,
path,
chain,
- }
+ };
}
/**
@@ -437,35 +418,35 @@ export function deriveBitcoinKey(
purpose: BitcoinPurpose = 44,
index = 0,
account = 0,
- network: 'mainnet' | 'testnet' = 'mainnet'
+ network: 'mainnet' | 'testnet' = 'mainnet',
): DerivedKey {
- const hdKey = deriveHDKey(mnemonic)
- const path = getBIPPath(purpose, COIN_TYPES.bitcoin, account, 0, index)
- const childKey = deriveChildKey(hdKey, path)
+ const hdKey = deriveHDKey(mnemonic);
+ const path = getBIPPath(purpose, COIN_TYPES.bitcoin, account, 0, index);
+ const childKey = deriveChildKey(hdKey, path);
if (!childKey.privateKey || !childKey.publicKey) {
- throw new Error('密钥派生失败')
+ throw new Error(t('error:crypto.keyDerivationFailed'));
}
- const privateKey = bytesToHex(childKey.privateKey)
- const publicKey = bytesToHex(childKey.publicKey)
+ const privateKey = bytesToHex(childKey.privateKey);
+ const publicKey = bytesToHex(childKey.publicKey);
- let address: string
+ let address: string;
switch (purpose) {
case 44:
- address = publicKeyToBitcoinAddress(childKey.publicKey, network)
- break
+ address = publicKeyToBitcoinAddress(childKey.publicKey, network);
+ break;
case 49:
- address = publicKeyToNestedSegwitAddress(childKey.publicKey, network)
- break
+ address = publicKeyToNestedSegwitAddress(childKey.publicKey, network);
+ break;
case 84:
- address = publicKeyToNativeSegwitAddress(childKey.publicKey, network)
- break
+ address = publicKeyToNativeSegwitAddress(childKey.publicKey, network);
+ break;
case 86:
- address = publicKeyToTaprootAddress(childKey.publicKey, network)
- break
+ address = publicKeyToTaprootAddress(childKey.publicKey, network);
+ break;
default:
- throw new Error(`不支持的 Bitcoin purpose: ${purpose}`)
+ throw new Error(t('error:crypto.unsupportedBitcoinPurpose', { purpose: String(purpose) }));
}
return {
@@ -475,7 +456,7 @@ export function deriveBitcoinKey(
path,
chain: 'bitcoin',
purpose,
- }
+ };
}
/**
@@ -485,10 +466,10 @@ export function deriveAllBitcoinKeys(
mnemonic: string,
index = 0,
account = 0,
- network: 'mainnet' | 'testnet' = 'mainnet'
+ network: 'mainnet' | 'testnet' = 'mainnet',
): DerivedKey[] {
- const purposes: BitcoinPurpose[] = [44, 49, 84, 86]
- return purposes.map((purpose) => deriveBitcoinKey(mnemonic, purpose, index, account, network))
+ const purposes: BitcoinPurpose[] = [44, 49, 84, 86];
+ return purposes.map((purpose) => deriveBitcoinKey(mnemonic, purpose, index, account, network));
}
/**
@@ -497,35 +478,31 @@ export function deriveAllBitcoinKeys(
export function deriveMultiChainKeys(
mnemonic: string,
chains: ChainType[] = ['ethereum', 'bitcoin', 'tron'],
- index = 0
+ index = 0,
): DerivedKey[] {
- return chains.map((chain) => deriveKey(mnemonic, chain, index))
+ return chains.map((chain) => deriveKey(mnemonic, chain, index));
}
/**
* 从助记词派生所有可能的地址(用于重复检测)
* 包括:ETH、BFMeta、Tron、以及 Bitcoin 的所有 purpose
*/
-export function deriveAllAddresses(
- mnemonic: string,
- index = 0,
- account = 0
-): DerivedKey[] {
- const results: DerivedKey[] = []
+export function deriveAllAddresses(mnemonic: string, index = 0, account = 0): DerivedKey[] {
+ const results: DerivedKey[] = [];
// Ethereum
- results.push(deriveKey(mnemonic, 'ethereum', index, account))
+ results.push(deriveKey(mnemonic, 'ethereum', index, account));
// BFMeta
- results.push(deriveKey(mnemonic, 'bfmeta', index, account))
+ results.push(deriveKey(mnemonic, 'bfmeta', index, account));
// Tron
- results.push(deriveKey(mnemonic, 'tron', index, account))
+ results.push(deriveKey(mnemonic, 'tron', index, account));
// Bitcoin (all purposes)
- results.push(...deriveAllBitcoinKeys(mnemonic, index, account))
+ results.push(...deriveAllBitcoinKeys(mnemonic, index, account));
- return results
+ return results;
}
/**
@@ -534,16 +511,16 @@ export function deriveAllAddresses(
* 返回 32 字节的私钥作为加密密钥
*/
export function deriveEncryptionKeyFromMnemonic(mnemonic: string): Uint8Array {
- const hdKey = deriveHDKey(mnemonic)
+ const hdKey = deriveHDKey(mnemonic);
// 使用特殊路径:change=1 用于加密密钥派生,区别于正常的 change=0
- const path = "m/44'/9999'/0'/1/0"
- const childKey = deriveChildKey(hdKey, path)
-
+ const path = "m/44'/9999'/0'/1/0";
+ const childKey = deriveChildKey(hdKey, path);
+
if (!childKey.privateKey) {
- throw new Error('密钥派生失败')
+ throw new Error(t('error:crypto.keyDerivationFailed'));
}
-
- return childKey.privateKey
+
+ return childKey.privateKey;
}
/**
@@ -552,10 +529,10 @@ export function deriveEncryptionKeyFromMnemonic(mnemonic: string): Uint8Array {
* 返回 32 字节的密钥
*/
export function deriveEncryptionKeyFromSecret(secret: string): Uint8Array {
- const encoder = new TextEncoder()
+ const encoder = new TextEncoder();
// 使用固定盐值前缀确保密钥唯一性
- const saltedSecret = encoder.encode(`KeyApp:EncryptionKey:${secret}`)
- return sha256(saltedSecret)
+ const saltedSecret = encoder.encode(`KeyApp:EncryptionKey:${secret}`);
+ return sha256(saltedSecret);
}
/**
@@ -565,17 +542,17 @@ export function isValidAddress(address: string, chain: ChainType): boolean {
switch (chain) {
case 'ethereum':
case 'bfmeta':
- return /^0x[a-fA-F0-9]{40}$/.test(address)
+ return /^0x[a-fA-F0-9]{40}$/.test(address);
case 'bitcoin':
// Legacy (1...), Nested SegWit (3...), Native SegWit (bc1q...), Taproot (bc1p...)
return (
/^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(address) ||
/^bc1q[a-z0-9]{38,}$/.test(address) ||
/^bc1p[a-z0-9]{58}$/.test(address)
- )
+ );
case 'tron':
- return /^T[a-zA-Z0-9]{33}$/.test(address)
+ return /^T[a-zA-Z0-9]{33}$/.test(address);
default:
- return false
+ return false;
}
}
diff --git a/src/lib/crypto/encryption.ts b/src/lib/crypto/encryption.ts
index bbef1178c..59b9f3583 100644
--- a/src/lib/crypto/encryption.ts
+++ b/src/lib/crypto/encryption.ts
@@ -1,46 +1,41 @@
/**
* 密码加密模块 - 使用 AES-GCM + PBKDF2
- *
+ *
* 安全设计:
* 1. PBKDF2 从密码派生密钥(防止暴力破解)
* 2. AES-256-GCM 加密数据(认证加密)
* 3. 随机 salt 和 iv(防止彩虹表攻击)
*/
-const PBKDF2_ITERATIONS = 100000
-const SALT_LENGTH = 16
-const IV_LENGTH = 12
-const KEY_LENGTH = 256
+const PBKDF2_ITERATIONS = 100000;
+const SALT_LENGTH = 16;
+const IV_LENGTH = 12;
+const KEY_LENGTH = 256;
+
+import i18n from '@/i18n';
+
+const t = i18n.t.bind(i18n);
interface EncryptedData {
/** Base64 编码的密文 */
- ciphertext: string
+ ciphertext: string;
/** Base64 编码的 salt */
- salt: string
+ salt: string;
/** Base64 编码的 iv */
- iv: string
+ iv: string;
/** 迭代次数 */
- iterations: number
+ iterations: number;
}
/**
* 从密码派生加密密钥
*/
-async function deriveKey(
- password: string,
- salt: Uint8Array
-): Promise {
- const encoder = new TextEncoder()
- const passwordBuffer = encoder.encode(password)
+async function deriveKey(password: string, salt: Uint8Array): Promise {
+ const encoder = new TextEncoder();
+ const passwordBuffer = encoder.encode(password);
// 导入密码作为原始密钥
- const passwordKey = await crypto.subtle.importKey(
- 'raw',
- passwordBuffer,
- 'PBKDF2',
- false,
- ['deriveKey']
- )
+ const passwordKey = await crypto.subtle.importKey('raw', passwordBuffer, 'PBKDF2', false, ['deriveKey']);
// 使用 PBKDF2 派生 AES 密钥
return crypto.subtle.deriveKey(
@@ -53,8 +48,8 @@ async function deriveKey(
passwordKey,
{ name: 'AES-GCM', length: KEY_LENGTH },
false,
- ['encrypt', 'decrypt']
- )
+ ['encrypt', 'decrypt'],
+ );
}
/**
@@ -63,33 +58,26 @@ async function deriveKey(
* @param password 密码
* @returns 加密后的数据对象
*/
-export async function encrypt(
- plaintext: string,
- password: string
-): Promise {
- const encoder = new TextEncoder()
- const data = encoder.encode(plaintext)
+export async function encrypt(plaintext: string, password: string): Promise {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(plaintext);
// 生成随机 salt 和 iv
- const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH))
- const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH))
+ const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
// 派生密钥
- const key = await deriveKey(password, salt)
+ const key = await deriveKey(password, salt);
// AES-GCM 加密
- const ciphertext = await crypto.subtle.encrypt(
- { name: 'AES-GCM', iv },
- key,
- data
- )
+ const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data);
return {
ciphertext: arrayBufferToBase64(ciphertext),
salt: arrayBufferToBase64(salt),
iv: arrayBufferToBase64(iv),
iterations: PBKDF2_ITERATIONS,
- }
+ };
}
/**
@@ -99,44 +87,38 @@ export async function encrypt(
* @returns 解密后的明文
* @throws 密码错误时抛出异常
*/
-export async function decrypt(
- encrypted: EncryptedData,
- password: string
-): Promise {
- const salt = base64ToUint8Array(encrypted.salt)
- const iv = base64ToUint8Array(encrypted.iv)
- const ciphertext = base64ToUint8Array(encrypted.ciphertext)
+export async function decrypt(encrypted: EncryptedData, password: string): Promise {
+ const salt = base64ToUint8Array(encrypted.salt);
+ const iv = base64ToUint8Array(encrypted.iv);
+ const ciphertext = base64ToUint8Array(encrypted.ciphertext);
// 派生密钥
- const key = await deriveKey(password, salt)
+ const key = await deriveKey(password, salt);
try {
// AES-GCM 解密
const plaintext = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv as BufferSource },
key,
- ciphertext as BufferSource
- )
+ ciphertext as BufferSource,
+ );
- const decoder = new TextDecoder()
- return decoder.decode(plaintext)
+ const decoder = new TextDecoder();
+ return decoder.decode(plaintext);
} catch {
- throw new Error('解密失败:密码错误或数据损坏')
+ throw new Error(t('error:crypto.decryptFailed'));
}
}
/**
* 验证密码是否正确(尝试解密)
*/
-export async function verifyPassword(
- encrypted: EncryptedData,
- password: string
-): Promise {
+export async function verifyPassword(encrypted: EncryptedData, password: string): Promise {
try {
- await decrypt(encrypted, password)
- return true
+ await decrypt(encrypted, password);
+ return true;
} catch {
- return false
+ return false;
}
}
@@ -144,15 +126,12 @@ export async function verifyPassword(
* 使用原始密钥加密(不使用 PBKDF2)
* 用于从助记词派生的密钥直接加密
*/
-export async function encryptWithRawKey(
- plaintext: string,
- rawKey: Uint8Array
-): Promise {
- const encoder = new TextEncoder()
- const data = encoder.encode(plaintext)
+export async function encryptWithRawKey(plaintext: string, rawKey: Uint8Array): Promise {
+ const encoder = new TextEncoder();
+ const data = encoder.encode(plaintext);
// 生成随机 iv(不需要 salt,因为密钥已经是完整的)
- const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH))
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
// 导入原始密钥
const key = await crypto.subtle.importKey(
@@ -160,33 +139,30 @@ export async function encryptWithRawKey(
rawKey.buffer.slice(rawKey.byteOffset, rawKey.byteOffset + rawKey.byteLength) as ArrayBuffer,
{ name: 'AES-GCM', length: KEY_LENGTH },
false,
- ['encrypt']
- )
+ ['encrypt'],
+ );
// AES-GCM 加密
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv as BufferSource },
key,
- data as BufferSource
- )
+ data as BufferSource,
+ );
return {
ciphertext: arrayBufferToBase64(ciphertext),
salt: '', // 不需要 salt
iv: arrayBufferToBase64(iv),
iterations: 0, // 标记为使用原始密钥
- }
+ };
}
/**
* 使用原始密钥解密
*/
-export async function decryptWithRawKey(
- encrypted: EncryptedData,
- rawKey: Uint8Array
-): Promise {
- const iv = base64ToUint8Array(encrypted.iv)
- const ciphertext = base64ToUint8Array(encrypted.ciphertext)
+export async function decryptWithRawKey(encrypted: EncryptedData, rawKey: Uint8Array): Promise {
+ const iv = base64ToUint8Array(encrypted.iv);
+ const ciphertext = base64ToUint8Array(encrypted.ciphertext);
// 导入原始密钥
const key = await crypto.subtle.importKey(
@@ -194,43 +170,43 @@ export async function decryptWithRawKey(
rawKey.buffer.slice(rawKey.byteOffset, rawKey.byteOffset + rawKey.byteLength) as ArrayBuffer,
{ name: 'AES-GCM', length: KEY_LENGTH },
false,
- ['decrypt']
- )
+ ['decrypt'],
+ );
try {
// AES-GCM 解密
const plaintext = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv as BufferSource },
key,
- ciphertext as BufferSource
- )
+ ciphertext as BufferSource,
+ );
- const decoder = new TextDecoder()
- return decoder.decode(plaintext)
+ const decoder = new TextDecoder();
+ return decoder.decode(plaintext);
} catch {
- throw new Error('解密失败:密钥错误或数据损坏')
+ throw new Error(t('error:crypto.decryptKeyFailed'));
}
}
// 工具函数:ArrayBuffer 转 Base64
function arrayBufferToBase64(buffer: ArrayBuffer | Uint8Array): string {
- const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)
- let binary = ''
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
+ let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
- binary += String.fromCharCode(bytes[i]!)
+ binary += String.fromCharCode(bytes[i]!);
}
- return btoa(binary)
+ return btoa(binary);
}
// 工具函数:Base64 转 Uint8Array
function base64ToUint8Array(base64: string): Uint8Array {
- const binary = atob(base64)
- const bytes = new Uint8Array(binary.length)
+ const binary = atob(base64);
+ const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
- bytes[i] = binary.charCodeAt(i)
+ bytes[i] = binary.charCodeAt(i);
}
- return bytes
+ return bytes;
}
// 导出类型
-export type { EncryptedData }
+export type { EncryptedData };
diff --git a/src/lib/crypto/secure-storage.ts b/src/lib/crypto/secure-storage.ts
index 131e55c08..a1428eae2 100644
--- a/src/lib/crypto/secure-storage.ts
+++ b/src/lib/crypto/secure-storage.ts
@@ -1,71 +1,74 @@
/**
* 安全存储抽象层
- *
+ *
* - DWEB 环境:使用原生生物识别 + Keychain/Keystore
* - Web 环境:使用 AES-GCM 密码加密 + localStorage
*/
-import { encrypt, decrypt, type EncryptedData } from './encryption'
+import { encrypt, decrypt, type EncryptedData } from './encryption';
+import i18n from '@/i18n';
+
+const t = i18n.t.bind(i18n);
// ==================== 类型定义 ====================
export interface BiometricOptions {
- title?: string | undefined
- subtitle?: string | undefined
- negativeButtonText?: string | undefined
- maxAttempts?: number | undefined
+ title?: string | undefined;
+ subtitle?: string | undefined;
+ negativeButtonText?: string | undefined;
+ maxAttempts?: number | undefined;
}
export interface BiometricResult {
- success: boolean
- code?: number | undefined
- errorDetail?: string | undefined
+ success: boolean;
+ code?: number | undefined;
+ errorDetail?: string | undefined;
}
export interface SecureStorageOptions {
/** 使用密码加密(Web 模式) */
- password?: string | undefined
+ password?: string | undefined;
/** 使用生物识别(DWEB 模式) */
- useBiometric?: boolean | undefined
+ useBiometric?: boolean | undefined;
/** 生物识别选项 */
- biometricOptions?: BiometricOptions | undefined
+ biometricOptions?: BiometricOptions | undefined;
}
export interface StoredData {
/** 存储类型 */
- type: 'web-encrypted' | 'dweb-biometric'
+ type: 'web-encrypted' | 'dweb-biometric';
/** 加密数据(Web 模式) */
- encrypted?: EncryptedData | undefined
+ encrypted?: EncryptedData | undefined;
/** 原始数据(DWEB 模式,由系统保护) */
- data?: string | undefined
+ data?: string | undefined;
}
// ==================== 环境检测 ====================
/** 检测是否在 DWEB 环境 */
export function isDwebEnvironment(): boolean {
- return typeof window !== 'undefined' && 'dwebTarget' in window
+ return typeof window !== 'undefined' && 'dwebTarget' in window;
}
/** DWEB biometric 插件类型 */
interface DwebBiometricPlugin {
- check(): Promise
- biometrics(): Promise<{ success: boolean; message?: string }>
+ check(): Promise;
+ biometrics(): Promise<{ success: boolean; message?: string }>;
}
/** BioetricsCheckResult 枚举值 */
-const BIOMETRIC_SUCCESS = 0
+const BIOMETRIC_SUCCESS = 0;
/** 动态导入 DWEB biometric 插件 */
async function getDwebBiometric(): Promise {
- if (!isDwebEnvironment()) return null
+ if (!isDwebEnvironment()) return null;
try {
// 使用变量规避 Vite 静态分析
- const moduleName = '@plaoc/plugins'
- const module = await import(/* @vite-ignore */ moduleName)
- return module.biometricsPlugin as DwebBiometricPlugin
+ const moduleName = '@plaoc/plugins';
+ const module = await import(/* @vite-ignore */ moduleName);
+ return module.biometricsPlugin as DwebBiometricPlugin;
} catch {
- return null
+ return null;
}
}
@@ -74,50 +77,50 @@ async function getDwebBiometric(): Promise {
export const BiometricAuth = {
/** 检查生物识别是否可用 */
async isAvailable(): Promise {
- const plugin = await getDwebBiometric()
- if (!plugin) return false
-
+ const plugin = await getDwebBiometric();
+ if (!plugin) return false;
+
try {
- const result = await plugin.check()
- return result === BIOMETRIC_SUCCESS
+ const result = await plugin.check();
+ return result === BIOMETRIC_SUCCESS;
} catch {
- return false
+ return false;
}
},
/** 验证生物识别 */
async authenticate(_options: BiometricOptions = {}): Promise {
- const plugin = await getDwebBiometric()
+ const plugin = await getDwebBiometric();
if (!plugin) {
- return { success: false, code: -1, errorDetail: '生物识别不可用' }
+ return { success: false, code: -1, errorDetail: t('error:biometric.unavailable') };
}
try {
- const result = await plugin.biometrics()
- return { success: result.success, code: result.success ? undefined : 0 }
+ const result = await plugin.biometrics();
+ return { success: result.success, code: result.success ? undefined : 0 };
} catch (err) {
- const error = err as Error & { data?: { errorCode: string; errorDetails: string } }
-
+ const error = err as Error & { data?: { errorCode: string; errorDetails: string } };
+
if (error.message?.toLowerCase().includes('authentication failed')) {
- return { success: false, code: 0 }
+ return { success: false, code: 0 };
}
-
+
if (error.data) {
return {
success: false,
code: parseInt(error.data.errorCode),
errorDetail: error.data.errorDetails,
- }
+ };
}
-
- return { success: false, code: -1, errorDetail: error.message }
+
+ return { success: false, code: -1, errorDetail: error.message };
}
},
-}
+};
// ==================== 安全存储 API ====================
-const STORAGE_PREFIX = 'secure:'
+const STORAGE_PREFIX = 'secure:';
export const SecureStorage = {
/** 检查安全存储是否可用 */
@@ -125,112 +128,105 @@ export const SecureStorage = {
return {
web: typeof localStorage !== 'undefined',
dweb: await BiometricAuth.isAvailable(),
- }
+ };
},
/** 存储数据 */
- async store(
- key: string,
- data: string,
- options: SecureStorageOptions
- ): Promise {
- const storageKey = STORAGE_PREFIX + key
-
+ async store(key: string, data: string, options: SecureStorageOptions): Promise {
+ const storageKey = STORAGE_PREFIX + key;
+
// DWEB 模式:使用生物识别保护
if (options.useBiometric && isDwebEnvironment()) {
- const authResult = await BiometricAuth.authenticate(options.biometricOptions)
+ const authResult = await BiometricAuth.authenticate(options.biometricOptions);
if (!authResult.success) {
- throw new Error('生物识别验证失败')
+ throw new Error(t('error:biometric.verificationFailed'));
}
-
+
// DWEB 环境下,数据由系统 Keychain/Keystore 保护
// 这里简化处理,实际生产环境应使用 @plaoc/plugins 的 secure storage
- const stored: StoredData = { type: 'dweb-biometric', data }
- localStorage.setItem(storageKey, JSON.stringify(stored))
- return
+ const stored: StoredData = { type: 'dweb-biometric', data };
+ localStorage.setItem(storageKey, JSON.stringify(stored));
+ return;
}
// Web 模式:使用密码加密
if (!options.password) {
- throw new Error('Web 模式需要提供密码')
+ throw new Error(t('error:secureStorage.webPasswordRequired'));
}
- const encrypted = await encrypt(data, options.password)
- const stored: StoredData = { type: 'web-encrypted', encrypted }
- localStorage.setItem(storageKey, JSON.stringify(stored))
+ const encrypted = await encrypt(data, options.password);
+ const stored: StoredData = { type: 'web-encrypted', encrypted };
+ localStorage.setItem(storageKey, JSON.stringify(stored));
},
/** 读取数据 */
- async retrieve(
- key: string,
- options: SecureStorageOptions
- ): Promise {
- const storageKey = STORAGE_PREFIX + key
- const raw = localStorage.getItem(storageKey)
-
- if (!raw) return null
+ async retrieve(key: string, options: SecureStorageOptions): Promise {
+ const storageKey = STORAGE_PREFIX + key;
+ const raw = localStorage.getItem(storageKey);
+
+ if (!raw) return null;
try {
- const stored: StoredData = JSON.parse(raw)
+ const stored: StoredData = JSON.parse(raw);
// DWEB 模式
if (stored.type === 'dweb-biometric') {
if (!isDwebEnvironment()) {
- throw new Error('需要在 DWEB 环境中访问')
+ throw new Error(t('error:secureStorage.dwebRequired'));
}
-
- const authResult = await BiometricAuth.authenticate(options.biometricOptions)
+
+ const authResult = await BiometricAuth.authenticate(options.biometricOptions);
if (!authResult.success) {
- throw new Error('生物识别验证失败')
+ throw new Error(t('error:biometric.verificationFailed'));
}
-
- return stored.data ?? null
+
+ return stored.data ?? null;
}
// Web 模式
if (stored.type === 'web-encrypted' && stored.encrypted) {
if (!options.password) {
- throw new Error('需要提供密码解密')
+ throw new Error(t('error:secureStorage.passwordRequired'));
}
- return await decrypt(stored.encrypted, options.password)
+ return await decrypt(stored.encrypted, options.password);
}
- return null
+ return null;
} catch (err) {
if (err instanceof SyntaxError) {
// 兼容旧数据格式
- return null
+ return null;
}
- throw err
+ throw err;
}
},
/** 删除数据 */
async delete(key: string): Promise {
- const storageKey = STORAGE_PREFIX + key
- localStorage.removeItem(storageKey)
+ const storageKey = STORAGE_PREFIX + key;
+ localStorage.removeItem(storageKey);
},
/** 检查数据是否存在 */
async exists(key: string): Promise {
- const storageKey = STORAGE_PREFIX + key
- return localStorage.getItem(storageKey) !== null
+ const storageKey = STORAGE_PREFIX + key;
+ return localStorage.getItem(storageKey) !== null;
},
/** 获取存储类型 */
async getType(key: string): Promise {
- const storageKey = STORAGE_PREFIX + key
- const raw = localStorage.getItem(storageKey)
- if (!raw) return null
-
+ const storageKey = STORAGE_PREFIX + key;
+ const raw = localStorage.getItem(storageKey);
+ if (!raw) return null;
+
try {
- const stored: StoredData = JSON.parse(raw)
- return stored.type
+ const stored: StoredData = JSON.parse(raw);
+ return stored.type;
} catch {
- return null
+ return null;
}
},
-}
+};
// ==================== 便捷函数 ====================
@@ -238,50 +234,47 @@ export const SecureStorage = {
export async function storeMnemonic(
walletId: string,
mnemonic: string,
- options: { password?: string; preferBiometric?: boolean } = {}
+ options: { password?: string; preferBiometric?: boolean } = {},
): Promise {
- const { dweb } = await SecureStorage.isAvailable()
-
+ const { dweb } = await SecureStorage.isAvailable();
+
// 优先使用生物识别(如果可用且用户偏好)
if (dweb && options.preferBiometric !== false) {
await SecureStorage.store(`mnemonic:${walletId}`, mnemonic, {
useBiometric: true,
biometricOptions: {
- title: '验证身份',
- subtitle: '使用生物识别保护钱包',
+ title: t('common:biometric.verifyIdentity'),
+ subtitle: t('common:biometric.protectWallet'),
},
- })
- return
+ });
+ return;
}
// 否则使用密码加密
if (!options.password) {
- throw new Error('需要提供密码')
+ throw new Error(t('error:secureStorage.passwordRequiredForMnemonic'));
}
-
+
await SecureStorage.store(`mnemonic:${walletId}`, mnemonic, {
password: options.password,
- })
+ });
}
/** 读取助记词 */
-export async function retrieveMnemonic(
- walletId: string,
- options: { password?: string } = {}
-): Promise {
- const type = await SecureStorage.getType(`mnemonic:${walletId}`)
-
+export async function retrieveMnemonic(walletId: string, options: { password?: string } = {}): Promise {
+ const type = await SecureStorage.getType(`mnemonic:${walletId}`);
+
if (type === 'dweb-biometric') {
return SecureStorage.retrieve(`mnemonic:${walletId}`, {
useBiometric: true,
biometricOptions: {
- title: '验证身份',
- subtitle: '访问钱包助记词',
+ title: t('common:biometric.verifyIdentity'),
+ subtitle: t('common:biometric.accessMnemonic'),
},
- })
+ });
}
return SecureStorage.retrieve(`mnemonic:${walletId}`, {
password: options.password,
- })
+ });
}
diff --git a/src/pages/authorize/signature.tsx b/src/pages/authorize/signature.tsx
index 8ae32a8b4..63a8d0423 100644
--- a/src/pages/authorize/signature.tsx
+++ b/src/pages/authorize/signature.tsx
@@ -1,14 +1,14 @@
-import { useCallback, useEffect, useMemo, useState } from 'react'
-import { useNavigation, useActivityParams, useFlow } from '@/stackflow'
-import { setWalletLockConfirmCallback } from '@/stackflow/activities/sheets'
-import { useTranslation } from 'react-i18next'
-import { useStore } from '@tanstack/react-store'
-import { PageHeader } from '@/components/layout/page-header'
-import { AppInfoCard } from '@/components/authorize/AppInfoCard'
-import { TransactionDetails } from '@/components/authorize/TransactionDetails'
-import { BalanceWarning } from '@/components/authorize/BalanceWarning'
-import { Button } from '@/components/ui/button'
-import { cn } from '@/lib/utils'
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useNavigation, useActivityParams, useFlow } from '@/stackflow';
+import { setWalletLockConfirmCallback } from '@/stackflow/activities/sheets';
+import { useTranslation } from 'react-i18next';
+import { useStore } from '@tanstack/react-store';
+import { PageHeader } from '@/components/layout/page-header';
+import { AppInfoCard } from '@/components/authorize/AppInfoCard';
+import { TransactionDetails } from '@/components/authorize/TransactionDetails';
+import { BalanceWarning } from '@/components/authorize/BalanceWarning';
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
import {
SignatureAuthService,
plaocAdapter,
@@ -17,113 +17,127 @@ import {
type SignatureRequest,
type TransferPayload,
type MessagePayload,
-} from '@/services/authorize'
-import { useToast } from '@/services'
-import { walletStore, walletSelectors } from '@/stores'
+} from '@/services/authorize';
+import { useToast } from '@/services';
+import { walletStore, walletSelectors } from '@/stores';
-const REQUEST_TIMEOUT_MS = 5 * 60 * 1000
+const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
type ParsedSignatureItem =
| { type: 'message'; payload: MessagePayload }
| { type: 'transfer'; payload: TransferPayload }
- | { type: 'destory'; payload: DestroyPayload }
+ | { type: 'destory'; payload: DestroyPayload };
function isRecord(value: unknown): value is Record {
- return typeof value === 'object' && value !== null
+ return typeof value === 'object' && value !== null;
}
function readOptionalStringLike(obj: Record, key: string): string | undefined {
- const v = obj[key]
- if (typeof v === 'string') return v
- if (typeof v === 'number' && Number.isFinite(v)) return String(v)
- return undefined
+ const v = obj[key];
+ if (typeof v === 'string') return v;
+ if (typeof v === 'number' && Number.isFinite(v)) return String(v);
+ return undefined;
}
function readRequiredStringLike(
obj: Record,
key: string,
- label: string
+ label: string,
): { ok: true; value: string } | { ok: false; error: string } {
- const value = readOptionalStringLike(obj, key)?.trim()
- if (!value) return { ok: false, error: `signaturedata 缺少字段:${label}` }
- return { ok: true, value }
+ const value = readOptionalStringLike(obj, key)?.trim();
+ // i18n-ignore: Technical error message for developers
+ if (!value) return { ok: false, error: `signaturedata missing field: ${label}` };
+ return { ok: true, value };
}
function readFirstStringLike(
obj: Record,
- keys: Array<{ key: string; label: string }>
+ keys: Array<{ key: string; label: string }>,
): { ok: true; value: string } | { ok: false; error: string } {
for (const k of keys) {
- const value = readOptionalStringLike(obj, k.key)?.trim()
- if (value) return { ok: true, value }
+ const value = readOptionalStringLike(obj, k.key)?.trim();
+ if (value) return { ok: true, value };
}
- return { ok: false, error: `signaturedata 缺少字段:${keys.map((k) => k.label).join(' / ')}` }
+ // i18n-ignore: Technical error message for developers
+ return { ok: false, error: `signaturedata missing field: ${keys.map((k) => k.label).join(' / ')}` };
}
-function parseSignatureType(rawType: unknown): { ok: true; value: ParsedSignatureItem['type'] } | { ok: false; error: string } {
+function parseSignatureType(
+ rawType: unknown,
+): { ok: true; value: ParsedSignatureItem['type'] } | { ok: false; error: string } {
if (typeof rawType === 'string') {
- const v = rawType.trim().toLowerCase()
- if (v === 'message') return { ok: true, value: 'message' }
- if (v === 'transfer') return { ok: true, value: 'transfer' }
- if (v === 'destroy' || v === 'destory') return { ok: true, value: 'destory' }
- return { ok: false, error: `不支持的签名类型:${rawType}` }
+ const v = rawType.trim().toLowerCase();
+ if (v === 'message') return { ok: true, value: 'message' };
+ if (v === 'transfer') return { ok: true, value: 'transfer' };
+ if (v === 'destroy' || v === 'destory') return { ok: true, value: 'destory' };
+ // i18n-ignore: Technical error message for developers
+ return { ok: false, error: `Unsupported signature type: ${rawType}` };
}
if (typeof rawType === 'number' && Number.isInteger(rawType)) {
// mpay legacy enum: message=0, transfer=1, ..., destory=7
- if (rawType === 0) return { ok: true, value: 'message' }
- if (rawType === 1) return { ok: true, value: 'transfer' }
- if (rawType === 7) return { ok: true, value: 'destory' }
- return { ok: false, error: `不支持的签名类型:${rawType}` }
+ if (rawType === 0) return { ok: true, value: 'message' };
+ if (rawType === 1) return { ok: true, value: 'transfer' };
+ if (rawType === 7) return { ok: true, value: 'destory' };
+ // i18n-ignore: Technical error message for developers
+ return { ok: false, error: `Unsupported signature type: ${rawType}` };
}
- return { ok: false, error: 'signaturedata.type 必须是字符串或数字' }
+ // i18n-ignore: Technical error message for developers
+ return { ok: false, error: 'signaturedata.type must be string or number' };
}
-function parseSignaturedataParam(signaturedata: string | undefined): { ok: true; item: ParsedSignatureItem } | { ok: false; error: string } {
+function parseSignaturedataParam(
+ signaturedata: string | undefined,
+): { ok: true; item: ParsedSignatureItem } | { ok: false; error: string } {
if (signaturedata === undefined || signaturedata.trim() === '') {
- return { ok: false, error: '缺少 signaturedata 参数' }
+ // i18n-ignore: Technical error message for developers
+ return { ok: false, error: 'Missing signaturedata parameter' };
}
- let parsed: unknown
+ let parsed: unknown;
try {
- parsed = JSON.parse(signaturedata)
+ parsed = JSON.parse(signaturedata);
} catch {
- return { ok: false, error: 'signaturedata 不是合法的 JSON' }
+ // i18n-ignore: Technical error message for developers
+ return { ok: false, error: 'signaturedata is not valid JSON' };
}
if (!Array.isArray(parsed)) {
- return { ok: false, error: 'signaturedata 必须是 JSON 数组' }
+ // i18n-ignore: Technical error message for developers
+ return { ok: false, error: 'signaturedata must be a JSON array' };
}
if (parsed.length === 0) {
- return { ok: false, error: 'signaturedata 不能为空数组' }
+ // i18n-ignore: Technical error message for developers
+ return { ok: false, error: 'signaturedata cannot be an empty array' };
}
- const first = parsed[0]
+ const first = parsed[0];
if (!isRecord(first)) {
- return { ok: false, error: 'signaturedata[0] 必须是对象' }
+ // i18n-ignore: Technical error message for developers
+ return { ok: false, error: 'signaturedata[0] must be an object' };
}
- const typeRes = parseSignatureType(first.type)
- if (!typeRes.ok) return typeRes
+ const typeRes = parseSignatureType(first.type);
+ if (!typeRes.ok) return typeRes;
const chainNameRes = readFirstStringLike(first, [
{ key: 'chainName', label: 'chainName' },
{ key: 'chain', label: 'chain' },
- ])
- if (!chainNameRes.ok) return chainNameRes
- const chainName = chainNameRes.value
+ ]);
+ if (!chainNameRes.ok) return chainNameRes;
+ const chainName = chainNameRes.value;
if (typeRes.value === 'message') {
const senderRes = readFirstStringLike(first, [
{ key: 'senderAddress', label: 'senderAddress' },
{ key: 'from', label: 'from' },
- ])
- if (!senderRes.ok) return senderRes
+ ]);
+ if (!senderRes.ok) return senderRes;
- const messageRes = readRequiredStringLike(first, 'message', 'message')
- if (!messageRes.ok) return messageRes
+ const messageRes = readRequiredStringLike(first, 'message', 'message');
+ if (!messageRes.ok) return messageRes;
return {
ok: true,
@@ -135,50 +149,50 @@ function parseSignaturedataParam(signaturedata: string | undefined): { ok: true;
message: messageRes.value,
},
},
- }
+ };
}
if (typeRes.value === 'transfer') {
const senderRes = readFirstStringLike(first, [
{ key: 'senderAddress', label: 'senderAddress' },
{ key: 'from', label: 'from' },
- ])
- if (!senderRes.ok) return senderRes
+ ]);
+ if (!senderRes.ok) return senderRes;
const receiveRes = readFirstStringLike(first, [
{ key: 'receiveAddress', label: 'receiveAddress' },
{ key: 'to', label: 'to' },
- ])
- if (!receiveRes.ok) return receiveRes
+ ]);
+ if (!receiveRes.ok) return receiveRes;
const amountRes = readFirstStringLike(first, [
{ key: 'balance', label: 'balance' },
{ key: 'amount', label: 'amount' },
- ])
- if (!amountRes.ok) return amountRes
+ ]);
+ if (!amountRes.ok) return amountRes;
- const fee = readOptionalStringLike(first, 'fee')?.trim()
- const assetType = readOptionalStringLike(first, 'assetType')?.trim()
+ const fee = readOptionalStringLike(first, 'fee')?.trim();
+ const assetType = readOptionalStringLike(first, 'assetType')?.trim();
- let contractInfo: TransferPayload['contractInfo'] | undefined
- const rawContractInfo = first.contractInfo
+ let contractInfo: TransferPayload['contractInfo'] | undefined;
+ const rawContractInfo = first.contractInfo;
if (isRecord(rawContractInfo)) {
- const ciAssetType = readOptionalStringLike(rawContractInfo, 'assetType')?.trim()
- const ciDecimalsRaw = rawContractInfo.decimals
- const ciContractAddress = readOptionalStringLike(rawContractInfo, 'contractAddress')?.trim()
+ const ciAssetType = readOptionalStringLike(rawContractInfo, 'assetType')?.trim();
+ const ciDecimalsRaw = rawContractInfo.decimals;
+ const ciContractAddress = readOptionalStringLike(rawContractInfo, 'contractAddress')?.trim();
const ciDecimals =
typeof ciDecimalsRaw === 'number' && Number.isFinite(ciDecimalsRaw)
? ciDecimalsRaw
: typeof ciDecimalsRaw === 'string' && ciDecimalsRaw.trim() !== ''
? Number(ciDecimalsRaw)
- : undefined
+ : undefined;
if (ciAssetType && ciContractAddress && typeof ciDecimals === 'number' && Number.isFinite(ciDecimals)) {
contractInfo = {
assetType: ciAssetType,
contractAddress: ciContractAddress,
decimals: ciDecimals,
- }
+ };
}
}
@@ -196,29 +210,29 @@ function parseSignaturedataParam(signaturedata: string | undefined): { ok: true;
...(contractInfo ? { contractInfo } : {}),
},
},
- }
+ };
}
{
const senderRes = readFirstStringLike(first, [
{ key: 'senderAddress', label: 'senderAddress' },
{ key: 'from', label: 'from' },
- ])
- if (!senderRes.ok) return senderRes
+ ]);
+ if (!senderRes.ok) return senderRes;
const amountRes = readFirstStringLike(first, [
{ key: 'destoryAmount', label: 'destoryAmount' },
{ key: 'amount', label: 'amount' },
- ])
- if (!amountRes.ok) return amountRes
+ ]);
+ if (!amountRes.ok) return amountRes;
const destoryAddress =
readOptionalStringLike(first, 'destoryAddress')?.trim() ??
readOptionalStringLike(first, 'to')?.trim() ??
- senderRes.value
+ senderRes.value;
- const fee = readOptionalStringLike(first, 'fee')?.trim()
- const assetType = readOptionalStringLike(first, 'assetType')?.trim()
+ const fee = readOptionalStringLike(first, 'fee')?.trim();
+ const assetType = readOptionalStringLike(first, 'assetType')?.trim();
return {
ok: true,
@@ -233,7 +247,7 @@ function parseSignaturedataParam(signaturedata: string | undefined): { ok: true;
...(assetType ? { assetType } : {}),
},
},
- }
+ };
}
}
@@ -247,7 +261,7 @@ function isTransferPayload(payload: unknown): payload is TransferPayload {
'senderAddress' in payload &&
'receiveAddress' in payload &&
'balance' in payload
- )
+ );
}
/**
@@ -260,7 +274,7 @@ function isMessagePayload(payload: unknown): payload is MessagePayload {
'senderAddress' in payload &&
'message' in payload &&
!('receiveAddress' in payload)
- )
+ );
}
/**
@@ -273,15 +287,19 @@ function isDestroyPayload(payload: unknown): payload is DestroyPayload {
'senderAddress' in payload &&
'destoryAddress' in payload &&
'destoryAmount' in payload
- )
+ );
}
/**
* Format token symbol from asset type or chain name
*/
-function getTokenSymbol(payload: { contractInfo?: { assetType: string }; assetType?: string; chainName?: string }): string {
- if (payload.contractInfo?.assetType) return payload.contractInfo.assetType.toUpperCase()
- if (payload.assetType) return payload.assetType.toUpperCase()
+function getTokenSymbol(payload: {
+ contractInfo?: { assetType: string };
+ assetType?: string;
+ chainName?: string;
+}): string {
+ if (payload.contractInfo?.assetType) return payload.contractInfo.assetType.toUpperCase();
+ if (payload.assetType) return payload.assetType.toUpperCase();
// Default to chain native token (best-effort).
const chainSymbols: Record = {
@@ -290,8 +308,8 @@ function getTokenSymbol(payload: { contractInfo?: { assetType: string }; assetTy
tron: 'TRX',
binance: 'BNB',
bsc: 'BNB',
- }
- return chainSymbols[payload.chainName?.toLowerCase() ?? ''] ?? 'TOKEN'
+ };
+ return chainSymbols[payload.chainName?.toLowerCase() ?? ''] ?? 'TOKEN';
}
/**
@@ -300,55 +318,55 @@ function getTokenSymbol(payload: { contractInfo?: { assetType: string }; assetTy
function checkBalance(
walletBalance: string,
amount: string,
- fee: string | undefined
+ fee: string | undefined,
): { sufficient: boolean; required: string } {
- const balanceNum = parseFloat(walletBalance) || 0
- const amountNum = parseFloat(amount) || 0
- const feeNum = parseFloat(fee ?? '0') || 0
- const required = amountNum + feeNum
+ const balanceNum = parseFloat(walletBalance) || 0;
+ const amountNum = parseFloat(amount) || 0;
+ const feeNum = parseFloat(fee ?? '0') || 0;
+ const required = amountNum + feeNum;
return {
sufficient: balanceNum >= required,
required: required.toString(),
- }
+ };
}
export function SignatureAuthPage() {
- const { t: tAuthorize } = useTranslation('authorize')
- const { t: tCommon } = useTranslation('common')
- const { navigate, goBack } = useNavigation()
- const { push } = useFlow()
- const toast = useToast()
+ const { t: tAuthorize } = useTranslation('authorize');
+ const { t: tCommon } = useTranslation('common');
+ const { navigate, goBack } = useNavigation();
+ const { push } = useFlow();
+ const toast = useToast();
const { id: eventId, signaturedata } = useActivityParams<{
- id: string
- signaturedata?: string
- }>()
+ id: string;
+ signaturedata?: string;
+ }>();
- const currentWallet = useStore(walletStore, walletSelectors.getCurrentWallet)
+ const currentWallet = useStore(walletStore, walletSelectors.getCurrentWallet);
- const [appInfo, setAppInfo] = useState(null)
- const [signatureRequest, setSignatureRequest] = useState(null)
- const [loadError, setLoadError] = useState(null)
- const [isSubmitting, setIsSubmitting] = useState(false)
+ const [appInfo, setAppInfo] = useState(null);
+ const [signatureRequest, setSignatureRequest] = useState(null);
+ const [loadError, setLoadError] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
- const authService = useMemo(() => new SignatureAuthService(plaocAdapter, eventId), [eventId])
+ const authService = useMemo(() => new SignatureAuthService(plaocAdapter, eventId), [eventId]);
// Load app info and signature request
useEffect(() => {
- let cancelled = false
+ let cancelled = false;
async function run() {
- setLoadError(null)
+ setLoadError(null);
try {
- const parsed = parseSignaturedataParam(signaturedata)
+ const parsed = parseSignaturedataParam(signaturedata);
if (!parsed.ok) {
- setLoadError(parsed.error)
- return
+ setLoadError(parsed.error);
+ return;
}
- const info = await plaocAdapter.getCallerAppInfo(eventId)
- if (cancelled) return
- setAppInfo(info)
+ const info = await plaocAdapter.getCallerAppInfo(eventId);
+ if (cancelled) return;
+ setAppInfo(info);
const req: SignatureRequest = {
eventId,
@@ -357,156 +375,167 @@ export function SignatureAuthPage() {
appName: info.appName,
appHome: info.origin,
appLogo: info.appIcon,
- }
+ };
- setSignatureRequest(req)
+ setSignatureRequest(req);
} catch {
- if (cancelled) return
- setLoadError(tAuthorize('error.authFailed'))
+ if (cancelled) return;
+ setLoadError(tAuthorize('error.authFailed'));
}
}
- run()
+ run();
return () => {
- cancelled = true
- }
- }, [eventId, signaturedata, tAuthorize])
+ cancelled = true;
+ };
+ }, [eventId, signaturedata, tAuthorize]);
// Timeout handler
useEffect(() => {
const timer = window.setTimeout(() => {
void (async () => {
- await authService.reject('timeout')
- toast.show({ message: tAuthorize('error.timeout'), position: 'center' })
- navigate({ to: '/' })
- })()
- }, REQUEST_TIMEOUT_MS)
+ await authService.reject('timeout');
+ toast.show({ message: tAuthorize('error.timeout'), position: 'center' });
+ navigate({ to: '/' });
+ })();
+ }, REQUEST_TIMEOUT_MS);
return () => {
- window.clearTimeout(timer)
- }
- }, [authService, navigate, tAuthorize, toast])
+ window.clearTimeout(timer);
+ };
+ }, [authService, navigate, tAuthorize, toast]);
// Derived state for transfer payload
const transferPayload = useMemo(() => {
if (!signatureRequest || !isTransferPayload(signatureRequest.payload)) {
- return null
+ return null;
}
- return signatureRequest.payload
- }, [signatureRequest])
+ return signatureRequest.payload;
+ }, [signatureRequest]);
// Derived state for message payload
const messagePayload = useMemo(() => {
if (!signatureRequest || !isMessagePayload(signatureRequest.payload)) {
- return null
+ return null;
}
- return signatureRequest.payload
- }, [signatureRequest])
+ return signatureRequest.payload;
+ }, [signatureRequest]);
// Derived state for destroy payload
const destroyPayload = useMemo(() => {
if (!signatureRequest || !isDestroyPayload(signatureRequest.payload)) {
- return null
+ return null;
}
- return signatureRequest.payload
- }, [signatureRequest])
+ return signatureRequest.payload;
+ }, [signatureRequest]);
// Check balance for transfer
const balanceCheck = useMemo(() => {
- if (!currentWallet) return { sufficient: true, required: '0', walletBalance: '0' }
+ if (!currentWallet) return { sufficient: true, required: '0', walletBalance: '0' };
- const spend =
- transferPayload
- ? { chainName: transferPayload.chainName, amount: transferPayload.balance, fee: transferPayload.fee }
- : destroyPayload
- ? { chainName: destroyPayload.chainName, amount: destroyPayload.destoryAmount, fee: destroyPayload.fee }
- : null
+ const spend = transferPayload
+ ? { chainName: transferPayload.chainName, amount: transferPayload.balance, fee: transferPayload.fee }
+ : destroyPayload
+ ? { chainName: destroyPayload.chainName, amount: destroyPayload.destoryAmount, fee: destroyPayload.fee }
+ : null;
- if (!spend) return { sufficient: true, required: '0', walletBalance: '0' }
+ if (!spend) return { sufficient: true, required: '0', walletBalance: '0' };
const chainAddress = currentWallet.chainAddresses.find(
- (ca) => ca.chain.toLowerCase() === spend.chainName?.toLowerCase()
- )
+ (ca) => ca.chain.toLowerCase() === spend.chainName?.toLowerCase(),
+ );
// For now, use a mock balance - in real implementation, this would come from the wallet
- const walletBalance = chainAddress ? '1.0' : '0'
- const result = checkBalance(walletBalance, spend.amount, spend.fee)
- return { ...result, walletBalance }
- }, [currentWallet, destroyPayload, transferPayload])
+ const walletBalance = chainAddress ? '1.0' : '0';
+ const result = checkBalance(walletBalance, spend.amount, spend.fee);
+ return { ...result, walletBalance };
+ }, [currentWallet, destroyPayload, transferPayload]);
const tokenSymbol = useMemo(() => {
- if (transferPayload) return getTokenSymbol(transferPayload)
- if (destroyPayload) return getTokenSymbol(destroyPayload)
- return 'TOKEN'
- }, [destroyPayload, transferPayload])
+ if (transferPayload) return getTokenSymbol(transferPayload);
+ if (destroyPayload) return getTokenSymbol(destroyPayload);
+ return 'TOKEN';
+ }, [destroyPayload, transferPayload]);
const handleBack = useCallback(() => {
- goBack()
- }, [goBack])
+ goBack();
+ }, [goBack]);
const handleReject = useCallback(async () => {
- if (isSubmitting) return
- setIsSubmitting(true)
+ if (isSubmitting) return;
+ setIsSubmitting(true);
try {
- await authService.reject(balanceCheck.sufficient ? 'rejected' : 'insufficient_balance')
- navigate({ to: '/' })
+ await authService.reject(balanceCheck.sufficient ? 'rejected' : 'insufficient_balance');
+ navigate({ to: '/' });
} finally {
- setIsSubmitting(false)
+ setIsSubmitting(false);
}
- }, [authService, balanceCheck.sufficient, isSubmitting, navigate])
+ }, [authService, balanceCheck.sufficient, isSubmitting, navigate]);
const handleSign = useCallback(() => {
- if (!balanceCheck.sufficient) return
- if (isSubmitting) return
+ if (!balanceCheck.sufficient) return;
+ if (isSubmitting) return;
setWalletLockConfirmCallback(async (password: string) => {
- setIsSubmitting(true)
+ setIsSubmitting(true);
try {
- const encryptedSecret = currentWallet?.encryptedMnemonic
- if (!encryptedSecret) return false
- if (!signatureRequest) return false
+ const encryptedSecret = currentWallet?.encryptedMnemonic;
+ if (!encryptedSecret) return false;
+ if (!signatureRequest) return false;
- let signature: string = ''
+ let signature: string = '';
if (signatureRequest.type === 'message') {
- if (!messagePayload) return false
- const result = await authService.handleMessageSign(messagePayload, encryptedSecret, password)
- signature = typeof result === 'string' ? result : (result as { signature?: string }).signature ?? ''
+ if (!messagePayload) return false;
+ const result = await authService.handleMessageSign(messagePayload, encryptedSecret, password);
+ signature = typeof result === 'string' ? result : ((result as { signature?: string }).signature ?? '');
} else if (signatureRequest.type === 'transfer') {
- if (!transferPayload) return false
- const result = await authService.handleTransferSign(transferPayload, encryptedSecret, password)
- signature = typeof result === 'string' ? result : (result as { signature?: string }).signature ?? ''
+ if (!transferPayload) return false;
+ const result = await authService.handleTransferSign(transferPayload, encryptedSecret, password);
+ signature = typeof result === 'string' ? result : ((result as { signature?: string }).signature ?? '');
} else if (signatureRequest.type === 'destory') {
- if (!destroyPayload) return false
- const result = await authService.handleDestroySign(destroyPayload, encryptedSecret, password)
- signature = typeof result === 'string' ? result : (result as { signature?: string }).signature ?? ''
+ if (!destroyPayload) return false;
+ const result = await authService.handleDestroySign(destroyPayload, encryptedSecret, password);
+ signature = typeof result === 'string' ? result : ((result as { signature?: string }).signature ?? '');
} else {
- return false
+ return false;
}
- await authService.approve(signature)
- navigate({ to: '/' })
- return true
+ await authService.approve(signature);
+ navigate({ to: '/' });
+ return true;
} catch {
- return false
+ return false;
} finally {
- setIsSubmitting(false)
+ setIsSubmitting(false);
}
- })
+ });
- push("WalletLockConfirmJob", {
+ push('WalletLockConfirmJob', {
title: tAuthorize('button.confirm'),
- })
- }, [authService, balanceCheck.sufficient, currentWallet?.encryptedMnemonic, destroyPayload, isSubmitting, messagePayload, navigate, push, signatureRequest, tAuthorize, transferPayload])
+ });
+ }, [
+ authService,
+ balanceCheck.sufficient,
+ currentWallet?.encryptedMnemonic,
+ destroyPayload,
+ isSubmitting,
+ messagePayload,
+ navigate,
+ push,
+ signatureRequest,
+ tAuthorize,
+ transferPayload,
+ ]);
// Error state
if (loadError) {
return (
-
+
-
-
{loadError}
+
+
{loadError}
- )
+ );
}
// Determine page title based on request type
@@ -524,18 +553,16 @@ export function SignatureAuthPage() {
? tAuthorize('signature.type.message')
: signatureRequest?.type === 'destory'
? tAuthorize('signature.type.destroy')
- : tAuthorize('title.signature')
+ : tAuthorize('title.signature');
return (
-
+
{appInfo &&
}
-
- {tAuthorize('signature.reviewTransaction')}
-
+
{tAuthorize('signature.reviewTransaction')}
{/* Transfer type display */}
{transferPayload && (
@@ -560,15 +587,15 @@ export function SignatureAuthPage() {
{/* Message type display */}
{messagePayload && (
-
-
- {tAuthorize('signature.messageToSign')}
-
-
+
+
{tAuthorize('signature.messageToSign')}
+
{messagePayload.message}
@@ -597,8 +624,8 @@ export function SignatureAuthPage() {
{/* Signature type badge */}
{signatureRequest && (
-
-
+
+
{signatureRequest.type === 'transfer' && tAuthorize('signature.type.transfer')}
{signatureRequest.type === 'message' && tAuthorize('signature.type.message')}
{signatureRequest.type === 'destory' && tAuthorize('signature.type.destroy')}
@@ -608,15 +635,9 @@ export function SignatureAuthPage() {
{/* Action buttons */}
-
+
-
- )
+ );
}
diff --git a/src/pages/destroy/index.tsx b/src/pages/destroy/index.tsx
index 3ba5a8fbb..2e35b36a5 100644
--- a/src/pages/destroy/index.tsx
+++ b/src/pages/destroy/index.tsx
@@ -1,26 +1,26 @@
/**
* DestroyPage - 资产销毁页面
- *
+ *
* 仅支持 BioForest 链,主资产不可销毁
*/
-import { useMemo, useRef, useCallback } from 'react'
-import { useTranslation } from 'react-i18next'
-import { useNavigation, useActivityParams, useFlow } from '@/stackflow'
-import { setTransferConfirmCallback, setTransferWalletLockCallback } from '@/stackflow/activities/sheets'
-import { PageHeader } from '@/components/layout/page-header'
-import { AssetSelector } from '@/components/asset'
-import { AmountInput } from '@/components/transfer/amount-input'
-import { GradientButton, Alert } from '@/components/common'
-import { ChainIcon } from '@/components/wallet/chain-icon'
-import { SendResult } from '@/components/transfer/send-result'
-import { useToast, useHaptics } from '@/services'
-import { useBurn } from '@/hooks/use-burn'
-import { Amount } from '@/types/amount'
-import type { AssetInfo } from '@/types/asset'
-import { tokenBalancesToTokenInfoList, type TokenInfo } from '@/components/token'
-import { IconFlame } from '@tabler/icons-react'
-import { ChainProviderGate, useChainProvider } from '@/contexts'
+import { useMemo, useRef, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useNavigation, useActivityParams, useFlow } from '@/stackflow';
+import { setTransferConfirmCallback, setTransferWalletLockCallback } from '@/stackflow/activities/sheets';
+import { PageHeader } from '@/components/layout/page-header';
+import { AssetSelector } from '@/components/asset';
+import { AmountInput } from '@/components/transfer/amount-input';
+import { GradientButton, Alert } from '@/components/common';
+import { ChainIcon } from '@/components/wallet/chain-icon';
+import { SendResult } from '@/components/transfer/send-result';
+import { useToast, useHaptics } from '@/services';
+import { useBurn } from '@/hooks/use-burn';
+import { Amount } from '@/types/amount';
+import type { AssetInfo } from '@/types/asset';
+import { tokenBalancesToTokenInfoList, type TokenInfo } from '@/components/token';
+import { IconFlame } from '@tabler/icons-react';
+import { ChainProviderGate, useChainProvider } from '@/contexts';
import {
useChainConfigState,
chainConfigSelectors,
@@ -28,7 +28,7 @@ import {
useCurrentWallet,
useSelectedChain,
type ChainType,
-} from '@/stores'
+} from '@/stores';
const CHAIN_NAMES: Record
= {
ethereum: 'Ethereum',
@@ -43,7 +43,7 @@ const CHAIN_NAMES: Record = {
biwmeta: 'BIWMeta',
ethmeta: 'ETHMeta',
malibu: 'Malibu',
-}
+};
/** Convert TokenInfo to AssetInfo */
function tokenToAsset(token: TokenInfo): AssetInfo {
@@ -53,7 +53,7 @@ function tokenToAsset(token: TokenInfo): AssetInfo {
amount: Amount.fromFormatted(token.balance, token.decimals ?? 8, token.symbol),
decimals: token.decimals ?? 8,
logoUrl: token.icon,
- }
+ };
}
/** Convert AssetInfo to TokenInfo */
@@ -65,31 +65,31 @@ function assetToToken(asset: AssetInfo, chain: ChainType): TokenInfo {
decimals: asset.decimals,
chain,
icon: asset.logoUrl,
- }
+ };
}
function DestroyPageContent() {
- const { t } = useTranslation(['transaction', 'common', 'security'])
- const { goBack: navGoBack } = useNavigation()
- const { push } = useFlow()
- const toast = useToast()
- const haptics = useHaptics()
- const isWalletLockSheetOpen = useRef(false)
+ const { t } = useTranslation(['transaction', 'common', 'security']);
+ const { goBack: navGoBack } = useNavigation();
+ const { push } = useFlow();
+ const toast = useToast();
+ const haptics = useHaptics();
+ const isWalletLockSheetOpen = useRef(false);
// Read params
const { assetType: initialAssetType, assetLocked: assetLockedParam } = useActivityParams<{
- assetType?: string
- assetLocked?: string
- }>()
-
- const selectedChain = useSelectedChain()
- const currentWallet = useCurrentWallet()
- const currentChainAddress = useCurrentChainAddress()
- const chainConfigState = useChainConfigState()
+ assetType?: string;
+ assetLocked?: string;
+ }>();
+
+ const selectedChain = useSelectedChain();
+ const currentWallet = useCurrentWallet();
+ const currentChainAddress = useCurrentChainAddress();
+ const chainConfigState = useChainConfigState();
const chainConfig = chainConfigState.snapshot
? chainConfigSelectors.getChainById(chainConfigState, selectedChain)
- : null
- const selectedChainName = chainConfig?.name ?? CHAIN_NAMES[selectedChain] ?? selectedChain
+ : null;
+ const selectedChainName = chainConfig?.name ?? CHAIN_NAMES[selectedChain] ?? selectedChain;
// 使用 useChainProvider() 获取确保非空的 provider
const chainProvider = useChainProvider();
@@ -97,126 +97,118 @@ function DestroyPageContent() {
// 直接调用,不需要条件判断
const tokenBalancesState = chainProvider.tokenBalances.useState(
{ address: currentChainAddress?.address ?? '' },
- { enabled: !!currentChainAddress?.address }
+ { enabled: !!currentChainAddress?.address },
);
const tokens = useMemo(() => {
- if (!tokenBalancesState?.data) return []
- return tokenBalancesToTokenInfoList(tokenBalancesState.data, selectedChain)
- }, [tokenBalancesState?.data, selectedChain])
+ if (!tokenBalancesState?.data) return [];
+ return tokenBalancesToTokenInfoList(tokenBalancesState.data, selectedChain);
+ }, [tokenBalancesState?.data, selectedChain]);
// Filter out main asset (cannot be destroyed)
const destroyableTokens = useMemo(() => {
- if (!chainConfig) return []
- return tokens.filter((token: TokenInfo) => token.symbol.toUpperCase() !== chainConfig.symbol.toUpperCase())
- }, [tokens, chainConfig])
+ if (!chainConfig) return [];
+ return tokens.filter((token: TokenInfo) => token.symbol.toUpperCase() !== chainConfig.symbol.toUpperCase());
+ }, [tokens, chainConfig]);
// Find initial asset from params
const initialAsset = useMemo(() => {
- if (!initialAssetType || destroyableTokens.length === 0) return null
- const found = destroyableTokens.find(
- (t: TokenInfo) => t.symbol.toUpperCase() === initialAssetType.toUpperCase()
- )
- return found ? tokenToAsset(found) : null
- }, [initialAssetType, destroyableTokens])
-
- const assetLocked = assetLockedParam === 'true'
-
- const {
- state,
- setAmount,
- setAsset,
- goToConfirm,
- submit,
- submitWithTwoStepSecret,
- reset,
- canProceed
- } = useBurn({
+ if (!initialAssetType || destroyableTokens.length === 0) return null;
+ const found = destroyableTokens.find((t: TokenInfo) => t.symbol.toUpperCase() === initialAssetType.toUpperCase());
+ return found ? tokenToAsset(found) : null;
+ }, [initialAssetType, destroyableTokens]);
+
+ const assetLocked = assetLockedParam === 'true';
+
+ const { state, setAmount, setAsset, goToConfirm, submit, submitWithTwoStepSecret, reset, canProceed } = useBurn({
initialAsset: initialAsset ?? undefined,
assetLocked,
useMock: false,
walletId: currentWallet?.id,
fromAddress: currentChainAddress?.address,
chainConfig,
- })
+ });
// Handle asset selection
- const handleAssetSelect = useCallback((token: TokenInfo) => {
- setAsset(tokenToAsset(token))
- }, [setAsset])
+ const handleAssetSelect = useCallback(
+ (token: TokenInfo) => {
+ setAsset(tokenToAsset(token));
+ },
+ [setAsset],
+ );
// Selected token for AssetSelector
const selectedToken = useMemo(() => {
- if (!state.asset) return null
- return assetToToken(state.asset, selectedChain)
- }, [state.asset, selectedChain])
+ if (!state.asset) return null;
+ return assetToToken(state.asset, selectedChain);
+ }, [state.asset, selectedChain]);
// Derive formatted values for display
- const balance = state.asset?.amount ?? null
- const symbol = state.asset?.assetType ?? 'TOKEN'
+ const balance = state.asset?.amount ?? null;
+ const symbol = state.asset?.assetType ?? 'TOKEN';
const handleProceed = () => {
- if (!goToConfirm()) return
+ if (!goToConfirm()) return;
- haptics.impact('light')
+ haptics.impact('light');
// Set up callback: TransferConfirm -> TransferWalletLock
setTransferConfirmCallback(
async () => {
- if (isWalletLockSheetOpen.current) return
- isWalletLockSheetOpen.current = true
+ if (isWalletLockSheetOpen.current) return;
+ isWalletLockSheetOpen.current = true;
- await haptics.impact('medium')
+ await haptics.impact('medium');
setTransferWalletLockCallback(async (walletLockKey: string, twoStepSecret?: string) => {
if (!twoStepSecret) {
- const result = await submit(walletLockKey)
+ const result = await submit(walletLockKey);
if (result.status === 'password') {
- return { status: 'wallet_lock_invalid' as const }
+ return { status: 'wallet_lock_invalid' as const };
}
if (result.status === 'two_step_secret_required') {
- return { status: 'two_step_secret_required' as const }
+ return { status: 'two_step_secret_required' as const };
}
if (result.status === 'ok') {
- isWalletLockSheetOpen.current = false
- return { status: 'ok' as const, txHash: result.txHash, pendingTxId: result.pendingTxId }
+ isWalletLockSheetOpen.current = false;
+ return { status: 'ok' as const, txHash: result.txHash, pendingTxId: result.pendingTxId };
}
if (result.status === 'error') {
- return { status: 'error' as const, message: result.message, pendingTxId: result.pendingTxId }
+ return { status: 'error' as const, message: result.message, pendingTxId: result.pendingTxId };
}
- return { status: 'error' as const, message: '销毁失败' }
+ return { status: 'error' as const, message: t('error:destroy.failed') };
}
- const result = await submitWithTwoStepSecret(walletLockKey, twoStepSecret)
+ const result = await submitWithTwoStepSecret(walletLockKey, twoStepSecret);
if (result.status === 'ok') {
- isWalletLockSheetOpen.current = false
- return { status: 'ok' as const, txHash: result.txHash, pendingTxId: result.pendingTxId }
+ isWalletLockSheetOpen.current = false;
+ return { status: 'ok' as const, txHash: result.txHash, pendingTxId: result.pendingTxId };
}
if (result.status === 'password') {
- return { status: 'two_step_secret_invalid' as const, message: '安全密码错误' }
+ return { status: 'two_step_secret_invalid' as const, message: t('error:security.passwordInvalid') };
}
if (result.status === 'error') {
- return { status: 'error' as const, message: result.message, pendingTxId: result.pendingTxId }
+ return { status: 'error' as const, message: result.message, pendingTxId: result.pendingTxId };
}
- return { status: 'error' as const, message: '未知错误' }
- })
+ return { status: 'error' as const, message: t('error:unknown') };
+ });
push('TransferWalletLockJob', {
title: t('security:walletLock.verifyTitle'),
- })
+ });
},
{
minFee: state.feeMinAmount?.toFormatted() ?? state.feeAmount?.toFormatted() ?? '0',
- }
- )
+ },
+ );
push('TransferConfirmJob', {
amount: state.amount?.toFormatted() ?? '0',
@@ -225,64 +217,62 @@ function DestroyPageContent() {
feeAmount: state.feeAmount?.toFormatted() ?? '0',
feeSymbol: state.feeSymbol,
feeLoading: state.feeLoading ? 'true' : 'false',
- })
- }
+ });
+ };
const handleDone = () => {
if (state.resultStatus === 'success') {
- haptics.impact('success')
+ haptics.impact('success');
}
- navGoBack()
- }
+ navGoBack();
+ };
const handleRetry = () => {
- reset()
- }
+ reset();
+ };
const handleViewExplorer = useCallback(() => {
- if (!state.txHash) return
- const queryTx = chainConfig?.explorer?.queryTx
+ if (!state.txHash) return;
+ const queryTx = chainConfig?.explorer?.queryTx;
if (!queryTx) {
- toast.show(t('sendPage.explorerNotImplemented'))
- return
+ toast.show(t('sendPage.explorerNotImplemented'));
+ return;
}
- const url = queryTx.replace(':hash', state.txHash).replace(':signature', state.txHash)
- window.open(url, '_blank', 'noopener,noreferrer')
- }, [state.txHash, chainConfig?.explorer?.queryTx, toast, t])
+ const url = queryTx.replace(':hash', state.txHash).replace(':signature', state.txHash);
+ window.open(url, '_blank', 'noopener,noreferrer');
+ }, [state.txHash, chainConfig?.explorer?.queryTx, toast, t]);
// Check if chain supports destroy
- const isBioforestChain = chainConfig?.chainKind === 'bioforest'
+ const isBioforestChain = chainConfig?.chainKind === 'bioforest';
if (!currentWallet || !currentChainAddress) {
return (
- )
+ );
}
if (!isBioforestChain) {
return (
-
+
-
-
- {t('destroyPage.notSupported', '当前链不支持资产销毁')}
-
+
+
{t('destroyPage.notSupported')}
- )
+ );
}
// Result step
if (state.step === 'result' || state.step === 'burning') {
return (
- )
+ );
}
return (
-
+
{/* Current chain info & sender address */}
-
+
{selectedChainName}
{currentChainAddress?.address && (
-
+
{t('sendPage.from')}:
{currentChainAddress.address.slice(0, 8)}...{currentChainAddress.address.slice(-6)}
@@ -321,9 +313,7 @@ function DestroyPageContent() {
{/* Asset selector */}
-
+
{destroyableTokens.length === 0 && (
-
- {t('destroyPage.noDestroyableAssets', '暂无可销毁的资产')}
-
+
{t('destroyPage.noDestroyableAssets')}
)}
{/* Amount input */}
{state.asset && (
- {t('destroyPage.warning', '销毁操作不可撤销,请仔细核对销毁数量。')}
-
+ {t('destroyPage.warning')}
{/* Fee info */}
{state.feeAmount && (
- {t('sendPage.fee', '手续费')}
+ {t('sendPage.fee')}
{state.feeAmount.toFormatted()} {state.feeSymbol}
@@ -377,28 +363,28 @@ function DestroyPageContent() {
disabled={!canProceed}
onClick={handleProceed}
>
-
- {t('destroyPage.confirm', '确认销毁')}
+
+ {t('destroyPage.confirm')}
- )
+ );
}
// ==================== 主组件:使用 ChainProviderGate 包裹 ====================
export function DestroyPage() {
- const { goBack } = useNavigation()
- const selectedChain = useSelectedChain()
- const { t } = useTranslation(['transaction', 'common'])
+ const { goBack } = useNavigation();
+ const selectedChain = useSelectedChain();
+ const { t } = useTranslation(['transaction', 'common']);
return (
-
+
@@ -407,7 +393,7 @@ export function DestroyPage() {
>
- )
+ );
}
-export default DestroyPage
+export default DestroyPage;
diff --git a/src/pages/guide/WelcomeScreen.tsx b/src/pages/guide/WelcomeScreen.tsx
index 99a08bf4d..577008eb0 100644
--- a/src/pages/guide/WelcomeScreen.tsx
+++ b/src/pages/guide/WelcomeScreen.tsx
@@ -38,23 +38,26 @@ export function WelcomeScreen() {
const [currentSlide, setCurrentSlide] = useState(0);
const migration = useMigrationOptional();
- const slides: WelcomeSlide[] = useMemo(() => [
- {
- icon:
,
- title: t('welcome.slides.transfer.title'),
- description: t('welcome.slides.transfer.description'),
- },
- {
- icon:
,
- title: t('welcome.slides.multichain.title'),
- description: t('welcome.slides.multichain.description'),
- },
- {
- icon:
,
- title: t('welcome.slides.security.title'),
- description: t('welcome.slides.security.description'),
- },
- ], [t]);
+ const slides: WelcomeSlide[] = useMemo(
+ () => [
+ {
+ icon:
,
+ title: t('welcome.slides.transfer.title'),
+ description: t('welcome.slides.transfer.description'),
+ },
+ {
+ icon:
,
+ title: t('welcome.slides.multichain.title'),
+ description: t('welcome.slides.multichain.description'),
+ },
+ {
+ icon:
,
+ title: t('welcome.slides.security.title'),
+ description: t('welcome.slides.security.description'),
+ },
+ ],
+ [t],
+ );
const currentSlideData = slides[currentSlide];
if (!currentSlideData) return null;
@@ -128,9 +131,15 @@ export function WelcomeScreen() {
{/* Migration button - shown when mpay data detected */}
{shouldShowMigration && (
-
+
- {t('welcome.migrateFromMpay', { defaultValue: '从 mpay 迁移钱包' })}
+ {t('welcome.migrateFromMpay')}
)}
{t('welcome.getStarted')}
-
+
{t('welcome.haveWallet')}
diff --git a/src/pages/onboarding/migrate.tsx b/src/pages/onboarding/migrate.tsx
index 38338ac62..9b330f432 100644
--- a/src/pages/onboarding/migrate.tsx
+++ b/src/pages/onboarding/migrate.tsx
@@ -7,7 +7,11 @@
import { useState, useCallback } from 'react';
import { useNavigation } from '@/stackflow';
import { useTranslation } from 'react-i18next';
-import { IconArrowLeft as ArrowLeft, IconDownload as Download, IconAlertCircle as AlertCircle } from '@tabler/icons-react';
+import {
+ IconArrowLeft as ArrowLeft,
+ IconDownload as Download,
+ IconAlertCircle as AlertCircle,
+} from '@tabler/icons-react';
import { Button } from '@/components/ui/button';
import { useMigration } from '@/contexts/MigrationContext';
import { PatternLock, patternToString } from '@/components/security/pattern-lock';
@@ -139,17 +143,15 @@ export function MigrationPage() {
-
{t('title', { defaultValue: '钱包迁移' })}
+
{t('title')}
-
- {t('noDataFound', { defaultValue: '未检测到 mpay 钱包数据' })}
-
-
{t('goBack', { defaultValue: '返回' })}
+
{t('noDataFound')}
+
{t('goBack')}
);
@@ -164,17 +166,14 @@ export function MigrationPage() {
-
{t('detected.title', { defaultValue: '检测到 mpay 钱包' })}
+
{t('detected.title')}
- {t('detected.description', {
- defaultValue: '发现 {{count}} 个钱包可以迁移到 KeyApp',
- count: detection?.walletCount ?? 0,
- })}
+ {t('detected.description', { count: detection?.walletCount ?? 0 })}
- {t('startMigration', { defaultValue: '开始迁移' })}
+ {t('startMigration')}
- {t('skipMigration', { defaultValue: '跳过,创建新钱包' })}
+ {t('skipMigration')}
@@ -192,10 +191,8 @@ export function MigrationPage() {
case 'pattern':
return (
-
{t('pattern.title', { defaultValue: '验证钱包锁' })}
-
- {t('pattern.description', { defaultValue: '请绘制钱包锁图案以继续迁移' })}
-
+
{t('pattern.title')}
+
{t('pattern.description')}
- {t('skipMigration', { defaultValue: '跳过,创建新钱包' })}
+ {t('skipMigration')}
);
@@ -244,7 +241,7 @@ export function MigrationPage() {
-
{t('title', { defaultValue: '钱包迁移' })}
+
{t('title')}
)}
diff --git a/src/pages/send/index.tsx b/src/pages/send/index.tsx
index 3d26d5e65..cf673679c 100644
--- a/src/pages/send/index.tsx
+++ b/src/pages/send/index.tsx
@@ -1,7 +1,11 @@
import { useEffect, useMemo, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigation, useActivityParams, useFlow } from '@/stackflow';
-import { setTransferConfirmCallback, setTransferWalletLockCallback, setScannerResultCallback } from '@/stackflow/activities/sheets';
+import {
+ setTransferConfirmCallback,
+ setTransferWalletLockCallback,
+ setScannerResultCallback,
+} from '@/stackflow/activities/sheets';
import type { Contact, ContactAddress } from '@/stores';
import { addressBookStore, addressBookSelectors, preferencesActions } from '@/stores';
import { PageHeader } from '@/components/layout/page-header';
@@ -51,7 +55,12 @@ function SendPageContent() {
const isWalletLockSheetOpen = useRef(false);
// Read params for pre-fill from scanner
- const { address: initialAddress, amount: initialAmount, assetType: initialAssetType, assetLocked: assetLockedParam } = useActivityParams<{
+ const {
+ address: initialAddress,
+ amount: initialAmount,
+ assetType: initialAssetType,
+ assetLocked: assetLockedParam,
+ } = useActivityParams<{
address?: string;
amount?: string;
assetType?: string;
@@ -75,7 +84,7 @@ function SendPageContent() {
// 直接调用,不需要条件判断
const tokenBalancesState = chainProvider.tokenBalances.useState(
{ address: currentChainAddress?.address ?? '' },
- { enabled: !!currentChainAddress?.address }
+ { enabled: !!currentChainAddress?.address },
);
const tokens = useMemo(() => {
if (!tokenBalancesState?.data) return [];
@@ -114,21 +123,25 @@ function SendPageContent() {
}, [chainConfig, tokens, initialAssetType]);
// getBalance callback - single source of truth from tokens
- const getBalance = useCallback((assetType: string): Amount | null => {
- const token = tokens.find(t => t.symbol === assetType);
- if (!token) return null;
- return Amount.fromFormatted(token.balance, token.decimals ?? chainConfig?.decimals ?? 8, token.symbol);
- }, [tokens, chainConfig?.decimals]);
+ const getBalance = useCallback(
+ (assetType: string): Amount | null => {
+ const token = tokens.find((t) => t.symbol === assetType);
+ if (!token) return null;
+ return Amount.fromFormatted(token.balance, token.decimals ?? chainConfig?.decimals ?? 8, token.symbol);
+ },
+ [tokens, chainConfig?.decimals],
+ );
// useSend hook with getBalance for real-time balance validation
- const { state, setToAddress, setAmount, setAsset, setFee, goToConfirm, submit, submitWithTwoStepSecret, canProceed } = useSend({
- initialAsset: initialAsset ?? undefined,
- useMock: false,
- walletId: currentWallet?.id,
- fromAddress: currentChainAddress?.address,
- chainConfig,
- getBalance,
- });
+ const { state, setToAddress, setAmount, setAsset, setFee, goToConfirm, submit, submitWithTwoStepSecret, canProceed } =
+ useSend({
+ initialAsset: initialAsset ?? undefined,
+ useMock: false,
+ walletId: currentWallet?.id,
+ fromAddress: currentChainAddress?.address,
+ chainConfig,
+ getBalance,
+ });
// Selected token for AssetSelector (convert from state.asset)
const selectedToken = useMemo((): TokenInfo | null => {
@@ -146,16 +159,19 @@ function SendPageContent() {
}, [state.asset, selectedChain, getBalance]);
// Handle asset selection from AssetSelector
- const handleAssetSelect = useCallback((token: TokenInfo) => {
- const asset = {
- assetType: token.symbol,
- name: token.name,
- amount: Amount.fromFormatted(token.balance, token.decimals ?? chainConfig?.decimals ?? 8, token.symbol),
- decimals: token.decimals ?? chainConfig?.decimals ?? 8,
- logoUrl: token.icon,
- };
- setAsset(asset);
- }, [chainConfig?.decimals, setAsset]);
+ const handleAssetSelect = useCallback(
+ (token: TokenInfo) => {
+ const asset = {
+ assetType: token.symbol,
+ name: token.name,
+ amount: Amount.fromFormatted(token.balance, token.decimals ?? chainConfig?.decimals ?? 8, token.symbol),
+ decimals: token.decimals ?? chainConfig?.decimals ?? 8,
+ logoUrl: token.icon,
+ };
+ setAsset(asset);
+ },
+ [chainConfig?.decimals, setAsset],
+ );
// Sync initialAsset to useSend state only once (when first loading)
const hasInitializedAsset = useRef(false);
@@ -204,13 +220,20 @@ function SendPageContent() {
}, [push, selectedChain]);
// Derive formatted values for display - get balance from tokens (single source of truth)
- const currentToken = useMemo(() =>
- state.asset ? tokens.find(t => t.symbol === state.asset?.assetType) : null,
- [state.asset, tokens]
+ const currentToken = useMemo(
+ () => (state.asset ? tokens.find((t) => t.symbol === state.asset?.assetType) : null),
+ [state.asset, tokens],
);
- const balance = useMemo(() =>
- currentToken ? Amount.fromFormatted(currentToken.balance, currentToken.decimals ?? state.asset?.decimals ?? 8, currentToken.symbol) : null,
- [currentToken, state.asset?.decimals]
+ const balance = useMemo(
+ () =>
+ currentToken
+ ? Amount.fromFormatted(
+ currentToken.balance,
+ currentToken.decimals ?? state.asset?.decimals ?? 8,
+ currentToken.symbol,
+ )
+ : null,
+ [currentToken, state.asset?.decimals],
);
const symbol = state.asset?.assetType ?? 'TOKEN';
@@ -271,7 +294,7 @@ function SendPageContent() {
return { status: 'error' as const, message: result.message, pendingTxId: result.pendingTxId };
}
- return { status: 'error' as const, message: '转账失败' };
+ return { status: 'error' as const, message: t('error:transaction.transferFailed') };
}
// 第二次调用:有钱包锁和二次签名
@@ -283,14 +306,17 @@ function SendPageContent() {
}
if (result.status === 'password') {
- return { status: 'two_step_secret_invalid' as const, message: '安全密码错误' };
+ return {
+ status: 'two_step_secret_invalid' as const,
+ message: t('error:transaction.securityPasswordWrong'),
+ };
}
if (result.status === 'error') {
return { status: 'error' as const, message: result.message, pendingTxId: result.pendingTxId };
}
- return { status: 'error' as const, message: '未知错误' };
+ return { status: 'error' as const, message: t('error:transaction.unknownError') };
});
push('TransferWalletLockJob', {
@@ -300,7 +326,7 @@ function SendPageContent() {
{
minFee: state.feeMinAmount?.toFormatted() ?? state.feeAmount?.toFormatted() ?? '0',
onFeeChange: setFee,
- }
+ },
);
push('TransferConfirmJob', {
@@ -333,13 +359,13 @@ function SendPageContent() {
{/* Current chain info & sender address */}
-
+
{selectedChainName}
{currentChainAddress?.address && (
-
+
{t('sendPage.from')}:
{currentChainAddress.address.slice(0, 8)}...{currentChainAddress.address.slice(-6)}
@@ -351,9 +377,7 @@ function SendPageContent() {
{/* Asset selector (only show if multiple tokens available) */}
{tokens.length > 1 && (
-
+
-
+
{t('sendPage.continue')}
diff --git a/src/pages/settings/index.tsx b/src/pages/settings/index.tsx
index e2a727f90..1b1c179c9 100644
--- a/src/pages/settings/index.tsx
+++ b/src/pages/settings/index.tsx
@@ -20,7 +20,15 @@ import {
IconIdBadge2,
} from '@tabler/icons-react';
import { PageHeader } from '@/components/layout/page-header';
-import { useCurrentWallet, useLanguage, useCurrency, useTheme, chainConfigStore, chainConfigSelectors, useUserProfile } from '@/stores';
+import {
+ useCurrentWallet,
+ useLanguage,
+ useCurrency,
+ useTheme,
+ chainConfigStore,
+ chainConfigSelectors,
+ useUserProfile,
+} from '@/stores';
import { ContactAvatar } from '@/components/common/contact-avatar';
import { SettingsItem } from './settings-item';
import { SettingsSection } from './settings-section';
@@ -30,10 +38,10 @@ import { setSetTwoStepSecretCallback } from '@/stackflow/activities/sheets';
/** 支持的语言显示名称映射 */
const LANGUAGE_NAMES: Record = {
- 'zh-CN': '简体中文',
- 'zh-TW': '繁體中文',
+ 'zh-CN': '简体中文', // i18n-ignore: native language name
+ 'zh-TW': '繁體中文', // i18n-ignore: native language name
en: 'English',
- ja: '日本語',
+ ja: '日本語', // i18n-ignore: native language name
ko: '한국어',
ar: 'العربية',
};
@@ -57,7 +65,9 @@ export function SettingsPage() {
const currentCurrency = useCurrency();
const currentTheme = useTheme();
const [appearanceSheetOpen, setAppearanceSheetOpen] = useState(false);
- const [twoStepSecretStatus, setTwoStepSecretStatus] = useState<'loading' | 'set' | 'not_set' | 'unavailable'>('loading');
+ const [twoStepSecretStatus, setTwoStepSecretStatus] = useState<'loading' | 'set' | 'not_set' | 'unavailable'>(
+ 'loading',
+ );
// Check if pay password is set for BioForest chain
useEffect(() => {
@@ -68,9 +78,7 @@ export function SettingsPage() {
}
// Find BioForest chain address
- const bfmAddress = currentWallet.chainAddresses.find(
- (ca) => ca.chain === 'bfmeta' || ca.chain === 'bfm'
- );
+ const bfmAddress = currentWallet.chainAddresses.find((ca) => ca.chain === 'bfmeta' || ca.chain === 'bfm');
if (!bfmAddress) {
setTwoStepSecretStatus('unavailable');
@@ -114,9 +122,7 @@ export function SettingsPage() {
const handleSetTwoStepSecret = async () => {
if (twoStepSecretStatus !== 'not_set' || !currentWallet) return;
- const bfmAddress = currentWallet.chainAddresses.find(
- (ca) => ca.chain === 'bfmeta' || ca.chain === 'bfm'
- );
+ const bfmAddress = currentWallet.chainAddresses.find((ca) => ca.chain === 'bfmeta' || ca.chain === 'bfm');
if (!bfmAddress) return;
const chainConfig = chainConfigSelectors.getChainById(chainConfigStore.state, 'bfmeta');
@@ -160,7 +166,7 @@ export function SettingsPage() {
} catch {
return false;
}
- }
+ },
);
push('SetTwoStepSecretJob', { chainName: 'BioForest Chain' });
@@ -174,15 +180,13 @@ export function SettingsPage() {
navigate({ to: '/my-card' })}
- className="bg-card flex w-full items-center gap-4 rounded-xl p-4 shadow-sm transition-colors hover:bg-accent"
+ className="bg-card hover:bg-accent flex w-full items-center gap-4 rounded-xl p-4 shadow-sm transition-colors"
data-testid="my-card-button"
>
{profile.username || t('common:myCard.defaultName')}
-
- {t('settings:items.myCard')}
-
+
{t('settings:items.myCard')}
@@ -272,7 +276,7 @@ export function SettingsPage() {
}
- label="小程序可信源"
+ label={t('settings:items.miniappSources')}
onClick={() => navigate({ to: '/settings/sources' })}
/>
@@ -300,10 +304,7 @@ export function SettingsPage() {
-
-
+
+
);
}
diff --git a/src/pages/settings/language.tsx b/src/pages/settings/language.tsx
index 216e426a8..3c3375989 100644
--- a/src/pages/settings/language.tsx
+++ b/src/pages/settings/language.tsx
@@ -7,8 +7,8 @@ import { cn } from '@/lib/utils';
/** 语言显示名称映射 - 使用原文 */
const LANGUAGE_DISPLAY: Record
= {
- 'zh-CN': '简体中文',
- 'zh-TW': '中文(繁體)',
+ 'zh-CN': '简体中文', // i18n-ignore: native language name
+ 'zh-TW': '中文(繁體)', // i18n-ignore: native language name
en: 'English',
ar: 'العربية',
};
diff --git a/src/services/migration/mpay-crypto.ts b/src/services/migration/mpay-crypto.ts
index be0d523dc..e2941f261 100644
--- a/src/services/migration/mpay-crypto.ts
+++ b/src/services/migration/mpay-crypto.ts
@@ -7,54 +7,51 @@
* 此模块提供 mpay 数据解密能力
*/
-import { validateMnemonic } from '@/lib/crypto/mnemonic'
+import { validateMnemonic } from '@/lib/crypto/mnemonic';
+import i18n from '@/i18n';
+
+const t = i18n.t.bind(i18n);
/**
* SHA256 哈希
*/
async function sha256Binary(data: ArrayBuffer): Promise {
- return crypto.subtle.digest('SHA-256', data)
+ return crypto.subtle.digest('SHA-256', data);
}
/**
* UTF8 字符串转 ArrayBuffer
*/
function encodeUTF8(text: string): ArrayBuffer {
- return new TextEncoder().encode(text).buffer as ArrayBuffer
+ return new TextEncoder().encode(text).buffer as ArrayBuffer;
}
/**
* ArrayBuffer 转 UTF8 字符串
*/
function decodeUTF8(data: ArrayBuffer): string {
- return new TextDecoder().decode(data)
+ return new TextDecoder().decode(data);
}
/**
* Base64 转 ArrayBuffer
*/
function base64ToArrayBuffer(base64: string): ArrayBuffer {
- const binary = atob(base64)
- const bytes = new Uint8Array(binary.length)
+ const binary = atob(base64);
+ const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
- bytes[i] = binary.charCodeAt(i)
+ bytes[i] = binary.charCodeAt(i);
}
- return bytes.buffer as ArrayBuffer
+ return bytes.buffer as ArrayBuffer;
}
/**
* 获取 mpay 格式的 AES-CTR 密钥
*/
async function getMpayCryptoKey(password: string): Promise {
- const passwordBuffer = encodeUTF8(password)
- const keyMaterial = await sha256Binary(passwordBuffer)
- return crypto.subtle.importKey(
- 'raw',
- keyMaterial,
- { name: 'AES-CTR', length: 256 },
- false,
- ['decrypt']
- )
+ const passwordBuffer = encodeUTF8(password);
+ const keyMaterial = await sha256Binary(passwordBuffer);
+ return crypto.subtle.importKey('raw', keyMaterial, { name: 'AES-CTR', length: 256 }, false, ['decrypt']);
}
/**
@@ -67,23 +64,16 @@ async function getMpayCryptoKey(password: string): Promise {
* @returns 解密后的明文
* @throws 密码错误时抛出异常
*/
-export async function decryptMpayData(
- password: string,
- encryptedBase64: string
-): Promise {
- const key = await getMpayCryptoKey(password)
- const encrypted = base64ToArrayBuffer(encryptedBase64)
- const counter = new Uint8Array(16) // mpay 使用全零 counter
+export async function decryptMpayData(password: string, encryptedBase64: string): Promise {
+ const key = await getMpayCryptoKey(password);
+ const encrypted = base64ToArrayBuffer(encryptedBase64);
+ const counter = new Uint8Array(16); // mpay 使用全零 counter
try {
- const decrypted = await crypto.subtle.decrypt(
- { name: 'AES-CTR', counter, length: 128 },
- key,
- encrypted
- )
- return decodeUTF8(decrypted)
+ const decrypted = await crypto.subtle.decrypt({ name: 'AES-CTR', counter, length: 128 }, key, encrypted);
+ return decodeUTF8(decrypted);
} catch {
- throw new Error('mpay 数据解密失败:密码错误或数据损坏')
+ throw new Error(t('error:mpay.decryptFailed'));
}
}
@@ -94,15 +84,12 @@ export async function decryptMpayData(
* @param encryptedBase64 mpay 加密的数据(通常是 importPhrase)
* @returns 密码是否正确
*/
-export async function verifyMpayPassword(
- password: string,
- encryptedBase64: string
-): Promise {
+export async function verifyMpayPassword(password: string, encryptedBase64: string): Promise {
try {
- const decrypted = await decryptMpayData(password, encryptedBase64)
- const words = decrypted.trim().split(/\s+/)
- return validateMnemonic(words)
+ const decrypted = await decryptMpayData(password, encryptedBase64);
+ const words = decrypted.trim().split(/\s+/);
+ return validateMnemonic(words);
} catch {
- return false
+ return false;
}
}
diff --git a/src/services/migration/mpay-transformer.ts b/src/services/migration/mpay-transformer.ts
index abcd280fc..85387c195 100644
--- a/src/services/migration/mpay-transformer.ts
+++ b/src/services/migration/mpay-transformer.ts
@@ -4,25 +4,24 @@
* 将 mpay 数据格式转换为 KeyApp 格式
*/
-import type { Wallet, ChainAddress, ChainType } from '@/stores/wallet'
-import type { EncryptedData } from '@/lib/crypto'
-import { encrypt } from '@/lib/crypto'
-import type {
- MpayMainWallet,
- MpayChainAddressInfo,
- MpayAddressBookEntry,
-} from './types'
-import type { Contact } from '@/stores/address-book'
-import { decryptMpayData } from './mpay-crypto'
+import type { Wallet, ChainAddress, ChainType } from '@/stores/wallet';
+import type { EncryptedData } from '@/lib/crypto';
+import { encrypt } from '@/lib/crypto';
+import type { MpayMainWallet, MpayChainAddressInfo, MpayAddressBookEntry } from './types';
+import type { Contact } from '@/stores/address-book';
+import { decryptMpayData } from './mpay-crypto';
+import i18n from '@/i18n';
+
+const t = i18n.t.bind(i18n);
function deriveThemeHue(secret: string): number {
- let hash = 0
+ let hash = 0;
for (let i = 0; i < secret.length; i++) {
- const char = secret.charCodeAt(i)
- hash = ((hash << 5) - hash) + char
- hash = hash & hash
+ const char = secret.charCodeAt(i);
+ hash = (hash << 5) - hash + char;
+ hash = hash & hash;
}
- return Math.abs(hash) % 360
+ return Math.abs(hash) % 360;
}
/**
@@ -38,13 +37,13 @@ const CHAIN_NAME_MAP: Record = {
Tron: 'tron',
BTC: 'bitcoin',
BSC: 'binance',
-}
+};
/**
* 转换 mpay 链名为 KeyApp ChainType
*/
function mapChainName(mpayChain: string): ChainType | null {
- return CHAIN_NAME_MAP[mpayChain] ?? null
+ return CHAIN_NAME_MAP[mpayChain] ?? null;
}
/**
@@ -56,13 +55,10 @@ function mapChainName(mpayChain: string): ChainType | null {
/**
* 转换 mpay ChainAddressInfo 为 KeyApp ChainAddress
*/
-function transformChainAddress(
- mpayAddress: MpayChainAddressInfo
-): ChainAddress | null {
- const chain = mapChainName(mpayAddress.chain)
+function transformChainAddress(mpayAddress: MpayChainAddressInfo): ChainAddress | null {
+ const chain = mapChainName(mpayAddress.chain);
if (!chain) {
-
- return null
+ return null;
}
return {
@@ -70,7 +66,7 @@ function transformChainAddress(
address: mpayAddress.address,
publicKey: '', // Will be derived on wallet unlock
// tokens 已从 ChainAddress 移除 - 余额数据从 chain-provider 获取
- }
+ };
}
/**
@@ -81,17 +77,17 @@ function transformChainAddress(
*/
function determineChainFromList(chainList?: string[]): ChainType | undefined {
if (!chainList || chainList.length === 0) {
- return undefined
+ return undefined;
}
for (const mpayChain of chainList) {
- const chain = mapChainName(mpayChain)
+ const chain = mapChainName(mpayChain);
if (chain) {
- return chain
+ return chain;
}
}
- return undefined
+ return undefined;
}
/**
@@ -101,8 +97,8 @@ function determineChainFromList(chainList?: string[]): ChainType | undefined {
* @returns KeyApp 联系人
*/
export function transformAddressBookEntry(entry: MpayAddressBookEntry): Contact {
- const now = Date.now()
- const chain = determineChainFromList(entry.chainList) ?? 'ethereum'
+ const now = Date.now();
+ const chain = determineChainFromList(entry.chainList) ?? 'ethereum';
const contact: Contact = {
id: entry.addressBookId,
@@ -117,13 +113,13 @@ export function transformAddressBookEntry(entry: MpayAddressBookEntry): Contact
],
createdAt: now,
updatedAt: now,
- }
+ };
if (entry.remarks) {
- contact.memo = entry.remarks
+ contact.memo = entry.remarks;
}
- return contact
+ return contact;
}
/**
@@ -131,19 +127,19 @@ export function transformAddressBookEntry(entry: MpayAddressBookEntry): Contact
*/
export interface TransformResult {
/** 转换后的钱包列表 */
- wallets: Wallet[]
+ wallets: Wallet[];
/** 跳过的地址(不支持的链) */
skippedAddresses: Array<{
- address: string
- chain: string
- reason: string
- }>
+ address: string;
+ chain: string;
+ reason: string;
+ }>;
/** 转换统计 */
stats: {
- totalWallets: number
- totalAddresses: number
- skippedAddresses: number
- }
+ totalWallets: number;
+ totalAddresses: number;
+ skippedAddresses: number;
+ };
}
/**
@@ -157,42 +153,42 @@ export interface TransformResult {
export async function transformMpayData(
mpayWallets: MpayMainWallet[],
mpayAddresses: MpayChainAddressInfo[],
- password: string
+ password: string,
): Promise {
- const wallets: Wallet[] = []
- const skippedAddresses: TransformResult['skippedAddresses'] = []
+ const wallets: Wallet[] = [];
+ const skippedAddresses: TransformResult['skippedAddresses'] = [];
// 按 mainWalletId 分组地址
- const addressesByWallet = new Map()
+ const addressesByWallet = new Map();
for (const addr of mpayAddresses) {
- const list = addressesByWallet.get(addr.mainWalletId) ?? []
- list.push(addr)
- addressesByWallet.set(addr.mainWalletId, list)
+ const list = addressesByWallet.get(addr.mainWalletId) ?? [];
+ list.push(addr);
+ addressesByWallet.set(addr.mainWalletId, list);
}
for (const mpayWallet of mpayWallets) {
try {
// 解密助记词
- const mnemonic = await decryptMpayData(password, mpayWallet.importPhrase)
+ const mnemonic = await decryptMpayData(password, mpayWallet.importPhrase);
// 用 KeyApp 格式重新加密
- const encryptedMnemonic: EncryptedData = await encrypt(mnemonic, password)
+ const encryptedMnemonic: EncryptedData = await encrypt(mnemonic, password);
// 获取该钱包的所有地址
- const walletAddresses = addressesByWallet.get(mpayWallet.mainWalletId) ?? []
+ const walletAddresses = addressesByWallet.get(mpayWallet.mainWalletId) ?? [];
// 转换地址
- const chainAddresses: ChainAddress[] = []
+ const chainAddresses: ChainAddress[] = [];
for (const mpayAddr of walletAddresses) {
- const converted = transformChainAddress(mpayAddr)
+ const converted = transformChainAddress(mpayAddr);
if (converted) {
- chainAddresses.push(converted)
+ chainAddresses.push(converted);
} else {
skippedAddresses.push({
address: mpayAddr.address,
chain: mpayAddr.chain,
- reason: `不支持的链类型: ${mpayAddr.chain}`,
- })
+ reason: t('error:chain.unsupportedType', { chain: mpayAddr.chain }),
+ });
}
}
@@ -202,10 +198,9 @@ export async function transformMpayData(
chainAddresses.find((ca) => ca.chain === 'bfmeta')?.chain ??
chainAddresses.find((ca) => ca.chain === 'ethereum')?.chain ??
chainAddresses[0]?.chain ??
- 'ethereum'
+ 'ethereum';
- const primaryAddress =
- chainAddresses.find((ca) => ca.chain === primaryChain)?.address ?? ''
+ const primaryAddress = chainAddresses.find((ca) => ca.chain === primaryChain)?.address ?? '';
// 创建 KeyApp 钱包 (不包含 tokens,余额从 chain-provider 获取)
const wallet = {
@@ -218,11 +213,10 @@ export async function transformMpayData(
createdAt: mpayWallet.createTimestamp,
themeHue: deriveThemeHue(mpayWallet.mainWalletId),
// tokens 已移除 - 从 chain-provider.tokenBalances 获取
- }
+ };
- wallets.push(wallet)
+ wallets.push(wallet);
} catch (error) {
-
// 继续处理其他钱包
}
}
@@ -235,7 +229,7 @@ export async function transformMpayData(
totalAddresses: mpayAddresses.length,
skippedAddresses: skippedAddresses.length,
},
- }
+ };
}
-export { mapChainName, transformChainAddress, determineChainFromList }
+export { mapChainName, transformChainAddress, determineChainFromList };
diff --git a/src/services/miniapp-runtime/index.ts b/src/services/miniapp-runtime/index.ts
index c37b1ba6a..e378ddc28 100644
--- a/src/services/miniapp-runtime/index.ts
+++ b/src/services/miniapp-runtime/index.ts
@@ -63,6 +63,9 @@ import type { MiniappManifest, MiniappTargetDesktop } from '../ecosystem/types';
import { getBridge } from '../ecosystem/provider';
import { toastService } from '../toast';
import { getDesktopAppSlotRect, getIconRef } from './runtime-refs';
+import i18n from '@/i18n';
+
+const t = i18n.t.bind(i18n);
export {
getDesktopContainerRef,
getDesktopAppSlotRect,
@@ -511,11 +514,11 @@ function startPreparing(appId: string, targetDesktop: MiniappTargetDesktop, hasS
if (performance.now() - startAt > PREPARING_TIMEOUT) {
if (!slotReady) {
- failPreparing(appId, '启动失败:请停留在目标桌面页后重试');
+ failPreparing(appId, t('error:miniapp.launchFailed.stayOnDesktop'));
} else if (!iconReady) {
- failPreparing(appId, '启动失败:图标未就绪,请返回桌面重试');
+ failPreparing(appId, t('error:miniapp.launchFailed.iconNotReady'));
} else {
- failPreparing(appId, '启动失败:加载容器未就绪,请重试');
+ failPreparing(appId, t('error:miniapp.launchFailed.containerNotReady'));
}
return;
}
diff --git a/src/services/transaction/pending-tx.ts b/src/services/transaction/pending-tx.ts
index 0a6dd0ba2..a2b3a8dd2 100644
--- a/src/services/transaction/pending-tx.ts
+++ b/src/services/transaction/pending-tx.ts
@@ -1,47 +1,71 @@
/**
* Pending Transaction Service
- *
+ *
* 未上链交易管理 - IndexedDB 存储实现
* 专注状态管理,不关心交易内容本身
*/
-import { z } from 'zod'
-import { openDB, type DBSchema, type IDBPDatabase } from 'idb'
-import { derive, transform } from '@biochain/key-fetch'
-import { getChainProvider } from '@/services/chain-adapter/providers'
-import { defineServiceMeta } from '@/lib/service-meta'
-import { SignedTransactionSchema } from '@/services/chain-adapter/types'
+import { z } from 'zod';
+import { openDB, type DBSchema, type IDBPDatabase } from 'idb';
+import { derive, transform } from '@biochain/key-fetch';
+import { getChainProvider } from '@/services/chain-adapter/providers';
+import { defineServiceMeta } from '@/lib/service-meta';
+import { SignedTransactionSchema } from '@/services/chain-adapter/types';
// ==================== Schema ====================
/** 未上链交易状态 */
export const PendingTxStatusSchema = z.enum([
- 'created', // 交易已创建,待广播
+ 'created', // 交易已创建,待广播
'broadcasting', // 广播中
- 'broadcasted', // 广播成功,待上链
- 'confirmed', // 已上链确认
- 'failed', // 广播失败
-])
+ 'broadcasted', // 广播成功,待上链
+ 'confirmed', // 已上链确认
+ 'failed', // 广播失败
+]);
// TransactionType as zod schema for validation
const TransactionTypeSchema = z.enum([
- 'send', 'receive', 'signature', 'stake', 'unstake', 'destroy', 'gift', 'grab',
- 'trust', 'signFor', 'emigrate', 'immigrate', 'exchange', 'swap', 'issueAsset',
- 'increaseAsset', 'mint', 'issueEntity', 'destroyEntity', 'locationName', 'dapp',
- 'certificate', 'mark', 'approve', 'interaction', 'other'
-])
+ 'send',
+ 'receive',
+ 'signature',
+ 'stake',
+ 'unstake',
+ 'destroy',
+ 'gift',
+ 'grab',
+ 'trust',
+ 'signFor',
+ 'emigrate',
+ 'immigrate',
+ 'exchange',
+ 'swap',
+ 'issueAsset',
+ 'increaseAsset',
+ 'mint',
+ 'issueEntity',
+ 'destroyEntity',
+ 'locationName',
+ 'dapp',
+ 'certificate',
+ 'mark',
+ 'approve',
+ 'interaction',
+ 'other',
+]);
/** 用于 UI 展示的最小元数据(可选,由调用方提供) */
-export const PendingTxMetaSchema = z.object({
- /** 交易类型标识,用于 UI 展示,必须是 TransactionType */
- type: TransactionTypeSchema.optional(),
- /** 展示用的金额 */
- displayAmount: z.string().optional(),
- /** 展示用的符号 */
- displaySymbol: z.string().optional(),
- /** 展示用的目标地址 */
- displayToAddress: z.string().optional(),
-}).passthrough() // 允许扩展字段
+export const PendingTxMetaSchema = z
+ .object({
+ /** 交易类型标识,用于 UI 展示,必须是 TransactionType */
+ type: TransactionTypeSchema.optional(),
+ /** 展示用的金额 */
+ displayAmount: z.string().optional(),
+ /** 展示用的符号 */
+ displaySymbol: z.string().optional(),
+ /** 展示用的目标地址 */
+ displayToAddress: z.string().optional(),
+ })
+ .passthrough(); // 允许扩展字段
/** 未上链交易记录 - 专注状态管理 */
export const PendingTxSchema = z.object({
@@ -81,11 +105,11 @@ export const PendingTxSchema = z.object({
rawTx: SignedTransactionSchema,
/** UI 展示用的元数据(可选) */
meta: PendingTxMetaSchema.optional(),
-})
+});
-export type PendingTx = z.infer
-export type PendingTxStatus = z.infer
-export type PendingTxMeta = z.infer
+export type PendingTx = z.infer;
+export type PendingTxStatus = z.infer;
+export type PendingTxMeta = z.infer;
/** 创建 pending tx 的输入 */
export const CreatePendingTxInputSchema = z.object({
@@ -94,9 +118,9 @@ export const CreatePendingTxInputSchema = z.object({
fromAddress: z.string(),
rawTx: SignedTransactionSchema,
meta: PendingTxMetaSchema.optional(),
-})
+});
-export type CreatePendingTxInput = z.infer
+export type CreatePendingTxInput = z.infer;
/** 更新状态的输入 */
export const UpdatePendingTxStatusInputSchema = z.object({
@@ -107,22 +131,27 @@ export const UpdatePendingTxStatusInputSchema = z.object({
errorMessage: z.string().optional(),
confirmedBlockHeight: z.number().optional(),
confirmedAt: z.number().optional(),
-})
+});
-export type UpdatePendingTxStatusInput = z.infer
+export type UpdatePendingTxStatusInput = z.infer;
// ==================== Service Meta ====================
export const pendingTxServiceMeta = defineServiceMeta('pendingTx', (s) =>
- s.description('未上链交易管理服务 - 专注状态管理,不关心交易内容')
+ s
+ .description('未上链交易管理服务 - 专注状态管理,不关心交易内容') // i18n-ignore
// ===== 查询 =====
.api('getAll', z.object({ walletId: z.string() }), z.array(PendingTxSchema))
.api('getById', z.object({ id: z.string() }), PendingTxSchema.nullable())
- .api('getByStatus', z.object({
- walletId: z.string(),
- status: PendingTxStatusSchema,
- }), z.array(PendingTxSchema))
+ .api(
+ 'getByStatus',
+ z.object({
+ walletId: z.string(),
+ status: PendingTxStatusSchema,
+ }),
+ z.array(PendingTxSchema),
+ )
.api('getPending', z.object({ walletId: z.string() }), z.array(PendingTxSchema))
// ===== 生命周期管理 =====
@@ -133,10 +162,10 @@ export const pendingTxServiceMeta = defineServiceMeta('pendingTx', (s) =>
// ===== 清理 =====
.api('delete', z.object({ id: z.string() }), z.void())
.api('deleteConfirmed', z.object({ walletId: z.string() }), z.void())
- .api('deleteAll', z.object({ walletId: z.string() }), z.void())
-)
+ .api('deleteAll', z.object({ walletId: z.string() }), z.void()),
+);
-export type IPendingTxService = typeof pendingTxServiceMeta.Type
+export type IPendingTxService = typeof pendingTxServiceMeta.Type;
// ==================== 过期检查器接口 ====================
@@ -151,7 +180,7 @@ export interface ExpirationChecker {
* @param currentBlockHeight 当前区块高度
* @returns 是否已过期
*/
- isExpired(rawTx: unknown, currentBlockHeight: number): boolean
+ isExpired(rawTx: unknown, currentBlockHeight: number): boolean;
}
/**
@@ -160,13 +189,13 @@ export interface ExpirationChecker {
*/
export const bioChainExpirationChecker: ExpirationChecker = {
isExpired(rawTx: unknown, currentBlockHeight: number): boolean {
- const tx = rawTx as { effectiveBlockHeight?: number }
+ const tx = rawTx as { effectiveBlockHeight?: number };
if (typeof tx?.effectiveBlockHeight === 'number') {
- return currentBlockHeight > tx.effectiveBlockHeight
+ return currentBlockHeight > tx.effectiveBlockHeight;
}
- return false // 无 effectiveBlockHeight 时不判定过期
- }
-}
+ return false; // 无 effectiveBlockHeight 时不判定过期
+ },
+};
/**
* 获取链对应的过期检查器
@@ -176,9 +205,9 @@ export const bioChainExpirationChecker: ExpirationChecker = {
export function getExpirationChecker(chainId: string): ExpirationChecker | undefined {
// BioChain 系列链使用 bioChainExpirationChecker
if (chainId.startsWith('bfmeta') || chainId.startsWith('bfm') || chainId === 'bioforest') {
- return bioChainExpirationChecker
+ return bioChainExpirationChecker;
}
- return undefined
+ return undefined;
}
/**
@@ -191,49 +220,49 @@ export function getExpirationChecker(chainId: string): ExpirationChecker | undef
export function isPendingTxExpired(
pendingTx: PendingTx,
currentBlockHeight?: number,
- maxAge: number = 24 * 60 * 60 * 1000
+ maxAge: number = 24 * 60 * 60 * 1000,
): boolean {
// 1. 基于时间的过期检查(适用于所有链)
- const now = Date.now()
+ const now = Date.now();
if (now - pendingTx.createdAt > maxAge) {
- return true
+ return true;
}
// 2. 基于区块高度的过期检查(针对 BioChain 等支持的链)
if (currentBlockHeight !== undefined) {
- const checker = getExpirationChecker(pendingTx.chainId)
+ const checker = getExpirationChecker(pendingTx.chainId);
if (checker?.isExpired(pendingTx.rawTx, currentBlockHeight)) {
- return true
+ return true;
}
}
- return false
+ return false;
}
// ==================== IndexedDB 实现 ====================
-const DB_NAME = 'bfm-pending-tx-db'
-const DB_VERSION = 1
-const STORE_NAME = 'pendingTx'
+const DB_NAME = 'bfm-pending-tx-db';
+const DB_VERSION = 1;
+const STORE_NAME = 'pendingTx';
interface PendingTxDBSchema extends DBSchema {
pendingTx: {
- key: string
- value: PendingTx
+ key: string;
+ value: PendingTx;
indexes: {
- 'by-wallet': string
- 'by-status': string
- 'by-wallet-status': [string, string]
- }
- }
+ 'by-wallet': string;
+ 'by-status': string;
+ 'by-wallet-status': [string, string];
+ };
+ };
}
-type PendingTxChangeCallback = (tx: PendingTx, event: 'created' | 'updated' | 'deleted') => void
+type PendingTxChangeCallback = (tx: PendingTx, event: 'created' | 'updated' | 'deleted') => void;
class PendingTxServiceImpl implements IPendingTxService {
- private db: IDBPDatabase | null = null
- private initialized = false
- private subscribers = new Set()
+ private db: IDBPDatabase | null = null;
+ private initialized = false;
+ private subscribers = new Set();
/**
* 订阅 pending tx 变化
@@ -241,10 +270,10 @@ class PendingTxServiceImpl implements IPendingTxService {
* @returns 取消订阅函数
*/
subscribe(callback: PendingTxChangeCallback): () => void {
- this.subscribers.add(callback)
+ this.subscribers.add(callback);
return () => {
- this.subscribers.delete(callback)
- }
+ this.subscribers.delete(callback);
+ };
}
/**
@@ -253,74 +282,72 @@ class PendingTxServiceImpl implements IPendingTxService {
private notify(tx: PendingTx, event: 'created' | 'updated' | 'deleted') {
this.subscribers.forEach((callback) => {
try {
- callback(tx, event)
- } catch (error) {
-
- }
- })
+ callback(tx, event);
+ } catch (error) {}
+ });
}
private async ensureDb(): Promise> {
if (this.db && this.initialized) {
- return this.db
+ return this.db;
}
this.db = await openDB(DB_NAME, DB_VERSION, {
upgrade(db) {
if (!db.objectStoreNames.contains(STORE_NAME)) {
- const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' })
- store.createIndex('by-wallet', 'walletId')
- store.createIndex('by-status', 'status')
- store.createIndex('by-wallet-status', ['walletId', 'status'])
+ const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' });
+ store.createIndex('by-wallet', 'walletId');
+ store.createIndex('by-status', 'status');
+ store.createIndex('by-wallet-status', ['walletId', 'status']);
}
},
- })
- this.initialized = true
- return this.db
+ });
+ this.initialized = true;
+ return this.db;
}
// ===== 查询 =====
async getAll({ walletId }: { walletId: string }): Promise {
- const db = await this.ensureDb()
- const records = await db.getAllFromIndex(STORE_NAME, 'by-wallet', walletId)
+ const db = await this.ensureDb();
+ const records = await db.getAllFromIndex(STORE_NAME, 'by-wallet', walletId);
return records
.map((r) => PendingTxSchema.safeParse(r))
.filter((r) => r.success)
.map((r) => r.data)
- .sort((a, b) => b.createdAt - a.createdAt)
+ .sort((a, b) => b.createdAt - a.createdAt);
}
async getById({ id }: { id: string }): Promise {
- const db = await this.ensureDb()
- const record = await db.get(STORE_NAME, id)
- if (!record) return null
- const parsed = PendingTxSchema.safeParse(record)
- return parsed.success ? parsed.data : null
+ const db = await this.ensureDb();
+ const record = await db.get(STORE_NAME, id);
+ if (!record) return null;
+ const parsed = PendingTxSchema.safeParse(record);
+ return parsed.success ? parsed.data : null;
}
async getByStatus({ walletId, status }: { walletId: string; status: PendingTxStatus }): Promise {
- const db = await this.ensureDb()
- const records = await db.getAllFromIndex(STORE_NAME, 'by-wallet-status', [walletId, status])
+ const db = await this.ensureDb();
+ const records = await db.getAllFromIndex(STORE_NAME, 'by-wallet-status', [walletId, status]);
return records
.map((r) => PendingTxSchema.safeParse(r))
.filter((r) => r.success)
.map((r) => r.data)
- .sort((a, b) => b.createdAt - a.createdAt)
+ .sort((a, b) => b.createdAt - a.createdAt);
}
async getPending({ walletId }: { walletId: string }): Promise {
- const all = await this.getAll({ walletId })
- return all.filter((tx) => tx.status !== 'confirmed')
+ const all = await this.getAll({ walletId });
+ return all.filter((tx) => tx.status !== 'confirmed');
}
// ===== 生命周期管理 =====
async create(input: CreatePendingTxInput): Promise {
- const db = await this.ensureDb()
- const now = Date.now()
+ const db = await this.ensureDb();
+ const now = Date.now();
// 使用区块链签名作为 ID(即交易哈希)
- const txHash = input.rawTx.signature
+ const txHash = input.rawTx.signature;
const pendingTx: PendingTx = {
id: txHash,
@@ -334,19 +361,19 @@ class PendingTxServiceImpl implements IPendingTxService {
updatedAt: now,
rawTx: input.rawTx,
meta: input.meta,
- }
+ };
- await db.put(STORE_NAME, pendingTx)
- this.notify(pendingTx, 'created')
- return pendingTx
+ await db.put(STORE_NAME, pendingTx);
+ this.notify(pendingTx, 'created');
+ return pendingTx;
}
async updateStatus(input: UpdatePendingTxStatusInput): Promise {
- const db = await this.ensureDb()
- const existing = await db.get(STORE_NAME, input.id)
+ const db = await this.ensureDb();
+ const existing = await db.get(STORE_NAME, input.id);
if (!existing) {
- throw new Error(`PendingTx not found: ${input.id}`)
+ throw new Error(`PendingTx not found: ${input.id}`);
}
const updated: PendingTx = {
@@ -358,114 +385,118 @@ class PendingTxServiceImpl implements IPendingTxService {
...(input.errorMessage !== undefined && { errorMessage: input.errorMessage }),
...(input.confirmedBlockHeight !== undefined && { confirmedBlockHeight: input.confirmedBlockHeight }),
...(input.confirmedAt !== undefined && { confirmedAt: input.confirmedAt }),
- }
+ };
- await db.put(STORE_NAME, updated)
- this.notify(updated, 'updated')
- return updated
+ await db.put(STORE_NAME, updated);
+ this.notify(updated, 'updated');
+ return updated;
}
async incrementRetry({ id }: { id: string }): Promise {
- const db = await this.ensureDb()
- const existing = await db.get(STORE_NAME, id)
+ const db = await this.ensureDb();
+ const existing = await db.get(STORE_NAME, id);
if (!existing) {
- throw new Error(`PendingTx not found: ${id}`)
+ throw new Error(`PendingTx not found: ${id}`);
}
const updated: PendingTx = {
...existing,
retryCount: (existing.retryCount ?? 0) + 1,
updatedAt: Date.now(),
- }
+ };
- await db.put(STORE_NAME, updated)
- this.notify(updated, 'updated')
- return updated
+ await db.put(STORE_NAME, updated);
+ this.notify(updated, 'updated');
+ return updated;
}
// ===== 清理 =====
async delete({ id }: { id: string }): Promise {
- const db = await this.ensureDb()
- const existing = await db.get(STORE_NAME, id)
- await db.delete(STORE_NAME, id)
+ const db = await this.ensureDb();
+ const existing = await db.get(STORE_NAME, id);
+ await db.delete(STORE_NAME, id);
if (existing) {
- this.notify(existing, 'deleted')
+ this.notify(existing, 'deleted');
}
}
async deleteConfirmed({ walletId }: { walletId: string }): Promise {
- const confirmed = await this.getByStatus({ walletId, status: 'confirmed' })
- const db = await this.ensureDb()
- const tx = db.transaction(STORE_NAME, 'readwrite')
- await Promise.all(confirmed.map((item) => tx.store.delete(item.id)))
- await tx.done
+ const confirmed = await this.getByStatus({ walletId, status: 'confirmed' });
+ const db = await this.ensureDb();
+ const tx = db.transaction(STORE_NAME, 'readwrite');
+ await Promise.all(confirmed.map((item) => tx.store.delete(item.id)));
+ await tx.done;
}
- async deleteExpired({ walletId, maxAge, currentBlockHeight }: {
- walletId: string
- maxAge: number
- currentBlockHeight?: number
+ async deleteExpired({
+ walletId,
+ maxAge,
+ currentBlockHeight,
+ }: {
+ walletId: string;
+ maxAge: number;
+ currentBlockHeight?: number;
}): Promise {
- const all = await this.getAll({ walletId })
- const now = Date.now()
+ const all = await this.getAll({ walletId });
+ const now = Date.now();
const expired = all.filter((pendingTx) => {
// 1. 已确认或失败超过 maxAge 的交易
if (pendingTx.status === 'confirmed' || pendingTx.status === 'failed') {
- return now - pendingTx.updatedAt > maxAge
+ return now - pendingTx.updatedAt > maxAge;
}
// 2. 基于区块高度的过期检查(针对 BioChain 等支持的链)
if (currentBlockHeight !== undefined) {
- const checker = getExpirationChecker(pendingTx.chainId)
+ const checker = getExpirationChecker(pendingTx.chainId);
if (checker?.isExpired(pendingTx.rawTx, currentBlockHeight)) {
- return true
+ return true;
}
}
- return false
- })
+ return false;
+ });
- if (expired.length === 0) return 0
+ if (expired.length === 0) return 0;
- const db = await this.ensureDb()
- const tx = db.transaction(STORE_NAME, 'readwrite')
- await Promise.all(expired.map((item) => tx.store.delete(item.id)))
- await tx.done
+ const db = await this.ensureDb();
+ const tx = db.transaction(STORE_NAME, 'readwrite');
+ await Promise.all(expired.map((item) => tx.store.delete(item.id)));
+ await tx.done;
- return expired.length
+ return expired.length;
}
async deleteAll({ walletId }: { walletId: string }): Promise {
- const all = await this.getAll({ walletId })
- const db = await this.ensureDb()
- const tx = db.transaction(STORE_NAME, 'readwrite')
- await Promise.all(all.map((item) => tx.store.delete(item.id)))
- await tx.done
+ const all = await this.getAll({ walletId });
+ const db = await this.ensureDb();
+ const tx = db.transaction(STORE_NAME, 'readwrite');
+ await Promise.all(all.map((item) => tx.store.delete(item.id)));
+ await tx.done;
}
}
/** 单例服务实例 */
-export const pendingTxService = new PendingTxServiceImpl()
+export const pendingTxService = new PendingTxServiceImpl();
// ==================== Key-Fetch Instance Factory ====================
// 缓存已创建的 fetcher 实例
-const pendingTxFetchers = new Map>()
+const pendingTxFetchers = new Map>();
/**
* 获取 pending tx 的 key-fetch 实例
* 依赖 blockHeight 自动刷新
*/
export function getPendingTxFetcher(chainId: string, walletId: string) {
- const key = `${chainId}:${walletId}`
+ const key = `${chainId}:${walletId}`;
if (!pendingTxFetchers.has(key)) {
- const chainProvider = getChainProvider(chainId)
+ const chainProvider = getChainProvider(chainId);
if (!chainProvider?.supports('blockHeight')) {
- return null
+ return null;
}
const fetcher = derive({
@@ -476,33 +507,33 @@ export function getPendingTxFetcher(chainId: string, walletId: string) {
transform({
transform: async () => {
// 检查 pending 交易状态,更新/移除已上链的
- const pending = await pendingTxService.getPending({ walletId })
+ const pending = await pendingTxService.getPending({ walletId });
for (const tx of pending) {
if (tx.status === 'broadcasted' && tx.txHash) {
try {
// 检查是否已上链
- const txInfo = await chainProvider.transaction.fetch({ txHash: tx.txHash })
+ const txInfo = await chainProvider.transaction.fetch({ txHash: tx.txHash });
if (txInfo?.status === 'confirmed') {
// 直接删除已确认的交易
- await pendingTxService.delete({ id: tx.id })
+ await pendingTxService.delete({ id: tx.id });
}
} catch (e) {
- console.error('检查pending交易状态失败', e)
+ console.error('检查pending交易状态失败', e); // i18n-ignore;
// 查询失败,跳过
}
}
}
// 返回最新的 pending 列表
- return await pendingTxService.getPending({ walletId })
+ return await pendingTxService.getPending({ walletId });
},
}),
],
- })
+ });
- pendingTxFetchers.set(key, fetcher)
+ pendingTxFetchers.set(key, fetcher);
}
- return pendingTxFetchers.get(key)!
+ return pendingTxFetchers.get(key)!;
}
diff --git a/src/stackflow/activities/SettingsSourcesActivity.tsx b/src/stackflow/activities/SettingsSourcesActivity.tsx
index de73b6a7e..9d74e4ae4 100644
--- a/src/stackflow/activities/SettingsSourcesActivity.tsx
+++ b/src/stackflow/activities/SettingsSourcesActivity.tsx
@@ -3,111 +3,98 @@
* 管理小程序订阅源
*/
-import { useState } from 'react'
-import type { ActivityComponentType } from '@stackflow/react'
-import { AppScreen } from '@stackflow/plugin-basic-ui'
-import { useTranslation } from 'react-i18next'
-import { useStore } from '@tanstack/react-store'
-import { cn } from '@/lib/utils'
-import {
- IconPlus,
- IconTrash,
- IconRefresh,
- IconCheck,
- IconX,
- IconWorld,
- IconArrowLeft,
-} from '@tabler/icons-react'
-import { ecosystemStore, ecosystemActions, type SourceRecord } from '@/stores/ecosystem'
-import { refreshSources } from '@/services/ecosystem/registry'
-import { useFlow } from '../stackflow'
+import { useState } from 'react';
+import type { ActivityComponentType } from '@stackflow/react';
+import { AppScreen } from '@stackflow/plugin-basic-ui';
+import { useTranslation } from 'react-i18next';
+import { useStore } from '@tanstack/react-store';
+import { cn } from '@/lib/utils';
+import { IconPlus, IconTrash, IconRefresh, IconCheck, IconX, IconWorld, IconArrowLeft } from '@tabler/icons-react';
+import { ecosystemStore, ecosystemActions, type SourceRecord } from '@/stores/ecosystem';
+import { refreshSources } from '@/services/ecosystem/registry';
+import { useFlow } from '../stackflow';
export const SettingsSourcesActivity: ActivityComponentType = () => {
- const { t } = useTranslation('common')
- const { pop } = useFlow()
- const state = useStore(ecosystemStore)
+ const { t } = useTranslation('common');
+ const { pop } = useFlow();
+ const state = useStore(ecosystemStore);
- const [isAdding, setIsAdding] = useState(false)
- const [newUrl, setNewUrl] = useState('')
- const [newName, setNewName] = useState('')
- const [isRefreshing, setIsRefreshing] = useState(false)
- const [error, setError] = useState(null)
+ const [isAdding, setIsAdding] = useState(false);
+ const [newUrl, setNewUrl] = useState('');
+ const [newName, setNewName] = useState('');
+ const [isRefreshing, setIsRefreshing] = useState(false);
+ const [error, setError] = useState(null);
const handleAdd = async () => {
if (!newUrl.trim()) {
- setError('请输入订阅 URL')
- return
+ setError(t('sources.enterUrl'));
+ return;
}
// 验证 URL
try {
- new URL(newUrl)
+ new URL(newUrl);
} catch {
- setError('无效的 URL 格式')
- return
+ setError(t('sources.invalidUrl'));
+ return;
}
// 检查是否已存在
if (state.sources.some((s) => s.url === newUrl)) {
- setError('该订阅源已存在')
- return
+ setError(t('sources.alreadyExists'));
+ return;
}
- ecosystemActions.addSource(newUrl, newName || '自定义源')
- setNewUrl('')
- setNewName('')
- setIsAdding(false)
- setError(null)
- }
+ ecosystemActions.addSource(newUrl, newName || t('sources.customSource'));
+ setNewUrl('');
+ setNewName('');
+ setIsAdding(false);
+ setError(null);
+ };
const handleRemove = (url: string) => {
- ecosystemActions.removeSource(url)
- }
+ ecosystemActions.removeSource(url);
+ };
const handleToggle = (url: string) => {
- ecosystemActions.toggleSource(url)
- }
+ ecosystemActions.toggleSource(url);
+ };
const handleRefresh = async () => {
- setIsRefreshing(true)
+ setIsRefreshing(true);
try {
- await refreshSources()
- } catch (e) {
-
- }
- setIsRefreshing(false)
- }
+ await refreshSources();
+ } catch (e) {}
+ setIsRefreshing(false);
+ };
return (
-
+
{/* Header */}
-
+
pop()}
- className="p-1.5 rounded-full hover:bg-muted transition-colors"
- aria-label={t('back', '返回')}
+ className="hover:bg-muted rounded-full p-1.5 transition-colors"
+ aria-label={t('back')}
>
-
可信源管理
+ {t('sources.title')}
-
+
{/* Sources List */}
-
+
{state.sources.map((source) => (
{
/>
))}
- {state.sources.length === 0 && (
-
- 暂无订阅源
-
- )}
+ {state.sources.length === 0 && 暂无订阅源
}
{/* Add Source */}
{isAdding ? (
-
+
{
- setNewUrl(e.target.value)
- setError(null)
+ setNewUrl(e.target.value);
+ setError(null);
}}
- placeholder="订阅 URL (https://...)"
- className="w-full px-3 py-2 rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
-
+ placeholder={t('sources.urlPlaceholder')}
+ className="bg-background focus:ring-primary w-full rounded-lg border px-3 py-2 focus:ring-2 focus:outline-none"
/>
setNewName(e.target.value)}
- placeholder="名称 (可选)"
- className="w-full px-3 py-2 rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
+ placeholder={t('sources.namePlaceholder')}
+ className="bg-background focus:ring-primary w-full rounded-lg border px-3 py-2 focus:ring-2 focus:outline-none"
/>
- {error && (
-
{error}
- )}
+ {error &&
{error}
}
{
- setIsAdding(false)
- setNewUrl('')
- setNewName('')
- setError(null)
+ setIsAdding(false);
+ setNewUrl('');
+ setNewName('');
+ setError(null);
}}
- className="flex-1 py-2 rounded-lg bg-muted hover:bg-muted/80 font-medium"
+ className="bg-muted hover:bg-muted/80 flex-1 rounded-lg py-2 font-medium"
>
- 取消
+ {t('cancel')}
- 添加
+ {t('add')}
@@ -172,10 +152,10 @@ export const SettingsSourcesActivity: ActivityComponentType = () => {
setIsAdding(true)}
- className="w-full py-3 rounded-xl border-2 border-dashed border-muted-foreground/30 hover:border-primary hover:bg-primary/5 transition-colors flex items-center justify-center gap-2 text-muted-foreground hover:text-primary"
+ className="border-muted-foreground/30 hover:border-primary hover:bg-primary/5 text-muted-foreground hover:text-primary flex w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed py-3 transition-colors"
>
- 添加订阅源
+ {t('sources.addSource')}
)}
@@ -184,75 +164,62 @@ export const SettingsSourcesActivity: ActivityComponentType = () => {
- )
-}
+ );
+};
interface SourceItemProps {
- source: SourceRecord
- onToggle: () => void
- onRemove: () => void
+ source: SourceRecord;
+ onToggle: () => void;
+ onRemove: () => void;
}
function SourceItem({ source, onToggle, onRemove }: SourceItemProps) {
- const isDefault = source.url === '/ecosystem.json'
+ const isDefault = source.url === '/ecosystem.json';
return (
{/* Icon */}
-
-
+
+
{/* Info */}
-
+
-
{source.name}
- {isDefault && (
-
- 官方
-
- )}
+ {source.name}
+ {isDefault && 官方}
-
- {source.url}
-
-
+
{source.url}
+
更新于 {new Date(source.lastUpdated).toLocaleDateString()}
{/* Actions */}
-
+
{/* Toggle */}
- {source.enabled ? (
-
- ) : (
-
- )}
+ {source.enabled ? : }
{/* Remove (not for default) */}
{!isDefault && (
@@ -260,5 +227,5 @@ function SourceItem({ source, onToggle, onRemove }: SourceItemProps) {
- )
+ );
}
diff --git a/src/stackflow/activities/sheets/ChainSwitchConfirmJob.tsx b/src/stackflow/activities/sheets/ChainSwitchConfirmJob.tsx
index cc56ff4c6..a746b7858 100644
--- a/src/stackflow/activities/sheets/ChainSwitchConfirmJob.tsx
+++ b/src/stackflow/activities/sheets/ChainSwitchConfirmJob.tsx
@@ -3,75 +3,75 @@
* 当 DApp 请求 wallet_switchEthereumChain 时显示
*/
-import type { ActivityComponentType } from '@stackflow/react'
-import { BottomSheet } from '@/components/layout/bottom-sheet'
-import { useTranslation } from 'react-i18next'
-import { IconArrowRight, IconAlertTriangle } from '@tabler/icons-react'
-import { ChainIcon } from '@/components/wallet/chain-icon'
-import { MiniappSheetHeader } from '@/components/ecosystem'
-import { useFlow } from '../../stackflow'
-import { ActivityParamsProvider, useActivityParams } from '../../hooks'
-import { parseHexChainId, getKeyAppChainId } from '@biochain/bio-sdk'
-import { chainConfigService } from '@/services/chain-config'
-import type { ChainType } from '@/stores'
+import type { ActivityComponentType } from '@stackflow/react';
+import { BottomSheet } from '@/components/layout/bottom-sheet';
+import { useTranslation } from 'react-i18next';
+import { IconArrowRight, IconAlertTriangle } from '@tabler/icons-react';
+import { ChainIcon } from '@/components/wallet/chain-icon';
+import { MiniappSheetHeader } from '@/components/ecosystem';
+import { useFlow } from '../../stackflow';
+import { ActivityParamsProvider, useActivityParams } from '../../hooks';
+import { parseHexChainId, getKeyAppChainId } from '@biochain/bio-sdk';
+import { chainConfigService } from '@/services/chain-config';
+import type { ChainType } from '@/stores';
type ChainSwitchConfirmJobParams = {
/** 当前链 ID (hex, e.g., '0x38') */
- fromChainId: string
+ fromChainId: string;
/** 目标链 ID (hex, e.g., '0x1') */
- toChainId: string
+ toChainId: string;
/** 请求来源小程序名称 */
- appName?: string
+ appName?: string;
/** 请求来源小程序图标 */
- appIcon?: string
-}
+ appIcon?: string;
+};
/** 获取链的显示名称 */
function getChainDisplayName(hexChainId: string): string {
- const keyAppId = getKeyAppChainId(hexChainId)
+ const keyAppId = getKeyAppChainId(hexChainId);
if (keyAppId) {
- return chainConfigService.getName(keyAppId)
+ return chainConfigService.getName(keyAppId);
}
// Fallback: 显示 decimal chainId
try {
- const decimal = parseHexChainId(hexChainId)
- return `Chain ${decimal}`
+ const decimal = parseHexChainId(hexChainId);
+ return `Chain ${decimal}`;
} catch {
- return hexChainId
+ return hexChainId;
}
}
/** 获取 KeyApp 链类型 */
function getChainType(hexChainId: string): ChainType | null {
- const keyAppId = getKeyAppChainId(hexChainId)
- return keyAppId as ChainType | null
+ const keyAppId = getKeyAppChainId(hexChainId);
+ return keyAppId as ChainType | null;
}
function ChainSwitchConfirmJobContent() {
- const { t } = useTranslation('common')
- const { pop } = useFlow()
- const { fromChainId, toChainId, appName, appIcon } = useActivityParams
()
+ const { t } = useTranslation('common');
+ const { pop } = useFlow();
+ const { fromChainId, toChainId, appName, appIcon } = useActivityParams();
- const fromChainName = getChainDisplayName(fromChainId)
- const toChainName = getChainDisplayName(toChainId)
- const fromChainType = getChainType(fromChainId)
- const toChainType = getChainType(toChainId)
+ const fromChainName = getChainDisplayName(fromChainId);
+ const toChainName = getChainDisplayName(toChainId);
+ const fromChainType = getChainType(fromChainId);
+ const toChainType = getChainType(toChainId);
const handleConfirm = () => {
const event = new CustomEvent('chain-switch-confirm', {
detail: { approved: true, toChainId },
- })
- window.dispatchEvent(event)
- pop()
- }
+ });
+ window.dispatchEvent(event);
+ pop();
+ };
const handleCancel = () => {
const event = new CustomEvent('chain-switch-confirm', {
detail: { approved: false },
- })
- window.dispatchEvent(event)
- pop()
- }
+ });
+ window.dispatchEvent(event);
+ pop();
+ };
return (
@@ -83,8 +83,8 @@ function ChainSwitchConfirmJobContent() {
{/* App Info */}
@@ -121,9 +121,7 @@ function ChainSwitchConfirmJobContent() {
{/* Warning */}
-
- {t('chainSwitchWarning', '切换网络后,您的交易将在新网络上进行。请确保您了解此操作的影响。')}
-
+
{t('chainSwitchWarning')}
{/* Buttons */}
@@ -132,13 +130,13 @@ function ChainSwitchConfirmJobContent() {
onClick={handleCancel}
className="bg-muted hover:bg-muted/80 flex-1 rounded-xl py-3 font-medium transition-colors"
>
- {t('cancel', '取消')}
+ {t('cancel')}
- {t('confirm', '确认')}
+ {t('confirm')}
@@ -146,7 +144,7 @@ function ChainSwitchConfirmJobContent() {
- )
+ );
}
export const ChainSwitchConfirmJob: ActivityComponentType
= ({ params }) => {
@@ -154,5 +152,5 @@ export const ChainSwitchConfirmJob: ActivityComponentType
- )
-}
+ );
+};
diff --git a/src/stackflow/activities/sheets/CryptoAuthorizeJob.tsx b/src/stackflow/activities/sheets/CryptoAuthorizeJob.tsx
index 1a7af9586..830604619 100644
--- a/src/stackflow/activities/sheets/CryptoAuthorizeJob.tsx
+++ b/src/stackflow/activities/sheets/CryptoAuthorizeJob.tsx
@@ -1,234 +1,221 @@
/**
* CryptoAuthorizeJob - Crypto 黑盒授权对话框
- *
+ *
* 用于小程序请求加密操作授权,用户需输入手势密码确认
*/
-import { useCallback, useState } from 'react'
-import type { ActivityComponentType } from '@stackflow/react'
-import { BottomSheet } from '@/components/layout/bottom-sheet'
-import { useTranslation } from 'react-i18next'
-import { IconLock, IconLoader2 } from '@tabler/icons-react'
-import { useFlow } from '../../stackflow'
-import { ActivityParamsProvider, useActivityParams } from '../../hooks'
-import { MiniappSheetHeader } from '@/components/ecosystem'
-import { PatternLock, patternToString } from '@/components/security/pattern-lock'
-import { walletStorageService } from '@/services/wallet-storage'
+import { useCallback, useState } from 'react';
+import type { ActivityComponentType } from '@stackflow/react';
+import { BottomSheet } from '@/components/layout/bottom-sheet';
+import { useTranslation } from 'react-i18next';
+import { IconLock, IconLoader2 } from '@tabler/icons-react';
+import { useFlow } from '../../stackflow';
+import { ActivityParamsProvider, useActivityParams } from '../../hooks';
+import { MiniappSheetHeader } from '@/components/ecosystem';
+import { PatternLock, patternToString } from '@/components/security/pattern-lock';
+import { walletStorageService } from '@/services/wallet-storage';
import {
- type CryptoAction,
- type TokenDuration,
- CRYPTO_ACTION_LABELS,
- TOKEN_DURATION_LABELS,
- TOKEN_DURATION_OPTIONS,
-} from '@/services/crypto-box'
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select'
-import { walletStore } from '@/stores'
+ type CryptoAction,
+ type TokenDuration,
+ CRYPTO_ACTION_LABELS,
+ TOKEN_DURATION_LABELS,
+ TOKEN_DURATION_OPTIONS,
+} from '@/services/crypto-box';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+import { walletStore } from '@/stores';
type CryptoAuthorizeJobParams = {
- /** 请求的操作权限 (JSON 字符串) */
- actions: string
- /** 授权时长 */
- duration: string
- /** 使用的地址 */
- address: string
- /** 链 ID */
- chainId?: string
- /** 请求来源小程序名称 */
- appName?: string
- /** 请求来源小程序图标 */
- appIcon?: string
-}
+ /** 请求的操作权限 (JSON 字符串) */
+ actions: string;
+ /** 授权时长 */
+ duration: string;
+ /** 使用的地址 */
+ address: string;
+ /** 链 ID */
+ chainId?: string;
+ /** 请求来源小程序名称 */
+ appName?: string;
+ /** 请求来源小程序图标 */
+ appIcon?: string;
+};
function CryptoAuthorizeJobContent() {
- const { t } = useTranslation('common')
- const { pop } = useFlow()
- const params = useActivityParams()
-
- const actions = JSON.parse(params.actions) as CryptoAction[]
- const duration = params.duration as TokenDuration
- const { address, chainId, appName, appIcon } = params
-
- // 找到使用该地址的钱包
- const targetWallet = walletStore.state.wallets.find(
- w => w.chainAddresses.some(ca => ca.address === address)
- )
- const walletName = targetWallet?.name
- const walletId = targetWallet?.id
-
- const [pattern, setPattern] = useState([])
- const [error, setError] = useState(false)
- const [isVerifying, setIsVerifying] = useState(false)
- const [selectedDuration, setSelectedDuration] = useState(duration)
-
- const handlePatternComplete = useCallback(
- async (nodes: number[]) => {
- setIsVerifying(true)
- setError(false)
-
- try {
- const patternKey = patternToString(nodes)
-
- // 验证手势密码是否正确
- const wallets = await walletStorageService.getAllWallets()
- if (wallets.length === 0) {
- setError(true)
- setPattern([])
- setIsVerifying(false)
- return
- }
-
- let isValid = false
- for (const wallet of wallets) {
- try {
- await walletStorageService.getMnemonic(wallet.id, patternKey)
- isValid = true
- break
- } catch {
- // 继续尝试下一个钱包
- }
- }
-
- if (isValid && walletId) {
- // 发送成功事件(包含 walletId 和 selectedDuration 用于 Token 创建)
- const event = new CustomEvent('crypto-authorize-confirm', {
- detail: { approved: true, patternKey, walletId, selectedDuration },
- })
- window.dispatchEvent(event)
- pop()
- } else {
- setError(true)
- setPattern([])
- }
- } catch {
- setError(true)
- setPattern([])
- } finally {
- setIsVerifying(false)
- }
- },
- [pop, selectedDuration]
- )
-
- const handleCancel = useCallback(() => {
- const event = new CustomEvent('crypto-authorize-confirm', {
- detail: { approved: false },
- })
- window.dispatchEvent(event)
- pop()
- }, [pop])
-
- return (
-
-
- {/* Handle */}
-
-
- {/* Header - 左侧 miniapp 信息,右侧钱包信息 */}
-
-
- {/* Content - 可滚动区域 */}
-
- {/* 权限和授权时长 */}
-
- {/* 请求权限 - 水平排列 */}
-
-
- {t('permissions', '权限')}:
-
- {actions.map(a => CRYPTO_ACTION_LABELS[a]?.name || a).join('、')}
-
-
-
- {/* 授权时长 */}
-
- {t('duration', '时长')}:
-
-
-
-
-
- {/* 手势密码区域 - 固定尺寸不可压缩 */}
-
-
- {t('drawPatternToConfirm', '请绘制手势密码确认')}
-
- {/* 固定尺寸容器,防止内容变化导致布局抖动 */}
-
- {/* 验证中状态 - 固定高度 */}
-
- {isVerifying && (
-
-
- {t('verifying', '验证中...')}
-
- )}
-
-
-
- {/* Cancel button */}
-
-
- {t('cancel', '取消')}
-
-
-
- {/* Safe area */}
-
+ const { t } = useTranslation('common');
+ const { pop } = useFlow();
+ const params = useActivityParams
();
+
+ const actions = JSON.parse(params.actions) as CryptoAction[];
+ const duration = params.duration as TokenDuration;
+ const { address, chainId, appName, appIcon } = params;
+
+ // 找到使用该地址的钱包
+ const targetWallet = walletStore.state.wallets.find((w) => w.chainAddresses.some((ca) => ca.address === address));
+ const walletName = targetWallet?.name;
+ const walletId = targetWallet?.id;
+
+ const [pattern, setPattern] = useState([]);
+ const [error, setError] = useState(false);
+ const [isVerifying, setIsVerifying] = useState(false);
+ const [selectedDuration, setSelectedDuration] = useState(duration);
+
+ const handlePatternComplete = useCallback(
+ async (nodes: number[]) => {
+ setIsVerifying(true);
+ setError(false);
+
+ try {
+ const patternKey = patternToString(nodes);
+
+ // 验证手势密码是否正确
+ const wallets = await walletStorageService.getAllWallets();
+ if (wallets.length === 0) {
+ setError(true);
+ setPattern([]);
+ setIsVerifying(false);
+ return;
+ }
+
+ let isValid = false;
+ for (const wallet of wallets) {
+ try {
+ await walletStorageService.getMnemonic(wallet.id, patternKey);
+ isValid = true;
+ break;
+ } catch {
+ // 继续尝试下一个钱包
+ }
+ }
+
+ if (isValid && walletId) {
+ // 发送成功事件(包含 walletId 和 selectedDuration 用于 Token 创建)
+ const event = new CustomEvent('crypto-authorize-confirm', {
+ detail: { approved: true, patternKey, walletId, selectedDuration },
+ });
+ window.dispatchEvent(event);
+ pop();
+ } else {
+ setError(true);
+ setPattern([]);
+ }
+ } catch {
+ setError(true);
+ setPattern([]);
+ } finally {
+ setIsVerifying(false);
+ }
+ },
+ [pop, selectedDuration],
+ );
+
+ const handleCancel = useCallback(() => {
+ const event = new CustomEvent('crypto-authorize-confirm', {
+ detail: { approved: false },
+ });
+ window.dispatchEvent(event);
+ pop();
+ }, [pop]);
+
+ return (
+
+
+ {/* Handle */}
+
+
+ {/* Header - 左侧 miniapp 信息,右侧钱包信息 */}
+
+
+ {/* Content - 可滚动区域 */}
+
+ {/* 权限和授权时长 */}
+
+ {/* 请求权限 - 水平排列 */}
+
+
+ {t('permissions')}:
+
+ {actions.map((a) => CRYPTO_ACTION_LABELS[a]?.name || a).join('、')}
+
+
+
+ {/* 授权时长 */}
+
+ {t('duration')}:
+
+
+
+
+
+ {/* 手势密码区域 - 固定尺寸不可压缩 */}
+
+
{t('drawPatternToConfirm')}
+ {/* 固定尺寸容器,防止内容变化导致布局抖动 */}
+
+ {/* 验证中状态 - 固定高度 */}
+
+ {isVerifying && (
+
+
+ {t('verifying')}
+
+ )}
+
+
+
+ {/* Cancel button */}
+
+
+ {t('cancel')}
+
+
+
+ {/* Safe area */}
+
+
+
+ );
}
export const CryptoAuthorizeJob: ActivityComponentType = ({ params }) => {
- return (
-
-
-
- )
-}
+ return (
+
+
+
+ );
+};
diff --git a/src/stackflow/activities/sheets/MiniappDestroyConfirmJob.tsx b/src/stackflow/activities/sheets/MiniappDestroyConfirmJob.tsx
index 48ed2e7ab..91a0b0a5b 100644
--- a/src/stackflow/activities/sheets/MiniappDestroyConfirmJob.tsx
+++ b/src/stackflow/activities/sheets/MiniappDestroyConfirmJob.tsx
@@ -3,82 +3,78 @@
* 用于小程序请求销毁资产时显示
*/
-import { useState, useCallback } from 'react'
-import type { ActivityComponentType } from '@stackflow/react'
-import { BottomSheet } from '@/components/layout/bottom-sheet'
-import { useTranslation } from 'react-i18next'
-import { cn } from '@/lib/utils'
-import { IconFlame, IconAlertTriangle, IconLoader2 } from '@tabler/icons-react'
-import { useFlow } from '../../stackflow'
-import { ActivityParamsProvider, useActivityParams } from '../../hooks'
-import { setWalletLockConfirmCallback } from './WalletLockConfirmJob'
-import { useCurrentWallet, useChainConfigState, chainConfigSelectors, walletStore } from '@/stores'
-import { submitBioforestBurn } from '@/hooks/use-burn.bioforest'
-import { Amount } from '@/types/amount'
-import { AmountDisplay } from '@/components/common/amount-display'
-import { MiniappSheetHeader } from '@/components/ecosystem'
-import { ChainBadge } from '@/components/wallet/chain-icon'
-import { ChainAddressDisplay } from '@/components/wallet/chain-address-display'
+import { useState, useCallback } from 'react';
+import type { ActivityComponentType } from '@stackflow/react';
+import { BottomSheet } from '@/components/layout/bottom-sheet';
+import { useTranslation } from 'react-i18next';
+import { cn } from '@/lib/utils';
+import { IconFlame, IconAlertTriangle, IconLoader2 } from '@tabler/icons-react';
+import { useFlow } from '../../stackflow';
+import { ActivityParamsProvider, useActivityParams } from '../../hooks';
+import { setWalletLockConfirmCallback } from './WalletLockConfirmJob';
+import { useCurrentWallet, useChainConfigState, chainConfigSelectors, walletStore } from '@/stores';
+import { submitBioforestBurn } from '@/hooks/use-burn.bioforest';
+import { Amount } from '@/types/amount';
+import { AmountDisplay } from '@/components/common/amount-display';
+import { MiniappSheetHeader } from '@/components/ecosystem';
+import { ChainBadge } from '@/components/wallet/chain-icon';
+import { ChainAddressDisplay } from '@/components/wallet/chain-address-display';
type MiniappDestroyConfirmJobParams = {
/** 来源小程序名称 */
- appName: string
+ appName: string;
/** 来源小程序图标 */
- appIcon?: string
+ appIcon?: string;
/** 发送地址 */
- from: string
+ from: string;
/** 金额 */
- amount: string
+ amount: string;
/** 链 ID */
- chain: string
+ chain: string;
/** 资产类型 */
- asset: string
-}
+ asset: string;
+};
function MiniappDestroyConfirmJobContent() {
- const { t } = useTranslation(['common', 'transaction'])
- const { pop, push } = useFlow()
- const params = useActivityParams()
- const { appName, appIcon, from, amount, chain, asset } = params
- const currentWallet = useCurrentWallet()
- const chainConfigState = useChainConfigState()
+ const { t } = useTranslation(['common', 'transaction']);
+ const { pop, push } = useFlow();
+ const params = useActivityParams();
+ const { appName, appIcon, from, amount, chain, asset } = params;
+ const currentWallet = useCurrentWallet();
+ const chainConfigState = useChainConfigState();
const chainConfig = chainConfigState.snapshot
? chainConfigSelectors.getChainById(chainConfigState, chain as 'bfmeta')
- : null
+ : null;
- const [isConfirming, setIsConfirming] = useState(false)
+ const [isConfirming, setIsConfirming] = useState(false);
// 查找使用该地址的钱包
- const targetWallet = walletStore.state.wallets.find(
- w => w.chainAddresses.some(ca => ca.address === from)
- )
- const walletName = targetWallet?.name || t('common:unknownWallet', '未知钱包')
+ const targetWallet = walletStore.state.wallets.find((w) => w.chainAddresses.some((ca) => ca.address === from));
+ const walletName = targetWallet?.name || t('common:unknownWallet');
const handleConfirm = useCallback(() => {
- if (isConfirming) return
+ if (isConfirming) return;
// 设置钱包锁验证回调
setWalletLockConfirmCallback(async (password: string) => {
- setIsConfirming(true)
+ setIsConfirming(true);
try {
if (!currentWallet?.id || !chainConfig) {
-
- return false
+ return false;
}
// 获取 applyAddress
- const { fetchAssetApplyAddress } = await import('@/hooks/use-burn.bioforest')
- const applyAddress = await fetchAssetApplyAddress(chainConfig, asset, from)
+ const { fetchAssetApplyAddress } = await import('@/hooks/use-burn.bioforest');
+ const applyAddress = await fetchAssetApplyAddress(chainConfig, asset, from);
if (!applyAddress) {
-
- return false
+ return false;
}
// 执行销毁
- const amountObj = Amount.fromFormatted(amount, chainConfig.decimals, asset)
+ const amountObj = Amount.fromFormatted(amount, chainConfig.decimals, asset);
const result = await submitBioforestBurn({
chainConfig,
@@ -88,15 +84,14 @@ function MiniappDestroyConfirmJobContent() {
recipientAddress: applyAddress,
assetType: asset,
amount: amountObj,
- })
+ });
if (result.status === 'password') {
- return false
+ return false;
}
if (result.status === 'error') {
-
- return false
+ return false;
}
// 发送成功事件
@@ -105,32 +100,31 @@ function MiniappDestroyConfirmJobContent() {
confirmed: true,
txHash: result.status === 'ok' ? result.txHash : undefined,
},
- })
- window.dispatchEvent(event)
+ });
+ window.dispatchEvent(event);
- pop()
- return true
+ pop();
+ return true;
} catch (error) {
-
- return false
+ return false;
} finally {
- setIsConfirming(false)
+ setIsConfirming(false);
}
- })
+ });
// 打开钱包锁验证
push('WalletLockConfirmJob', {
title: t('transaction:destroyPage.title'),
- })
- }, [isConfirming, currentWallet, chainConfig, asset, from, amount, pop, push, t])
+ });
+ }, [isConfirming, currentWallet, chainConfig, asset, from, amount, pop, push, t]);
const handleCancel = useCallback(() => {
const event = new CustomEvent('miniapp-destroy-confirm', {
detail: { confirmed: false },
- })
- window.dispatchEvent(event)
- pop()
- }, [pop])
+ });
+ window.dispatchEvent(event);
+ pop();
+ }, [pop]);
return (
@@ -143,7 +137,7 @@ function MiniappDestroyConfirmJobContent() {
{/* Header */}
{/* Amount */}
{/* From address */}
-
+
-
- {t('common:from', '来自')}
-
+ {t('common:from')}
{/* Chain & Asset */}
-
-
- {t('common:network', '网络')}
-
+
+ {t('common:network')}
{/* Warning */}
-
-
-
- {t('transaction:destroyPage.warning')}
-
+
+
+
{t('transaction:destroyPage.warning')}
@@ -199,9 +180,9 @@ function MiniappDestroyConfirmJobContent() {
- {t('common:cancel', '取消')}
+ {t('common:cancel')}
{isConfirming ? (
<>
- {t('common:confirming', '确认中...')}
+ {t('common:confirming')}
>
) : (
<>
@@ -230,15 +211,13 @@ function MiniappDestroyConfirmJobContent() {
- )
+ );
}
-export const MiniappDestroyConfirmJob: ActivityComponentType
= ({
- params,
-}) => {
+export const MiniappDestroyConfirmJob: ActivityComponentType = ({ params }) => {
return (
- )
-}
+ );
+};
diff --git a/src/stackflow/activities/sheets/MiniappSignTransactionJob.tsx b/src/stackflow/activities/sheets/MiniappSignTransactionJob.tsx
index 771fad7fe..c59280c3f 100644
--- a/src/stackflow/activities/sheets/MiniappSignTransactionJob.tsx
+++ b/src/stackflow/activities/sheets/MiniappSignTransactionJob.tsx
@@ -3,81 +3,80 @@
* 用于小程序请求 `bio_signTransaction` 时显示
*/
-import { useState, useCallback, useMemo } from 'react'
-import type { ActivityComponentType } from '@stackflow/react'
-import { BottomSheet } from '@/components/layout/bottom-sheet'
-import { useTranslation } from 'react-i18next'
-import { cn } from '@/lib/utils'
-import { IconAlertTriangle, IconLoader2 } from '@tabler/icons-react'
-import { useFlow } from '../../stackflow'
-import { ActivityParamsProvider, useActivityParams } from '../../hooks'
-import { setWalletLockConfirmCallback } from './WalletLockConfirmJob'
-import { walletStore } from '@/stores'
-import type { UnsignedTransaction } from '@/services/ecosystem'
-import { signUnsignedTransaction } from '@/services/ecosystem/handlers'
-import { MiniappSheetHeader } from '@/components/ecosystem'
-import { ChainBadge } from '@/components/wallet/chain-icon'
-import { ChainAddressDisplay } from '@/components/wallet/chain-address-display'
+import { useState, useCallback, useMemo } from 'react';
+import type { ActivityComponentType } from '@stackflow/react';
+import { BottomSheet } from '@/components/layout/bottom-sheet';
+import { useTranslation } from 'react-i18next';
+import { cn } from '@/lib/utils';
+import { IconAlertTriangle, IconLoader2 } from '@tabler/icons-react';
+import { useFlow } from '../../stackflow';
+import { ActivityParamsProvider, useActivityParams } from '../../hooks';
+import { setWalletLockConfirmCallback } from './WalletLockConfirmJob';
+import { walletStore } from '@/stores';
+import type { UnsignedTransaction } from '@/services/ecosystem';
+import { signUnsignedTransaction } from '@/services/ecosystem/handlers';
+import { MiniappSheetHeader } from '@/components/ecosystem';
+import { ChainBadge } from '@/components/wallet/chain-icon';
+import { ChainAddressDisplay } from '@/components/wallet/chain-address-display';
type MiniappSignTransactionJobParams = {
/** 来源小程序名称 */
- appName: string
+ appName: string;
/** 来源小程序图标 */
- appIcon?: string
+ appIcon?: string;
/** 签名地址 */
- from: string
+ from: string;
/** 链 ID */
- chain: string
+ chain: string;
/** 未签名交易(JSON 字符串) */
- unsignedTx: string
-}
+ unsignedTx: string;
+};
function findWalletIdByAddress(chainId: string, address: string): string | null {
- const isHexLike = address.startsWith('0x')
- const normalized = isHexLike ? address.toLowerCase() : address
+ const isHexLike = address.startsWith('0x');
+ const normalized = isHexLike ? address.toLowerCase() : address;
for (const wallet of walletStore.state.wallets) {
const match = wallet.chainAddresses.find((ca) => {
- if (ca.chain !== chainId) return false
- if (isHexLike || ca.address.startsWith('0x')) return ca.address.toLowerCase() === normalized
- return ca.address === normalized
- })
- if (match) return wallet.id
+ if (ca.chain !== chainId) return false;
+ if (isHexLike || ca.address.startsWith('0x')) return ca.address.toLowerCase() === normalized;
+ return ca.address === normalized;
+ });
+ if (match) return wallet.id;
}
- return null
+ return null;
}
function MiniappSignTransactionJobContent() {
- const { t } = useTranslation('common')
- const { pop, push } = useFlow()
- const params = useActivityParams()
- const { appName, appIcon, from, chain, unsignedTx: unsignedTxJson } = params
+ const { t } = useTranslation('common');
+ const { pop, push } = useFlow();
+ const params = useActivityParams();
+ const { appName, appIcon, from, chain, unsignedTx: unsignedTxJson } = params;
- const [isSubmitting, setIsSubmitting] = useState(false)
+ const [isSubmitting, setIsSubmitting] = useState(false);
const unsignedTx = useMemo((): UnsignedTransaction | null => {
try {
- return JSON.parse(unsignedTxJson) as UnsignedTransaction
+ return JSON.parse(unsignedTxJson) as UnsignedTransaction;
} catch {
- return null
+ return null;
}
- }, [unsignedTxJson])
+ }, [unsignedTxJson]);
const walletId = useMemo(() => {
- return findWalletIdByAddress(chain, from)
- }, [chain, from])
+ return findWalletIdByAddress(chain, from);
+ }, [chain, from]);
- // 查找钱包名称
- const targetWallet = walletStore.state.wallets.find(w => w.id === walletId)
- const walletName = targetWallet?.name || t('unknownWallet', '未知钱包')
+ const targetWallet = walletStore.state.wallets.find((w) => w.id === walletId);
+ const walletName = targetWallet?.name || t('unknownWallet');
const handleConfirm = useCallback(() => {
- if (isSubmitting) return
- if (!unsignedTx) return
- if (!walletId) return
+ if (isSubmitting) return;
+ if (!unsignedTx) return;
+ if (!walletId) return;
setWalletLockConfirmCallback(async (password: string) => {
- setIsSubmitting(true)
+ setIsSubmitting(true);
try {
const signedTx = await signUnsignedTransaction({
walletId,
@@ -85,47 +84,46 @@ function MiniappSignTransactionJobContent() {
from,
chainId: chain,
unsignedTx,
- })
+ });
const event = new CustomEvent('miniapp-sign-transaction-confirm', {
detail: {
confirmed: true,
signedTx,
},
- })
- window.dispatchEvent(event)
+ });
+ window.dispatchEvent(event);
- pop()
- return true
+ pop();
+ return true;
} catch (error) {
-
- return false
+ return false;
} finally {
- setIsSubmitting(false)
+ setIsSubmitting(false);
}
- })
+ });
push('WalletLockConfirmJob', {
- title: t('signTransaction', '签名交易'),
- })
- }, [chain, from, isSubmitting, pop, push, t, unsignedTx, walletId])
+ title: t('signTransaction'),
+ });
+ }, [chain, from, isSubmitting, pop, push, t, unsignedTx, walletId]);
const handleCancel = useCallback(() => {
const event = new CustomEvent('miniapp-sign-transaction-confirm', {
detail: { confirmed: false },
- })
- window.dispatchEvent(event)
- pop()
- }, [pop])
+ });
+ window.dispatchEvent(event);
+ pop();
+ }, [pop]);
const rawPreview = useMemo(() => {
- if (!unsignedTx) return ''
+ if (!unsignedTx) return '';
try {
- return JSON.stringify(unsignedTx.data, null, 2)
+ return JSON.stringify(unsignedTx.data, null, 2);
} catch {
- return String(unsignedTx.data)
+ return String(unsignedTx.data);
}
- }, [unsignedTx])
+ }, [unsignedTx]);
return (
@@ -135,8 +133,8 @@ function MiniappSignTransactionJobContent() {
{!unsignedTx && (
-
- {t('invalidTransaction', '无效的交易数据')}
-
+ {t('invalidTransaction')}
)}
{unsignedTx && !walletId && (
- {t('signingAddressNotFound', '找不到对应的钱包地址,无法签名')}
+ {t('signingAddressNotFound')}
)}
-
{t('network', '网络')}
+
{t('network')}
-
{t('signingAddress', '签名地址')}
+
{t('signingAddress')}
-
{t('transaction', '交易内容')}
+
{t('transaction')}
-
{rawPreview}
+
{rawPreview}
-
-
- {t('signTxWarning', '请确认您信任此应用,并仔细核对交易内容。')}
-
+
+
{t('signTxWarning')}
@@ -188,9 +182,9 @@ function MiniappSignTransactionJobContent() {
- {t('cancel', '取消')}
+ {t('cancel')}
{isSubmitting ? (
<>
- {t('signing', '签名中...')}
+ {t('signing')}
>
) : (
- t('sign', '签名')
+ t('sign')
)}
@@ -215,7 +209,7 @@ function MiniappSignTransactionJobContent() {
- )
+ );
}
export const MiniappSignTransactionJob: ActivityComponentType
= ({ params }) => {
@@ -223,5 +217,5 @@ export const MiniappSignTransactionJob: ActivityComponentType
- )
-}
+ );
+};
diff --git a/src/stackflow/activities/sheets/MiniappTransferConfirmJob.tsx b/src/stackflow/activities/sheets/MiniappTransferConfirmJob.tsx
index aa9d9b115..b6da1d29b 100644
--- a/src/stackflow/activities/sheets/MiniappTransferConfirmJob.tsx
+++ b/src/stackflow/activities/sheets/MiniappTransferConfirmJob.tsx
@@ -3,98 +3,91 @@
* 用于小程序请求发送转账时显示
*/
-import { useState, useCallback } from 'react'
-import type { ActivityComponentType } from '@stackflow/react'
-import { BottomSheet } from '@/components/layout/bottom-sheet'
-import { useTranslation } from 'react-i18next'
-import { cn } from '@/lib/utils'
-import { IconArrowDown, IconAlertTriangle, IconLoader2 } from '@tabler/icons-react'
-import { useFlow } from '../../stackflow'
-import { ActivityParamsProvider, useActivityParams } from '../../hooks'
-import { setWalletLockConfirmCallback } from './WalletLockConfirmJob'
-import { useCurrentWallet, walletStore } from '@/stores'
-import { SignatureAuthService, plaocAdapter } from '@/services/authorize'
-import { AddressDisplay } from '@/components/wallet/address-display'
-import { AmountDisplay } from '@/components/common/amount-display'
-import { MiniappSheetHeader } from '@/components/ecosystem'
-import { ChainBadge } from '@/components/wallet/chain-icon'
+import { useState, useCallback } from 'react';
+import type { ActivityComponentType } from '@stackflow/react';
+import { BottomSheet } from '@/components/layout/bottom-sheet';
+import { useTranslation } from 'react-i18next';
+import { cn } from '@/lib/utils';
+import { IconArrowDown, IconAlertTriangle, IconLoader2 } from '@tabler/icons-react';
+import { useFlow } from '../../stackflow';
+import { ActivityParamsProvider, useActivityParams } from '../../hooks';
+import { setWalletLockConfirmCallback } from './WalletLockConfirmJob';
+import { useCurrentWallet, walletStore } from '@/stores';
+import { SignatureAuthService, plaocAdapter } from '@/services/authorize';
+import { AddressDisplay } from '@/components/wallet/address-display';
+import { AmountDisplay } from '@/components/common/amount-display';
+import { MiniappSheetHeader } from '@/components/ecosystem';
+import { ChainBadge } from '@/components/wallet/chain-icon';
type MiniappTransferConfirmJobParams = {
/** 来源小程序名称 */
- appName: string
+ appName: string;
/** 来源小程序图标 */
- appIcon?: string
+ appIcon?: string;
/** 发送地址 */
- from: string
+ from: string;
/** 接收地址 */
- to: string
+ to: string;
/** 金额 */
- amount: string
+ amount: string;
/** 链 ID */
- chain: string
+ chain: string;
/** 代币 (可选) */
- asset?: string
-}
+ asset?: string;
+};
function MiniappTransferConfirmJobContent() {
- const { t } = useTranslation('common')
- const { pop, push } = useFlow()
- const params = useActivityParams()
- const { appName, appIcon, from, to, amount, chain, asset } = params
- const currentWallet = useCurrentWallet()
+ const { t } = useTranslation('common');
+ const { pop, push } = useFlow();
+ const params = useActivityParams();
+ const { appName, appIcon, from, to, amount, chain, asset } = params;
+ const currentWallet = useCurrentWallet();
- const [isConfirming, setIsConfirming] = useState(false)
+ const [isConfirming, setIsConfirming] = useState(false);
// 查找使用该地址的钱包
- const targetWallet = walletStore.state.wallets.find(
- w => w.chainAddresses.some(ca => ca.address === from)
- )
- const walletName = targetWallet?.name || t('unknownWallet', '未知钱包')
+ const targetWallet = walletStore.state.wallets.find((w) => w.chainAddresses.some((ca) => ca.address === from));
+ const walletName = targetWallet?.name || t('unknownWallet');
const handleConfirm = useCallback(() => {
- if (isConfirming) return
+ if (isConfirming) return;
// 设置钱包锁验证回调
setWalletLockConfirmCallback(async (password: string) => {
- setIsConfirming(true)
+ setIsConfirming(true);
try {
- const encryptedSecret = currentWallet?.encryptedMnemonic
+ const encryptedSecret = currentWallet?.encryptedMnemonic;
if (!encryptedSecret) {
-
- return false
+ return false;
}
// 创建签名服务
- const eventId = `miniapp_transfer_${Date.now()}`
- const authService = new SignatureAuthService(plaocAdapter, eventId)
+ const eventId = `miniapp_transfer_${Date.now()}`;
+ const authService = new SignatureAuthService(plaocAdapter, eventId);
// 执行转账签名
const transferPayload: {
- chainName: string
- senderAddress: string
- receiveAddress: string
- balance: string
- assetType?: string
+ chainName: string;
+ senderAddress: string;
+ receiveAddress: string;
+ balance: string;
+ assetType?: string;
} = {
chainName: chain,
senderAddress: from,
receiveAddress: to,
balance: amount,
- }
+ };
if (asset) {
- transferPayload.assetType = asset
+ transferPayload.assetType = asset;
}
- const signature = await authService.handleTransferSign(
- transferPayload,
- encryptedSecret,
- password
- )
+ const signature = await authService.handleTransferSign(transferPayload, encryptedSecret, password);
// TODO: 广播交易到链上 (需要调用 chain adapter 的 broadcastTransaction)
// 目前先返回签名作为 txHash 的占位符
- const txHash = signature
+ const txHash = signature;
// 发送成功事件
const event = new CustomEvent('miniapp-transfer-confirm', {
@@ -102,34 +95,33 @@ function MiniappTransferConfirmJobContent() {
confirmed: true,
txHash,
},
- })
- window.dispatchEvent(event)
+ });
+ window.dispatchEvent(event);
- pop()
- return true
+ pop();
+ return true;
} catch (error) {
-
- return false
+ return false;
} finally {
- setIsConfirming(false)
+ setIsConfirming(false);
}
- })
+ });
// 打开钱包锁验证
push('WalletLockConfirmJob', {
- title: t('confirmTransfer', '确认转账'),
- })
- }, [isConfirming, currentWallet, chain, from, to, amount, asset, pop, push, t])
+ title: t('confirmTransfer'),
+ });
+ }, [isConfirming, currentWallet, chain, from, to, amount, asset, pop, push, t]);
const handleCancel = useCallback(() => {
const event = new CustomEvent('miniapp-transfer-confirm', {
detail: { confirmed: false },
- })
- window.dispatchEvent(event)
- pop()
- }, [pop])
+ });
+ window.dispatchEvent(event);
+ pop();
+ }, [pop]);
- const displayAsset = asset || chain.toUpperCase()
+ const displayAsset = asset || chain.toUpperCase();
return (
@@ -141,8 +133,8 @@ function MiniappTransferConfirmJobContent() {
{/* Header */}
{/* From -> To */}
-
+
{/* From */}
-
- {t('from', '来自')}
-
+
{t('from')}
{/* Arrow */}
-
+
{/* To */}
-
- {t('to', '接收')}
-
+
{t('to')}
{/* Chain */}
-
-
- {t('network', '网络')}
-
+
+ {t('network')}
{/* Warning */}
-
-
- {t('transferWarning', '请仔细核对收款地址和金额,转账后无法撤回。')}
-
+
+
{t('transferWarning')}
@@ -212,9 +196,9 @@ function MiniappTransferConfirmJobContent() {
- {t('cancel', '取消')}
+ {t('cancel')}
{isConfirming ? (
<>
- {t('confirming', '确认中...')}
+ {t('confirming')}
>
) : (
- t('confirm', '确认')
+ t('confirm')
)}
@@ -240,15 +224,13 @@ function MiniappTransferConfirmJobContent() {
- )
+ );
}
-export const MiniappTransferConfirmJob: ActivityComponentType
= ({
- params,
-}) => {
+export const MiniappTransferConfirmJob: ActivityComponentType = ({ params }) => {
return (
- )
-}
+ );
+};
diff --git a/src/stackflow/activities/sheets/MnemonicOptionsJob.tsx b/src/stackflow/activities/sheets/MnemonicOptionsJob.tsx
index e8aa23626..1b43c6157 100644
--- a/src/stackflow/activities/sheets/MnemonicOptionsJob.tsx
+++ b/src/stackflow/activities/sheets/MnemonicOptionsJob.tsx
@@ -1,13 +1,13 @@
-import { useState } from "react";
-import type { ActivityComponentType } from "@stackflow/react";
-import { BottomSheet } from "@/components/layout/bottom-sheet";
-import { useTranslation } from "react-i18next";
-import { cn } from "@/lib/utils";
-import { IconCheck as Check } from "@tabler/icons-react";
-import { useFlow } from "../../stackflow";
-import { ActivityParamsProvider, useActivityParams } from "../../hooks";
-
-type MnemonicLanguage = "english" | "zh-Hans" | "zh-Hant";
+import { useState } from 'react';
+import type { ActivityComponentType } from '@stackflow/react';
+import { BottomSheet } from '@/components/layout/bottom-sheet';
+import { useTranslation } from 'react-i18next';
+import { cn } from '@/lib/utils';
+import { IconCheck as Check } from '@tabler/icons-react';
+import { useFlow } from '../../stackflow';
+import { ActivityParamsProvider, useActivityParams } from '../../hooks';
+
+type MnemonicLanguage = 'english' | 'zh-Hans' | 'zh-Hant';
type MnemonicLength = 12 | 15 | 18 | 21 | 24 | 36;
interface MnemonicOptions {
@@ -32,20 +32,20 @@ type MnemonicOptionsJobParams = {
};
const LANGUAGE_OPTIONS: { value: MnemonicLanguage; label: string }[] = [
- { value: "english", label: "English" },
- { value: "zh-Hans", label: "中文(简体)" },
- { value: "zh-Hant", label: "中文(繁體)" },
+ { value: 'english', label: 'English' },
+ { value: 'zh-Hans', label: '中文(简体)' }, // i18n-ignore: native language name
+ { value: 'zh-Hant', label: '中文(繁體)' }, // i18n-ignore: native language name
];
const LENGTH_OPTIONS: MnemonicLength[] = [12, 15, 18, 21, 24, 36];
function MnemonicOptionsJobContent() {
- const { t } = useTranslation("onboarding");
+ const { t } = useTranslation('onboarding');
const { pop } = useFlow();
const { language: initialLanguage, length: initialLength } = useActivityParams();
const [options, setOptions] = useState({
- language: (initialLanguage as MnemonicLanguage) || "english",
+ language: (initialLanguage as MnemonicLanguage) || 'english',
length: (Number(initialLength) as MnemonicLength) || 12,
});
@@ -70,19 +70,19 @@ function MnemonicOptionsJobContent() {
{/* Handle */}
{/* Title */}
-
-
{t("create.mnemonicOptions.title")}
+
+
{t('create.mnemonicOptions.title')}
{/* Content */}
{/* Language selection */}
-
{t("create.mnemonicOptions.language")}
+
{t('create.mnemonicOptions.language')}
{LANGUAGE_OPTIONS.map((option) => (
handleLanguageSelect(option.value)}
className={cn(
- "flex w-full items-center justify-between rounded-lg px-4 py-3",
- "transition-colors hover:bg-muted/50",
- options.language === option.value && "bg-muted"
+ 'flex w-full items-center justify-between rounded-lg px-4 py-3',
+ 'hover:bg-muted/50 transition-colors',
+ options.language === option.value && 'bg-muted',
)}
>
{option.label}
- {options.language === option.value && }
+ {options.language === option.value && }
))}
@@ -106,7 +106,7 @@ function MnemonicOptionsJobContent() {
{/* Length selection */}
-
{t("create.mnemonicOptions.wordCount")}
+
{t('create.mnemonicOptions.wordCount')}
{LENGTH_OPTIONS.map((length) => (
handleLengthSelect(length)}
className={cn(
- "flex items-center justify-center rounded-lg py-3 text-sm font-medium",
- "transition-colors",
- options.length === length ? "bg-primary text-primary-foreground" : "bg-muted hover:bg-muted/80"
+ 'flex items-center justify-center rounded-lg py-3 text-sm font-medium',
+ 'transition-colors',
+ options.length === length ? 'bg-primary text-primary-foreground' : 'bg-muted hover:bg-muted/80',
)}
>
- {length} {t("create.mnemonicOptions.words")}
+ {length} {t('create.mnemonicOptions.words')}
))}
@@ -132,11 +132,11 @@ function MnemonicOptionsJobContent() {
type="button"
onClick={handleConfirm}
className={cn(
- "w-full rounded-full py-3 font-medium text-primary-foreground transition-colors",
- "bg-primary hover:bg-primary/90"
+ 'text-primary-foreground w-full rounded-full py-3 font-medium transition-colors',
+ 'bg-primary hover:bg-primary/90',
)}
>
- {t("create.mnemonicOptions.confirm")}
+ {t('create.mnemonicOptions.confirm')}
diff --git a/src/stackflow/activities/sheets/PermissionRequestJob.tsx b/src/stackflow/activities/sheets/PermissionRequestJob.tsx
index 3031a9088..771f474d0 100644
--- a/src/stackflow/activities/sheets/PermissionRequestJob.tsx
+++ b/src/stackflow/activities/sheets/PermissionRequestJob.tsx
@@ -3,10 +3,10 @@
* 用于小程序首次请求权限时显示
*/
-import type { ActivityComponentType } from '@stackflow/react'
-import { BottomSheet } from '@/components/layout/bottom-sheet'
-import { useTranslation } from 'react-i18next'
-import { cn } from '@/lib/utils'
+import type { ActivityComponentType } from '@stackflow/react';
+import { BottomSheet } from '@/components/layout/bottom-sheet';
+import { useTranslation } from 'react-i18next';
+import { cn } from '@/lib/utils';
import {
IconWallet,
IconSignature,
@@ -14,81 +14,66 @@ import {
IconFileText,
IconShieldCheck,
IconApps,
-} from '@tabler/icons-react'
-import { useFlow } from '../../stackflow'
-import { ActivityParamsProvider, useActivityParams } from '../../hooks'
+} from '@tabler/icons-react';
+import { useFlow } from '../../stackflow';
+import { ActivityParamsProvider, useActivityParams } from '../../hooks';
type PermissionRequestJobParams = {
/** 小程序名称 */
- appName: string
+ appName: string;
/** 小程序图标 */
- appIcon?: string
+ appIcon?: string;
/** 请求的权限列表 (JSON 字符串) */
- permissions: string
-}
+ permissions: string;
+};
-const PERMISSION_INFO: Record
= {
- bio_requestAccounts: {
- icon: IconWallet,
- label: '查看账户',
- description: '查看您的钱包地址',
- },
- bio_createTransaction: {
- icon: IconFileText,
- label: '创建交易',
- description: '构造未签名交易(不做签名/不做广播)',
- },
- bio_signMessage: {
- icon: IconSignature,
- label: '签名消息',
- description: '请求签名消息(需要您确认)',
- },
- bio_signTypedData: {
- icon: IconSignature,
- label: '签名数据',
- description: '请求签名结构化数据(需要您确认)',
- },
- bio_signTransaction: {
- icon: IconSignature,
- label: '签名交易',
- description: '请求签名交易(需要您确认)',
- },
- bio_sendTransaction: {
- icon: IconCurrencyDollar,
- label: '发送交易',
- description: '请求发送转账(需要您确认)',
- },
-}
+const PERMISSION_ICONS: Record = {
+ bio_requestAccounts: IconWallet,
+ bio_createTransaction: IconFileText,
+ bio_signMessage: IconSignature,
+ bio_signTypedData: IconSignature,
+ bio_signTransaction: IconSignature,
+ bio_sendTransaction: IconCurrencyDollar,
+};
function PermissionRequestJobContent() {
- const { t } = useTranslation('common')
- const { pop } = useFlow()
- const { appName, appIcon, permissions: permissionsJson } = useActivityParams()
+ const { t } = useTranslation('permission');
+ const { pop } = useFlow();
+ const { appName, appIcon, permissions: permissionsJson } = useActivityParams();
- // 解析权限列表
const permissions: string[] = (() => {
try {
- return JSON.parse(permissionsJson) as string[]
+ return JSON.parse(permissionsJson) as string[];
} catch {
- return []
+ return [];
}
- })()
+ })();
const handleApprove = () => {
const event = new CustomEvent('permission-request', {
detail: { approved: true, permissions },
- })
- window.dispatchEvent(event)
- pop()
- }
+ });
+ window.dispatchEvent(event);
+ pop();
+ };
const handleReject = () => {
const event = new CustomEvent('permission-request', {
detail: { approved: false },
- })
- window.dispatchEvent(event)
- pop()
- }
+ });
+ window.dispatchEvent(event);
+ pop();
+ };
+
+ const getPermissionLabel = (permKey: string): string => {
+ const labelKey = `${permKey}.label` as const;
+ return t(labelKey as any) as string;
+ };
+
+ const getPermissionDescription = (permKey: string): string => {
+ const descKey = `${permKey}.description` as const;
+ return t(descKey as any) as string;
+ };
return (
@@ -100,7 +85,7 @@ function PermissionRequestJobContent() {
{/* Header */}
-
+
{appIcon ? (

) : (
@@ -108,40 +93,34 @@ function PermissionRequestJobContent() {
)}
{appName}
-
- {t('requestsPermissions', '请求以下权限')}
-
+
{t('requestsPermissions' as any)}
{/* Permissions List */}
{permissions.map((permission) => {
- const info = PERMISSION_INFO[permission]
- if (!info) return null
+ const Icon = PERMISSION_ICONS[permission];
+ if (!Icon) return null;
- const Icon = info.icon
return (
-
+
-
{info.label}
-
{info.description}
+
{getPermissionLabel(permission)}
+
{getPermissionDescription(permission)}
- )
+ );
})}
{/* Trust indicator */}
-
+
- {t('permissionNote', '敏感操作需要您的确认')}
+ {t('permissionNote' as any)}
@@ -151,16 +130,16 @@ function PermissionRequestJobContent() {
onClick={handleReject}
className="bg-muted hover:bg-muted/80 flex-1 rounded-xl py-3 font-medium transition-colors"
>
- {t('reject', '拒绝')}
+ {t('reject')}
- {t('approve', '允许')}
+ {t('approve')}
@@ -168,7 +147,7 @@ function PermissionRequestJobContent() {
- )
+ );
}
export const PermissionRequestJob: ActivityComponentType
= ({ params }) => {
@@ -176,5 +155,5 @@ export const PermissionRequestJob: ActivityComponentType
- )
-}
+ );
+};
diff --git a/src/stackflow/activities/sheets/SigningConfirmJob.tsx b/src/stackflow/activities/sheets/SigningConfirmJob.tsx
index e60cebe5b..1702531d0 100644
--- a/src/stackflow/activities/sheets/SigningConfirmJob.tsx
+++ b/src/stackflow/activities/sheets/SigningConfirmJob.tsx
@@ -3,62 +3,59 @@
* 用于小程序请求用户签名消息
*/
-import { useCallback, useState } from 'react'
-import type { ActivityComponentType } from '@stackflow/react'
-import { BottomSheet } from '@/components/layout/bottom-sheet'
-import { useTranslation } from 'react-i18next'
-import { cn } from '@/lib/utils'
-import { IconAlertTriangle, IconLoader2 } from '@tabler/icons-react'
-import { useFlow } from '../../stackflow'
-import { ActivityParamsProvider, useActivityParams } from '../../hooks'
-import { setWalletLockConfirmCallback } from './WalletLockConfirmJob'
-import { useCurrentWallet, walletStore } from '@/stores'
-import { SignatureAuthService, plaocAdapter } from '@/services/authorize'
-import { MiniappSheetHeader } from '@/components/ecosystem'
+import { useCallback, useState } from 'react';
+import type { ActivityComponentType } from '@stackflow/react';
+import { BottomSheet } from '@/components/layout/bottom-sheet';
+import { useTranslation } from 'react-i18next';
+import { cn } from '@/lib/utils';
+import { IconAlertTriangle, IconLoader2 } from '@tabler/icons-react';
+import { useFlow } from '../../stackflow';
+import { ActivityParamsProvider, useActivityParams } from '../../hooks';
+import { setWalletLockConfirmCallback } from './WalletLockConfirmJob';
+import { useCurrentWallet, walletStore } from '@/stores';
+import { SignatureAuthService, plaocAdapter } from '@/services/authorize';
+import { MiniappSheetHeader } from '@/components/ecosystem';
type SigningConfirmJobParams = {
/** 要签名的消息 */
- message: string
+ message: string;
/** 签名地址 */
- address: string
+ address: string;
/** 请求来源小程序名称 */
- appName?: string
+ appName?: string;
/** 请求来源小程序图标 */
- appIcon?: string
+ appIcon?: string;
/** 链名称(用于签名) */
- chainName?: string
-}
+ chainName?: string;
+};
function SigningConfirmJobContent() {
- const { t } = useTranslation('common')
- const { pop, push } = useFlow()
- const { message, address, appName, appIcon, chainName } = useActivityParams()
- const currentWallet = useCurrentWallet()
- const [isSubmitting, setIsSubmitting] = useState(false)
+ const { t } = useTranslation('common');
+ const { pop, push } = useFlow();
+ const { message, address, appName, appIcon, chainName } = useActivityParams();
+ const currentWallet = useCurrentWallet();
+ const [isSubmitting, setIsSubmitting] = useState(false);
// 查找使用该地址的钱包
- const targetWallet = walletStore.state.wallets.find(
- w => w.chainAddresses.some(ca => ca.address === address)
- )
- const walletName = targetWallet?.name || t('unknownWallet', '未知钱包')
+ const targetWallet = walletStore.state.wallets.find((w) => w.chainAddresses.some((ca) => ca.address === address));
+ const walletName = targetWallet?.name || t('unknownWallet');
const handleConfirm = useCallback(() => {
- if (isSubmitting) return
+ if (isSubmitting) return;
// 设置钱包锁验证回调
setWalletLockConfirmCallback(async (password: string) => {
- setIsSubmitting(true)
+ setIsSubmitting(true);
try {
- const encryptedSecret = currentWallet?.encryptedMnemonic
+ const encryptedSecret = currentWallet?.encryptedMnemonic;
if (!encryptedSecret) {
-
- return false
+ return false;
}
// 创建签名服务 (使用临时 eventId)
- const eventId = `miniapp_sign_${Date.now()}`
- const authService = new SignatureAuthService(plaocAdapter, eventId)
+ const eventId = `miniapp_sign_${Date.now()}`;
+ const authService = new SignatureAuthService(plaocAdapter, eventId);
// 执行真实签名(返回 { signature, publicKey })
const signResult = await authService.handleMessageSign(
@@ -68,8 +65,8 @@ function SigningConfirmJobContent() {
message,
},
encryptedSecret,
- password
- )
+ password,
+ );
// 发送成功事件(包含 signature 和 publicKey)
const event = new CustomEvent('signing-confirm', {
@@ -78,35 +75,34 @@ function SigningConfirmJobContent() {
signature: signResult.signature,
publicKey: signResult.publicKey,
},
- })
- window.dispatchEvent(event)
+ });
+ window.dispatchEvent(event);
- pop()
- return true
+ pop();
+ return true;
} catch (error) {
-
- return false
+ return false;
} finally {
- setIsSubmitting(false)
+ setIsSubmitting(false);
}
- })
+ });
// 打开钱包锁验证
push('WalletLockConfirmJob', {
- title: t('sign', '签名'),
- })
- }, [isSubmitting, currentWallet, chainName, address, message, pop, push, t])
+ title: t('sign'),
+ });
+ }, [isSubmitting, currentWallet, chainName, address, message, pop, push, t]);
const handleCancel = useCallback(() => {
const event = new CustomEvent('signing-confirm', {
detail: { confirmed: false },
- })
- window.dispatchEvent(event)
- pop()
- }, [pop])
+ });
+ window.dispatchEvent(event);
+ pop();
+ }, [pop]);
// Check if message looks like hex data (potential risk)
- const isHexData = message.startsWith('0x') && /^0x[0-9a-fA-F]+$/.test(message)
+ const isHexData = message.startsWith('0x') && /^0x[0-9a-fA-F]+$/.test(message);
return (
@@ -118,8 +114,8 @@ function SigningConfirmJobContent() {
{/* Header */}
{/* Message */}
-
- {t('message', '消息内容')}
-
+
{t('message')}
-
- {message}
-
+
{message}
@@ -147,9 +139,7 @@ function SigningConfirmJobContent() {
{isHexData && (
-
- {t('hexDataWarning', '此消息包含十六进制数据,请确认您信任此应用。')}
-
+
{t('hexDataWarning')}
)}
@@ -159,9 +149,9 @@ function SigningConfirmJobContent() {
- {t('cancel', '取消')}
+ {t('cancel')}
{isSubmitting ? (
<>
- {t('signing', '签名中...')}
+ {t('signing')}
>
) : (
- t('sign', '签名')
+ t('sign')
)}
@@ -187,7 +177,7 @@ function SigningConfirmJobContent() {
- )
+ );
}
export const SigningConfirmJob: ActivityComponentType
= ({ params }) => {
@@ -195,5 +185,5 @@ export const SigningConfirmJob: ActivityComponentType =
- )
-}
+ );
+};
diff --git a/src/stackflow/activities/sheets/WalletPickerJob.tsx b/src/stackflow/activities/sheets/WalletPickerJob.tsx
index 90d0a92e3..27a3cb4e9 100644
--- a/src/stackflow/activities/sheets/WalletPickerJob.tsx
+++ b/src/stackflow/activities/sheets/WalletPickerJob.tsx
@@ -3,30 +3,30 @@
* 用于小程序请求用户选择钱包
*/
-import { useMemo } from 'react'
-import type { ActivityComponentType } from '@stackflow/react'
-import { BottomSheet } from '@/components/layout/bottom-sheet'
-import { useTranslation } from 'react-i18next'
-import { useStore } from '@tanstack/react-store'
-import { walletStore, walletSelectors, type Wallet, type ChainAddress } from '@/stores'
-import { useFlow } from '../../stackflow'
-import { ActivityParamsProvider, useActivityParams } from '../../hooks'
-import { WalletList, type WalletListItem } from '@/components/wallet/wallet-list'
-import { MiniappSheetHeader } from '@/components/ecosystem'
-import { getKeyAppChainId, normalizeChainId } from '@biochain/bio-sdk'
-import { useChainConfigs } from '@/stores/chain-config'
-import { chainConfigService } from '@/services/chain-config'
+import { useMemo } from 'react';
+import type { ActivityComponentType } from '@stackflow/react';
+import { BottomSheet } from '@/components/layout/bottom-sheet';
+import { useTranslation } from 'react-i18next';
+import { useStore } from '@tanstack/react-store';
+import { walletStore, walletSelectors, type Wallet, type ChainAddress } from '@/stores';
+import { useFlow } from '../../stackflow';
+import { ActivityParamsProvider, useActivityParams } from '../../hooks';
+import { WalletList, type WalletListItem } from '@/components/wallet/wallet-list';
+import { MiniappSheetHeader } from '@/components/ecosystem';
+import { getKeyAppChainId, normalizeChainId } from '@biochain/bio-sdk';
+import { useChainConfigs } from '@/stores/chain-config';
+import { chainConfigService } from '@/services/chain-config';
type WalletPickerJobParams = {
/** 限定链类型 (支持: KeyApp 内部 ID, EVM hex chainId, API 名称如 BSC) */
- chain?: string
+ chain?: string;
/** 排除的地址(不显示在列表中) */
- exclude?: string
+ exclude?: string;
/** 请求来源小程序名称 */
- appName?: string
+ appName?: string;
/** 请求来源小程序图标 */
- appIcon?: string
-}
+ appIcon?: string;
+};
/**
* 将任意链标识符转换为 KeyApp 内部 ID
@@ -36,63 +36,58 @@ type WalletPickerJobParams = {
* - 已有的 KeyApp ID: 'binance' -> 'binance'
*/
function resolveChainId(chain: string | undefined): string | undefined {
- if (!chain) return undefined
+ if (!chain) return undefined;
// Try EVM hex chainId first (e.g., '0x38')
if (chain.startsWith('0x')) {
- const keyAppId = getKeyAppChainId(chain)
- if (keyAppId) return keyAppId
+ const keyAppId = getKeyAppChainId(chain);
+ if (keyAppId) return keyAppId;
}
// Try API name normalization (e.g., 'BSC' -> 'binance', 'BFMCHAIN' -> 'bfmeta')
- const normalized = normalizeChainId(chain)
+ const normalized = normalizeChainId(chain);
// Check if it's a known chain (using chainConfigService)
if (chainConfigService.getConfig(normalized)) {
- return normalized
+ return normalized;
}
// Return as-is (might be already a KeyApp ID like 'binance')
- return normalized
+ return normalized;
}
// Utility function for chain display names (currently unused - using direct chain IDs)
// function __getChainDisplayName(chainId: string): string { return CHAIN_DISPLAY_NAMES[chainId] || chainId }
function WalletPickerJobContent() {
- const { t } = useTranslation('common')
- const { pop } = useFlow()
- const { chain: rawChain, exclude, appName, appIcon } = useActivityParams()
- const chainConfigs = useChainConfigs()
+ const { t } = useTranslation('common');
+ const { pop } = useFlow();
+ const { chain: rawChain, exclude, appName, appIcon } = useActivityParams();
+ const chainConfigs = useChainConfigs();
// Resolve chain to KeyApp internal ID
- const chain = useMemo(() => resolveChainId(rawChain), [rawChain])
+ const chain = useMemo(() => resolveChainId(rawChain), [rawChain]);
// 获取链配置(用于显示名称和图标)
- const chainConfig = useMemo(
- () => chain ? chainConfigs.find(c => c.id === chain) : null,
- [chain, chainConfigs]
- )
- const chainDisplayName = chain ? chainConfigService.getName(chain) : undefined
+ const chainConfig = useMemo(() => (chain ? chainConfigs.find((c) => c.id === chain) : null), [chain, chainConfigs]);
+ const chainDisplayName = chain ? chainConfigService.getName(chain) : undefined;
- const walletState = useStore(walletStore)
- const currentWallet = walletSelectors.getCurrentWallet(walletState)
+ const walletState = useStore(walletStore);
+ const currentWallet = walletSelectors.getCurrentWallet(walletState);
// 转换钱包数据为 WalletListItem 格式,并过滤排除的地址
const walletItems = useMemo((): WalletListItem[] => {
- const excludeLower = exclude?.toLowerCase()
- const items: WalletListItem[] = []
+ const excludeLower = exclude?.toLowerCase();
+ const items: WalletListItem[] = [];
for (const wallet of walletState.wallets) {
- const chainAddress = chain
- ? wallet.chainAddresses.find((ca) => ca.chain === chain)
- : wallet.chainAddresses[0]
+ const chainAddress = chain ? wallet.chainAddresses.find((ca) => ca.chain === chain) : wallet.chainAddresses[0];
- if (!chainAddress) continue
+ if (!chainAddress) continue;
// 过滤排除的地址
if (excludeLower && chainAddress.address.toLowerCase() === excludeLower) {
- continue
+ continue;
}
items.push({
@@ -101,29 +96,27 @@ function WalletPickerJobContent() {
address: chainAddress.address,
themeHue: wallet.themeHue,
chainIconUrl: chainConfig?.icon,
- })
+ });
}
- return items
- }, [walletState.wallets, chain, exclude])
+ return items;
+ }, [walletState.wallets, chain, exclude]);
// 保存钱包到链地址的映射
const walletChainMap = useMemo(() => {
- const map = new Map()
+ const map = new Map();
walletState.wallets.forEach((wallet) => {
- const chainAddress = chain
- ? wallet.chainAddresses.find((ca) => ca.chain === chain)
- : wallet.chainAddresses[0]
+ const chainAddress = chain ? wallet.chainAddresses.find((ca) => ca.chain === chain) : wallet.chainAddresses[0];
if (chainAddress) {
- map.set(wallet.id, { wallet, chainAddress })
+ map.set(wallet.id, { wallet, chainAddress });
}
- })
- return map
- }, [walletState.wallets, chain])
+ });
+ return map;
+ }, [walletState.wallets, chain]);
const handleSelect = (walletId: string) => {
- const data = walletChainMap.get(walletId)
- if (!data) return
+ const data = walletChainMap.get(walletId);
+ if (!data) return;
const event = new CustomEvent('wallet-picker-select', {
detail: {
@@ -132,16 +125,16 @@ function WalletPickerJobContent() {
name: data.wallet.name,
publicKey: data.chainAddress.publicKey,
},
- })
- window.dispatchEvent(event)
- pop()
- }
+ });
+ window.dispatchEvent(event);
+ pop();
+ };
const handleCancel = () => {
- const event = new CustomEvent('wallet-picker-cancel')
- window.dispatchEvent(event)
- pop()
- }
+ const event = new CustomEvent('wallet-picker-cancel');
+ window.dispatchEvent(event);
+ pop();
+ };
return (
@@ -153,8 +146,8 @@ function WalletPickerJobContent() {
{/* Title with App Icon */}
{walletItems.length === 0 ? (
- {chain
- ? t('noWalletsForChain', '暂无支持 {{chain}} 的钱包', { chain })
- : t('noWallets', '暂无钱包')}
+ {chain ? t('noWalletsForChain', { chain }) : t('noWallets')}
) : (
- {t('cancel', '取消')}
+ {t('cancel')}
@@ -192,7 +183,7 @@ function WalletPickerJobContent() {
- )
+ );
}
export const WalletPickerJob: ActivityComponentType = ({ params }) => {
@@ -200,5 +191,5 @@ export const WalletPickerJob: ActivityComponentType = ({
- )
-}
+ );
+};
diff --git a/src/stores/ecosystem.ts b/src/stores/ecosystem.ts
index 6f2155ee1..ea3094778 100644
--- a/src/stores/ecosystem.ts
+++ b/src/stores/ecosystem.ts
@@ -84,7 +84,7 @@ function loadState(): EcosystemState {
sources: parsed.sources ?? [
{
url: `${import.meta.env.BASE_URL}miniapps/ecosystem.json`,
- name: 'Bio 官方生态',
+ name: 'Bio 官方生态', // i18n-ignore: config data
lastUpdated: new Date().toISOString(),
enabled: true,
},
@@ -104,7 +104,7 @@ function loadState(): EcosystemState {
sources: [
{
url: `${import.meta.env.BASE_URL}miniapps/ecosystem.json`,
- name: 'Bio 官方生态',
+ name: 'Bio 官方生态', // i18n-ignore: config data
lastUpdated: new Date().toISOString(),
enabled: true,
},
@@ -303,9 +303,11 @@ export const ecosystemActions = {
ecosystemStore.setState((state) => {
const next = subPages.length > 0 ? subPages : DEFAULT_AVAILABLE_SUBPAGES;
if (arraysEqual(state.availableSubPages, next)) return state;
+ const activeSubPage = next.includes(state.activeSubPage) ? state.activeSubPage : (next[0] ?? 'mine');
return {
...state,
availableSubPages: next,
+ activeSubPage,
};
});
},