diff --git a/.oxlintrc.json b/.oxlintrc.json index d6c2c4133..0a80a84ca 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -71,7 +71,8 @@ "exclude": [ "Trans", "Icon", - "TablerIcon" + "TablerIcon", + "code" ] }, "jsx-attributes": { @@ -92,6 +93,7 @@ "data-state", "data-side", "data-align", + "testId", "to", "href", "src", @@ -297,7 +299,13 @@ "^loading$", "loading...", "^--$", - "^≈ --$" + "^≈ --$", + "^/help$", + "^/clear$", + "^/vars$", + "^/copy$", + "^\\$[\\w{}]+$", + "^✕$" ] } } @@ -329,8 +337,7 @@ "**/*.stories.ts", "src/test/**", "scripts/**", - "e2e/**", - "src/services/bioforest-sdk/bioforest-chain-bundle.*" + "e2e/**" ], "settings": { "react": { diff --git a/miniapps/forge/scripts/e2e.ts b/miniapps/forge/scripts/e2e.ts index 46092d1d0..6bae299ce 100644 --- a/miniapps/forge/scripts/e2e.ts +++ b/miniapps/forge/scripts/e2e.ts @@ -26,7 +26,7 @@ async function main() { const updateSnapshots = args.has('--update-snapshots') || args.has('-u') const port = await findAvailablePort(5184) - console.log(`[e2e] Using port ${port}`) + process.stdout.write(`[e2e] Using port ${port}\n`) // Start vite dev server const vite = spawn('pnpm', ['vite', '--port', String(port)], { @@ -48,23 +48,30 @@ async function main() { }) vite.stderr?.on('data', (data) => { - console.error(data.toString()) + process.stderr.write(data.toString()) }) // Wait for server to be ready const maxWait = 30000 const startTime = Date.now() - while (!serverReady && Date.now() - startTime < maxWait) { - await new Promise(r => setTimeout(r, 200)) - } + await new Promise((resolve) => { + const checkReady = () => { + if (serverReady || Date.now() - startTime >= maxWait) { + resolve() + return + } + setTimeout(checkReady, 200) + } + checkReady() + }) if (!serverReady) { - console.error('[e2e] Server failed to start') + process.stderr.write('[e2e] Server failed to start\n') vite.kill() process.exit(1) } - console.log('[e2e] Server ready, running tests...') + process.stdout.write('[e2e] Server ready, running tests...\n') // Run playwright const playwrightArgs = ['playwright', 'test'] diff --git a/miniapps/forge/src/App.tsx b/miniapps/forge/src/App.tsx index dc4b7a03e..5001a6f10 100644 --- a/miniapps/forge/src/App.tsx +++ b/miniapps/forge/src/App.tsx @@ -353,7 +353,7 @@ export default function App() { {/* Redemption Mode */} {mode === 'redemption' && config && !configLoading && ( - {}} /> + {}} /> )} {/* Recharge Mode */} @@ -429,7 +429,10 @@ export default function App() { - {t('forge.pay')} ({getChainName(selectedOption.externalChain)}) + {t('common.chainLabel', { + label: t('forge.pay'), + chain: getChainName(selectedOption.externalChain), + })} @@ -477,7 +480,10 @@ export default function App() { - {t('forge.receive')} ({getChainName(selectedOption.internalChain)}) + {t('common.chainLabel', { + label: t('forge.receive'), + chain: getChainName(selectedOption.internalChain), + })} @@ -502,7 +508,7 @@ export default function App() {
{t('forge.ratio')} - 1:1 + {t('forge.ratioValue')}
{t('forge.depositAddress')} @@ -540,7 +546,10 @@ export default function App() {
- {t('forge.pay')} ({getChainName(selectedOption.externalChain)}) + {t('common.chainLabel', { + label: t('forge.pay'), + chain: getChainName(selectedOption.externalChain), + })}
@@ -556,7 +565,10 @@ export default function App() {
- {t('forge.receive')} ({getChainName(selectedOption.internalChain)}) + {t('common.chainLabel', { + label: t('forge.receive'), + chain: getChainName(selectedOption.internalChain), + })}
@@ -570,14 +582,14 @@ export default function App() {
{t('forge.ratio')} - 1:1 + {t('forge.ratioValue')}
{t('forge.network')}
{getChainName(selectedOption.externalChain)} - + {t('common.arrow')} {getChainName(selectedOption.internalChain)}
@@ -659,7 +671,9 @@ export default function App() {

{forgeHook.orderId && (

- {t('success.orderId')}: {forgeHook.orderId.slice(0, 16)}... + {t('success.orderIdLabel', { + id: `${forgeHook.orderId.slice(0, 16)}...`, + })}

)}
@@ -674,7 +688,12 @@ export default function App() { {/* Token Picker Modal */} {pickerOpen && (
-
setPickerOpen(false)} /> + ))} @@ -278,7 +283,10 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) { - {t('redemption.from')} ({getChainName(selectedOption.internalChain)}) + {t('common.chainLabel', { + label: t('redemption.from'), + chain: getChainName(selectedOption.internalChain), + })} @@ -300,7 +308,10 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) {
{selectedOption.rechargeItem.redemption && (
- {t('redemption.limits')}: {formatAmount(String(selectedOption.rechargeItem.redemption.min))} - {formatAmount(String(selectedOption.rechargeItem.redemption.max))} + {t('redemption.limitsRange', { + min: formatAmount(String(selectedOption.rechargeItem.redemption.min)), + max: formatAmount(String(selectedOption.rechargeItem.redemption.max)), + })}
)} @@ -319,7 +330,10 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) { - {t('redemption.to')} ({getChainName(selectedOption.externalChain)}) + {t('common.chainLabel', { + label: t('redemption.to'), + chain: getChainName(selectedOption.externalChain), + })} @@ -379,7 +393,10 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) {
- {t('redemption.from')} ({getChainName(selectedOption.internalChain)}) + {t('common.chainLabel', { + label: t('redemption.from'), + chain: getChainName(selectedOption.internalChain), + })}
@@ -395,7 +412,10 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) {
- {t('redemption.to')} ({getChainName(selectedOption.externalChain)}) + {t('common.chainLabel', { + label: t('redemption.to'), + chain: getChainName(selectedOption.externalChain), + })}
@@ -471,7 +491,9 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) {

{redemption.orderId && (

- {t('success.orderId')}: {redemption.orderId.slice(0, 16)}... + {t('success.orderIdLabel', { + id: `${redemption.orderId.slice(0, 16)}...`, + })}

)}
diff --git a/miniapps/forge/src/hooks/useForge.ts b/miniapps/forge/src/hooks/useForge.ts index 42d39b429..28378889f 100644 --- a/miniapps/forge/src/hooks/useForge.ts +++ b/miniapps/forge/src/hooks/useForge.ts @@ -228,7 +228,6 @@ export function useForge() { setState({ step: 'success', orderId: res.orderId, error: null }) } catch (err) { - console.error('[forge] submit failed', err) setState({ step: 'error', orderId: null, diff --git a/miniapps/forge/src/i18n/index.ts b/miniapps/forge/src/i18n/index.ts index f21df82ff..8716c0408 100644 --- a/miniapps/forge/src/i18n/index.ts +++ b/miniapps/forge/src/i18n/index.ts @@ -26,7 +26,7 @@ try { savedLanguage = parsed.language as LanguageCode } } -} catch (e) { +} catch { // Ignore error } diff --git a/miniapps/forge/src/i18n/locales/en.json b/miniapps/forge/src/i18n/locales/en.json index 104f86b85..6e55b75bc 100644 --- a/miniapps/forge/src/i18n/locales/en.json +++ b/miniapps/forge/src/i18n/locales/en.json @@ -16,9 +16,11 @@ "pay": "Pay", "receive": "Receive", "ratio": "Exchange Ratio", + "ratioValue": "1:1", "depositAddress": "Deposit Address", "network": "Network", "preview": "Preview Transaction", + "start": "Start Forge", "confirm": "Confirm Recharge", "continue": "Continue", "amountHint": "Amount is in {{symbol}}", @@ -31,7 +33,9 @@ "connect": "Connect Wallet", "from": "From", "to": "To", + "optionLabel": "{{asset}} → {{chain}}", "limits": "Limits", + "limitsRange": "Limits: {{min}} - {{max}}", "fee": "Fee", "receivable": "You will receive", "addressPlaceholder": "Enter external chain address", @@ -64,7 +68,8 @@ }, "success": { "title": "Complete", - "orderId": "Order" + "orderId": "Order", + "orderIdLabel": "Order: {{id}}" }, "error": { "sdkNotInit": "Bio SDK not initialized", @@ -74,10 +79,15 @@ }, "picker": { "title": "Select Token", - "selected": "Selected" + "selected": "Selected", + "close": "Close", + "optionAssets": "{{from}} → {{to}}", + "optionChains": "{{from}} → {{to}}" }, "common": { - "back": "Back" + "back": "Back", + "arrow": "→", + "chainLabel": "{{label}} ({{chain}})" }, "chain": { "ETH": "Ethereum", diff --git a/miniapps/forge/src/i18n/locales/zh-CN.json b/miniapps/forge/src/i18n/locales/zh-CN.json index 214d5e02b..aaa2789e3 100644 --- a/miniapps/forge/src/i18n/locales/zh-CN.json +++ b/miniapps/forge/src/i18n/locales/zh-CN.json @@ -16,9 +16,11 @@ "pay": "支付", "receive": "获得", "ratio": "兑换比例", + "ratioValue": "1:1", "depositAddress": "充值地址", "network": "网络", "preview": "预览交易", + "start": "开始锻造", "confirm": "确认充值", "continue": "继续", "amountHint": "数量单位:{{symbol}}", @@ -31,7 +33,9 @@ "connect": "连接钱包", "from": "从", "to": "到", + "optionLabel": "{{asset}} → {{chain}}", "limits": "限额", + "limitsRange": "限额:{{min}} - {{max}}", "fee": "手续费", "receivable": "预计到账", "addressPlaceholder": "输入外链地址", @@ -64,7 +68,8 @@ }, "success": { "title": "完成", - "orderId": "订单" + "orderId": "订单", + "orderIdLabel": "订单:{{id}}" }, "error": { "sdkNotInit": "Bio SDK 未初始化", @@ -74,10 +79,15 @@ }, "picker": { "title": "选择币种", - "selected": "已选" + "selected": "已选", + "close": "关闭", + "optionAssets": "{{from}} → {{to}}", + "optionChains": "{{from}} → {{to}}" }, "common": { - "back": "返回" + "back": "返回", + "arrow": "→", + "chainLabel": "{{label}}({{chain}})" }, "chain": { "ETH": "Ethereum", diff --git a/miniapps/forge/src/i18n/locales/zh-TW.json b/miniapps/forge/src/i18n/locales/zh-TW.json index 8aee5414e..744f5d7bc 100644 --- a/miniapps/forge/src/i18n/locales/zh-TW.json +++ b/miniapps/forge/src/i18n/locales/zh-TW.json @@ -16,9 +16,11 @@ "pay": "支付", "receive": "獲得", "ratio": "兌換比例", + "ratioValue": "1:1", "depositAddress": "充值地址", "network": "網絡", "preview": "預覽交易", + "start": "開始鍛造", "confirm": "確認充值", "continue": "繼續", "amountHint": "數量單位:{{symbol}}", @@ -31,7 +33,9 @@ "connect": "連接錢包", "from": "從", "to": "到", + "optionLabel": "{{asset}} → {{chain}}", "limits": "限額", + "limitsRange": "限額:{{min}} - {{max}}", "fee": "手續費", "receivable": "預計到賬", "addressPlaceholder": "輸入外鏈地址", @@ -64,7 +68,8 @@ }, "success": { "title": "完成", - "orderId": "訂單" + "orderId": "訂單", + "orderIdLabel": "訂單:{{id}}" }, "error": { "sdkNotInit": "Bio SDK 未初始化", @@ -74,10 +79,15 @@ }, "picker": { "title": "選擇幣種", - "selected": "已選" + "selected": "已選", + "close": "關閉", + "optionAssets": "{{from}} → {{to}}", + "optionChains": "{{from}} → {{to}}" }, "common": { - "back": "返回" + "back": "返回", + "arrow": "→", + "chainLabel": "{{label}}({{chain}})" }, "chain": { "ETH": "Ethereum", diff --git a/miniapps/forge/src/i18n/locales/zh.json b/miniapps/forge/src/i18n/locales/zh.json index 214d5e02b..aaa2789e3 100644 --- a/miniapps/forge/src/i18n/locales/zh.json +++ b/miniapps/forge/src/i18n/locales/zh.json @@ -16,9 +16,11 @@ "pay": "支付", "receive": "获得", "ratio": "兑换比例", + "ratioValue": "1:1", "depositAddress": "充值地址", "network": "网络", "preview": "预览交易", + "start": "开始锻造", "confirm": "确认充值", "continue": "继续", "amountHint": "数量单位:{{symbol}}", @@ -31,7 +33,9 @@ "connect": "连接钱包", "from": "从", "to": "到", + "optionLabel": "{{asset}} → {{chain}}", "limits": "限额", + "limitsRange": "限额:{{min}} - {{max}}", "fee": "手续费", "receivable": "预计到账", "addressPlaceholder": "输入外链地址", @@ -64,7 +68,8 @@ }, "success": { "title": "完成", - "orderId": "订单" + "orderId": "订单", + "orderIdLabel": "订单:{{id}}" }, "error": { "sdkNotInit": "Bio SDK 未初始化", @@ -74,10 +79,15 @@ }, "picker": { "title": "选择币种", - "selected": "已选" + "selected": "已选", + "close": "关闭", + "optionAssets": "{{from}} → {{to}}", + "optionChains": "{{from}} → {{to}}" }, "common": { - "back": "返回" + "back": "返回", + "arrow": "→", + "chainLabel": "{{label}}({{chain}})" }, "chain": { "ETH": "Ethereum", diff --git a/miniapps/forge/src/lib/tron-address.ts b/miniapps/forge/src/lib/tron-address.ts index 921feb257..772970da7 100644 --- a/miniapps/forge/src/lib/tron-address.ts +++ b/miniapps/forge/src/lib/tron-address.ts @@ -35,7 +35,7 @@ function encodeBase58(buffer: Uint8Array): string { } } - return leadingZeros + [...digits].reverse().map((d: number) => BASE58_ALPHABET[d]).join('') + return leadingZeros + digits.slice().reverse().map((d: number) => BASE58_ALPHABET[d]).join('') } /** @@ -86,7 +86,11 @@ export async function tronHexToBase58(hexAddress: string): Promise { } // Convert hex to bytes - const addressBytes = new Uint8Array(hex.match(/.{2}/g)!.map(byte => parseInt(byte, 16))) + const hexPairs = hex.match(/.{2}/g) + if (!hexPairs) { + throw new Error(`Invalid TRON hex address: ${hexAddress}`) + } + const addressBytes = new Uint8Array(hexPairs.map(byte => parseInt(byte, 16))) // Calculate checksum: SHA256(SHA256(address)) first 4 bytes const hash1 = await sha256(addressBytes) diff --git a/miniapps/forge/src/main.tsx b/miniapps/forge/src/main.tsx index cfa4bd4dd..207d0fb31 100644 --- a/miniapps/forge/src/main.tsx +++ b/miniapps/forge/src/main.tsx @@ -6,7 +6,12 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App' -createRoot(document.getElementById('root')!).render( +const rootElement = document.getElementById('root') +if (!rootElement) { + throw new Error('Root element not found') +} + +createRoot(rootElement).render( diff --git a/miniapps/teleport/scripts/e2e.ts b/miniapps/teleport/scripts/e2e.ts index c41ed3def..f41f9ebed 100644 --- a/miniapps/teleport/scripts/e2e.ts +++ b/miniapps/teleport/scripts/e2e.ts @@ -43,16 +43,21 @@ async function main() { } }) - vite.stderr?.on('data', (data) => { - - }) + vite.stderr?.on('data', (_data) => {}) // Wait for server to be ready const maxWait = 30000 const startTime = Date.now() - while (!serverReady && Date.now() - startTime < maxWait) { - await new Promise(r => setTimeout(r, 200)) - } + await new Promise((resolve) => { + const checkReady = () => { + if (serverReady || Date.now() - startTime >= maxWait) { + resolve() + return + } + setTimeout(checkReady, 200) + } + checkReady() + }) if (!serverReady) { diff --git a/packages/chain-effect/src/debug.ts b/packages/chain-effect/src/debug.ts index 18295094a..b49bdaafc 100644 --- a/packages/chain-effect/src/debug.ts +++ b/packages/chain-effect/src/debug.ts @@ -1,4 +1,5 @@ type DebugSetting = boolean | string | undefined +type DebugLogger = (...args: Array) => void function readLocalStorageSetting(): DebugSetting { if (typeof globalThis === "undefined") return undefined @@ -47,3 +48,30 @@ export function isChainEffectDebugEnabled(message: string): boolean { if (regex) return regex.test(message) return message.includes(setting) } + +function readDebugLogger(): DebugLogger | null { + if (typeof globalThis === "undefined") return null + const store = globalThis as typeof globalThis & { __CHAIN_EFFECT_LOG__?: unknown } + const value = store.__CHAIN_EFFECT_LOG__ + return typeof value === "function" ? (value as DebugLogger) : null +} + +export function formatChainEffectError(error: unknown): string { + if (error instanceof Error) return error.message + if (typeof error === "string") return error + try { + return JSON.stringify(error) + } catch { + return String(error) + } +} + +export function logChainEffectDebug( + message: string, + ...args: Array +): void { + if (!isChainEffectDebugEnabled(message)) return + const logger = readDebugLogger() + if (!logger) return + logger("[chain-effect]", message, ...args) +} diff --git a/packages/chain-effect/src/http-cache.ts b/packages/chain-effect/src/http-cache.ts index d06cd1f33..fb9012aad 100644 --- a/packages/chain-effect/src/http-cache.ts +++ b/packages/chain-effect/src/http-cache.ts @@ -48,7 +48,7 @@ function toStableJson(value: unknown): unknown { return value.map(toStableJson) } const sorted: UnknownRecord = {} - for (const key of Object.keys(value).sort()) { + for (const key of Object.keys(value).slice().sort()) { sorted[key] = toStableJson(value[key]) } return sorted diff --git a/packages/chain-effect/src/http.ts b/packages/chain-effect/src/http.ts index 78ce4c82f..390e84e8b 100644 --- a/packages/chain-effect/src/http.ts +++ b/packages/chain-effect/src/http.ts @@ -7,6 +7,7 @@ import { Effect, Schedule, Duration, Option } from 'effect'; import { Schema } from 'effect'; import { getFromCache, putToCache, deleteFromCache } from './http-cache'; +import { formatChainEffectError, logChainEffectDebug } from './debug'; // ==================== Error Types ==================== @@ -261,7 +262,7 @@ function toStableJson(value: unknown): unknown { return value.map(toStableJson); } const sorted: UnknownRecord = {}; - for (const key of Object.keys(value).sort()) { + for (const key of Object.keys(value).slice().sort()) { sorted[key] = toStableJson(value[key]); } return sorted; @@ -296,7 +297,7 @@ export function httpFetchCached(options: CachedFetchOptions): Effect.Effec return Effect.promise(async () => { const pending = pendingRequests.get(cacheKey); if (pending) { - console.log(`[httpFetchCached] PENDING: ${options.url}`); + logChainEffectDebug('httpFetchCached pending', options.url); return pending as Promise; } @@ -305,7 +306,11 @@ export function httpFetchCached(options: CachedFetchOptions): Effect.Effec try { return await canCache(result); } catch (error) { - console.error(`[httpFetchCached] CAN-CACHE ERROR: ${options.url}`, error); + logChainEffectDebug( + 'httpFetchCached can-cache error', + options.url, + formatChainEffectError(error), + ); return false; } }; @@ -321,10 +326,10 @@ export function httpFetchCached(options: CachedFetchOptions): Effect.Effec if (cacheStrategy === 'cache-first') { // Cache-First: 有缓存就返回 if (Option.isSome(cached) && cachedUsable && cachedValue !== null) { - console.log(`[httpFetchCached] CACHE-FIRST HIT: ${options.url}`); + logChainEffectDebug('httpFetchCached cache-first hit', options.url); return cachedValue; } - console.log(`[httpFetchCached] CACHE-FIRST MISS: ${options.url}`); + logChainEffectDebug('httpFetchCached cache-first miss', options.url); try { const result = await Effect.runPromise(httpFetch(fetchOptions)); if (await shouldCache(result)) { @@ -332,7 +337,11 @@ export function httpFetchCached(options: CachedFetchOptions): Effect.Effec } return result; } catch (error) { - console.error(`[httpFetchCached] CACHE-FIRST FETCH ERROR: ${options.url}`, error); + logChainEffectDebug( + 'httpFetchCached cache-first fetch error', + options.url, + formatChainEffectError(error), + ); throw error; } } @@ -340,16 +349,20 @@ export function httpFetchCached(options: CachedFetchOptions): Effect.Effec if (cacheStrategy === 'network-first') { // Network-First: 尝试 fetch,失败用缓存 try { - console.log(`[httpFetchCached] NETWORK-FIRST FETCH: ${options.url}`); + logChainEffectDebug('httpFetchCached network-first fetch', options.url); const result = await Effect.runPromise(httpFetch(fetchOptions)); if (await shouldCache(result)) { await Effect.runPromise(putToCache(options.url, options.body, result)); } return result; } catch (error) { - console.error(`[httpFetchCached] NETWORK-FIRST FETCH ERROR: ${options.url}`, error); + logChainEffectDebug( + 'httpFetchCached network-first fetch error', + options.url, + formatChainEffectError(error), + ); if (Option.isSome(cached) && cachedUsable && cachedValue !== null) { - console.log(`[httpFetchCached] NETWORK-FIRST FALLBACK: ${options.url}`); + logChainEffectDebug('httpFetchCached network-first fallback', options.url); return cachedValue; } throw error; @@ -360,12 +373,22 @@ export function httpFetchCached(options: CachedFetchOptions): Effect.Effec if (Option.isSome(cached) && cachedUsable) { const age = Date.now() - cached.value.timestamp; if (age < cacheTtl) { - console.log(`[httpFetchCached] TTL HIT: ${options.url} (age: ${age}ms, ttl: ${cacheTtl}ms)`); + logChainEffectDebug( + 'httpFetchCached ttl hit', + options.url, + `age=${age}ms`, + `ttl=${cacheTtl}ms`, + ); return cachedValue as T; } - console.log(`[httpFetchCached] TTL EXPIRED: ${options.url} (age: ${age}ms, ttl: ${cacheTtl}ms)`); + logChainEffectDebug( + 'httpFetchCached ttl expired', + options.url, + `age=${age}ms`, + `ttl=${cacheTtl}ms`, + ); } else { - console.log(`[httpFetchCached] TTL MISS: ${options.url}`); + logChainEffectDebug('httpFetchCached ttl miss', options.url); } try { @@ -375,7 +398,11 @@ export function httpFetchCached(options: CachedFetchOptions): Effect.Effec } return result; } catch (error) { - console.error(`[httpFetchCached] TTL FETCH ERROR: ${options.url}`, error); + logChainEffectDebug( + 'httpFetchCached ttl fetch error', + options.url, + formatChainEffectError(error), + ); throw error; } })(); diff --git a/packages/chain-effect/src/instance.ts b/packages/chain-effect/src/instance.ts index 93f07ac93..3e9074660 100644 --- a/packages/chain-effect/src/instance.ts +++ b/packages/chain-effect/src/instance.ts @@ -11,7 +11,7 @@ import { useState, useEffect, useCallback, useRef, useMemo, useSyncExternalStore } from "react" import { Effect, Stream, Fiber } from "effect" import type { FetchError } from "./http" -import { isChainEffectDebugEnabled } from "./debug" +import { formatChainEffectError, logChainEffectDebug } from "./debug" import type { DataSource } from "./source" type UnknownRecord = Record @@ -34,7 +34,7 @@ function toStableJson(value: unknown): unknown { return value.map(toStableJson) } const sorted: UnknownRecord = {} - for (const key of Object.keys(value).sort()) { + for (const key of Object.keys(value).slice().sort()) { sorted[key] = toStableJson(value[key]) } return sorted @@ -67,10 +67,8 @@ function summarizeValue(value: unknown): string { return String(value) } -function debugLog(...args: Array): void { - const message = `[chain-effect] ${args.join(" ")}` - if (!isChainEffectDebugEnabled(message)) return - console.log("[chain-effect]", ...args) +function debugLog(message: string, ...args: Array): void { + logChainEffectDebug(message, ...args) } /** 兼容旧 API 的 StreamInstance 接口 */ @@ -190,7 +188,7 @@ export function createStreamInstanceFromSource( Effect.catchAllCause((cause) => Effect.sync(() => { if (cancelled) return - console.error(`[${name}] changes stream failed:`, cause) + debugLog(`${name} changes stream failed`, formatChainEffectError(cause)) onError?.(cause) }) ) @@ -203,7 +201,7 @@ export function createStreamInstanceFromSource( releaseSource(input) } }).catch((err) => { - console.error(`[${name}] getOrCreateSource failed:`, err) + debugLog(`${name} getOrCreateSource failed`, formatChainEffectError(err)) onError?.(err) }) @@ -239,6 +237,8 @@ export function createStreamInstanceFromSource( setIsFetching(true) setError(undefined) + debugLog(`${name} subscribe`, inputKey) + debugLog(`${name} subscribe`, inputKey) const unsubscribe = instanceRef.current.subscribe( inputRef.current, (newData: TOutput) => { @@ -399,7 +399,7 @@ export function createStreamInstance( } // 检查缓存 - const key = getInputKey(inputRef.current) + const key = inputKey const cached = cache.get(key) if (cached && Date.now() - cached.timestamp < ttl) { snapshotRef.current = cached.value diff --git a/packages/chain-effect/src/poll-meta.ts b/packages/chain-effect/src/poll-meta.ts index 6d33f9da8..4d04fa20e 100644 --- a/packages/chain-effect/src/poll-meta.ts +++ b/packages/chain-effect/src/poll-meta.ts @@ -33,8 +33,8 @@ function getDB(): Promise { dbPromise = new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION) - request.onerror = () => reject(request.error) - request.onsuccess = () => resolve(request.result) + request.addEventListener("error", () => reject(request.error)) + request.addEventListener("success", () => resolve(request.result)) request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result @@ -61,8 +61,8 @@ export const getPollMeta = (key: string): Effect.Effect => const store = tx.objectStore(STORE_NAME) const request = store.get(key) - request.onerror = () => reject(request.error) - request.onsuccess = () => resolve(request.result ?? null) + request.addEventListener("error", () => reject(request.error)) + request.addEventListener("success", () => resolve(request.result ?? null)) }) }, catch: (error) => new Error(`Failed to get poll meta: ${error}`), @@ -82,8 +82,8 @@ export const setPollMeta = (meta: PollMeta): Effect.Effect => const store = tx.objectStore(STORE_NAME) const request = store.put(meta) - request.onerror = () => reject(request.error) - request.onsuccess = () => resolve() + request.addEventListener("error", () => reject(request.error)) + request.addEventListener("success", () => resolve()) }) }, catch: (error) => new Error(`Failed to set poll meta: ${error}`), @@ -103,8 +103,8 @@ export const deletePollMeta = (key: string): Effect.Effect => const store = tx.objectStore(STORE_NAME) const request = store.delete(key) - request.onerror = () => reject(request.error) - request.onsuccess = () => resolve() + request.addEventListener("error", () => reject(request.error)) + request.addEventListener("success", () => resolve()) }) }, catch: (error) => new Error(`Failed to delete poll meta: ${error}`), diff --git a/packages/chain-effect/src/react.ts b/packages/chain-effect/src/react.ts index 712f67470..880b28d33 100644 --- a/packages/chain-effect/src/react.ts +++ b/packages/chain-effect/src/react.ts @@ -145,6 +145,7 @@ export function useEffectOnce( const depsKey = useMemo(() => JSON.stringify(deps), [deps]) const run = useCallback(async () => { + void depsKey if (!enabled) { setData(undefined) setIsLoading(false) @@ -167,11 +168,11 @@ export function useEffectOnce( setError(firstError as E) } setIsLoading(false) - }, [effect, enabled]) + }, [effect, enabled, depsKey]) useEffect(() => { run() - }, [depsKey, enabled]) + }, [run]) return { data, isLoading, error, refetch: run } } @@ -196,8 +197,7 @@ export function createStreamHook( params: TParams, options?: UseStreamOptions ): UseStreamResult { - const paramsKey = useMemo(() => JSON.stringify(params), [params]) - const stream = useMemo(() => factory(params), [paramsKey]) + const stream = useMemo(() => factory(params), [params]) return useStream(stream, options) } } diff --git a/packages/chain-effect/src/source-registry.ts b/packages/chain-effect/src/source-registry.ts index 19d746518..65988944f 100644 --- a/packages/chain-effect/src/source-registry.ts +++ b/packages/chain-effect/src/source-registry.ts @@ -11,6 +11,7 @@ import { Effect, Fiber, FiberStatus, Duration, SubscriptionRef, Stream, Schedule import type { FetchError } from "./http" import type { DataSource } from "./source" import { updateNextPollTime, getDelayUntilNextPoll } from "./poll-meta" +import { formatChainEffectError, logChainEffectDebug } from "./debug" // ==================== 类型定义 ==================== @@ -116,7 +117,11 @@ async function createSourceInternal( const pollEffect = Effect.gen(function* () { // 计算初始延迟(基于持久化的 nextPollTime) const delay = yield* getDelayUntilNextPoll(pollKey, intervalMs) - console.log(`[SourceRegistry] Poll fiber started for ${pollKey}, delay: ${delay}ms, interval: ${intervalMs}ms`) + logChainEffectDebug( + `${pollKey} poll fiber started`, + `delay=${delay}ms`, + `interval=${intervalMs}ms` + ) if (delay > 0) { yield* Effect.sleep(Duration.millis(delay)) } @@ -124,14 +129,17 @@ async function createSourceInternal( // 开始轮询循环 yield* Stream.repeatEffect( Effect.gen(function* () { - console.log(`[SourceRegistry] Polling ${pollKey}...`) + logChainEffectDebug(`${pollKey} polling`) const result = yield* Effect.catchAll(options.fetch, (error) => { - console.error(`[SourceRegistry] Poll error for ${pollKey}:`, error) + logChainEffectDebug( + `${pollKey} poll error`, + formatChainEffectError(error) + ) return Effect.succeed(null as T | null) }) if (result !== null) { - console.log(`[SourceRegistry] Poll success for ${pollKey}, updating ref`) + logChainEffectDebug(`${pollKey} poll success`, "update ref") yield* SubscriptionRef.set(ref, result) yield* updateNextPollTime(pollKey, intervalMs) } @@ -148,7 +156,10 @@ async function createSourceInternal( // 执行立即获取 const immediateResult = yield* Effect.catchAll(options.fetch, (error) => { - console.error(`[SourceRegistry] Immediate fetch error for ${key}:`, error) + logChainEffectDebug( + `${key} immediate fetch error`, + formatChainEffectError(error) + ) return Effect.succeed(null as T | null) }) if (immediateResult !== null) { diff --git a/packages/chain-effect/src/source.ts b/packages/chain-effect/src/source.ts index 0171faf0c..d6f999594 100644 --- a/packages/chain-effect/src/source.ts +++ b/packages/chain-effect/src/source.ts @@ -13,7 +13,7 @@ import { Effect, Stream, Schedule, SubscriptionRef, PubSub, Fiber } from "effect import type { Duration } from "effect" import type { FetchError } from "./http" import type { EventBusService, WalletEventType } from "./event-bus" -import { isChainEffectDebugEnabled } from "./debug" +import { formatChainEffectError, logChainEffectDebug } from "./debug" type UnknownRecord = Record @@ -33,10 +33,8 @@ function summarizeValue(value: unknown): string { return String(value) } -function debugLog(...args: Array): void { - const message = `[chain-effect] ${args.join(" ")}` - if (!isChainEffectDebugEnabled(message)) return - console.log("[chain-effect]", ...args) +function debugLog(message: string, ...args: Array): void { + logChainEffectDebug(message, ...args) } // ==================== Event Bus ==================== @@ -282,7 +280,10 @@ export const createDependentSource = ( const forceRefresh = acc.prev !== null debugLog(`${name} fetch`, forceRefresh ? "force" : "cache") const result = yield* Effect.catchAll(fetch(next, forceRefresh), (error) => { - console.error(`[DependentSource] ${name} fetch error:`, error) + debugLog( + `${name} fetch error`, + formatChainEffectError(error) + ) return Effect.succeed(null as T | null) }) if (result !== null) { @@ -307,7 +308,10 @@ export const createDependentSource = ( // Stream 的 scanEffect 会从当前依赖值开始追踪,避免首个 changes 触发重复拉取 if (currentDep !== null) { const initialValue = yield* Effect.catchAll(fetch(currentDep, false), (error) => { - console.error(`[DependentSource] Initial fetch error for ${name}:`, error) + debugLog( + `${name} initial fetch error`, + formatChainEffectError(error) + ) return Effect.succeed(null as T | null) }) if (initialValue !== null) { diff --git a/packages/create-miniapp/src/commands/create.ts b/packages/create-miniapp/src/commands/create.ts index ce3428906..e2121c82b 100644 --- a/packages/create-miniapp/src/commands/create.ts +++ b/packages/create-miniapp/src/commands/create.ts @@ -27,12 +27,11 @@ import { } from '../utils/inject' const log = { - info: (msg: string) => {}, - success: (msg: string) => {}, - warn: (msg: string) => {}, - error: (msg: string) => {}, - step: (step: number, total: number, msg: string) => - {}, + info: (_msg: string) => {}, + success: (_msg: string) => {}, + warn: (_msg: string) => {}, + error: (_msg: string) => {}, + step: (_step: number, _total: number, _msg: string) => {}, } function getNextPort(outputDir: string): number { diff --git a/packages/key-ui/src/skeleton/Skeleton.tsx b/packages/key-ui/src/skeleton/Skeleton.tsx index c0f84c2b2..11513de2a 100644 --- a/packages/key-ui/src/skeleton/Skeleton.tsx +++ b/packages/key-ui/src/skeleton/Skeleton.tsx @@ -15,10 +15,11 @@ export interface SkeletonTextProps { } export function SkeletonText({ lines = 3, className }: SkeletonTextProps) { + const lineKeys = Array.from({ length: lines }, (_, i) => `line-${i + 1}`) return (
- {Array.from({ length: lines }).map((_, i) => ( - + {lineKeys.map((key, i) => ( + ))}
) @@ -40,10 +41,11 @@ export function SkeletonCard({ className }: SkeletonProps) { } export function SkeletonList({ count = 3, className }: { count?: number; className?: string }) { + const itemKeys = Array.from({ length: count }, (_, i) => `item-${i + 1}`) return (
- {Array.from({ length: count }).map((_, i) => ( -
+ {itemKeys.map((key) => ( +
diff --git a/src/clear/main.ts b/src/clear/main.ts index f7f1ad4e7..03d8ff25f 100644 --- a/src/clear/main.ts +++ b/src/clear/main.ts @@ -30,14 +30,14 @@ const steps: StepElement[] = [ if (indexedDB.databases) { const databases = await indexedDB.databases(); for (const db of databases) { - if (db.name) { - await new Promise((resolve) => { - const request = indexedDB.deleteDatabase(db.name!); - request.onsuccess = () => resolve(); - request.onerror = () => resolve(); - request.onblocked = () => resolve(); - }); - } + if (!db.name) continue; + await new Promise((resolve) => { + const request = indexedDB.deleteDatabase(db.name); + const handleFinish = () => resolve(); + request.addEventListener('success', handleFinish); + request.addEventListener('error', handleFinish); + request.addEventListener('blocked', handleFinish); + }); } } }, @@ -55,7 +55,8 @@ const steps: StepElement[] = [ ]; function createUI() { - const root = document.getElementById('root')!; + const root = document.getElementById('root'); + if (!root) return false; root.innerHTML = `
@@ -93,6 +94,7 @@ function createUI() {

`; + return true; } function delay(ms: number) { @@ -120,29 +122,31 @@ function setStepDone(stepId: string) { async function clearAllData() { let completed = 0; - - for (const step of steps) { - setStepActive(step.id); - await delay(300); - - try { - await step.action(); - } catch (e) {} - - setStepDone(step.id); - completed++; - updateProgress(completed, steps.length); - } + await steps.reduce( + async (prev, step) => { + await prev; + setStepActive(step.id); + await delay(300); + try { + await step.action(); + } catch {} + setStepDone(step.id); + completed++; + updateProgress(completed, steps.length); + }, + Promise.resolve(), + ); } async function main() { - createUI(); - - const title = document.getElementById('title')!; - const status = document.getElementById('status')!; - const error = document.getElementById('error')!; - const checkIcon = document.getElementById('checkIcon')!; - const container = document.querySelector('.container')!; + if (!createUI()) return; + + const title = document.getElementById('title'); + const status = document.getElementById('status'); + const error = document.getElementById('error'); + const checkIcon = document.getElementById('checkIcon'); + const container = document.querySelector('.container'); + if (!title || !status || !error || !checkIcon || !container) return; try { await clearAllData(); diff --git a/src/components/authorize/AppInfoCard.tsx b/src/components/authorize/AppInfoCard.tsx index a151c81e3..8b3817a46 100644 --- a/src/components/authorize/AppInfoCard.tsx +++ b/src/components/authorize/AppInfoCard.tsx @@ -22,7 +22,7 @@ function isUnknownOrigin(origin: string): boolean { } export function AppInfoCard({ appInfo, className }: AppInfoCardProps) { - const { t } = useTranslation('common'); + const { t } = useTranslation(['common', 'authorize']); const [imageFailed, setImageFailed] = useState(false); const isUnknown = useMemo(() => isUnknownOrigin(appInfo.origin), [appInfo.origin]); @@ -55,7 +55,7 @@ export function AppInfoCard({ appInfo, className }: AppInfoCardProps) { aria-label={t('a11y.unknownApp')} > - Unknown + {t('authorize:unknownApp')} )}
diff --git a/src/components/common/error-boundary.tsx b/src/components/common/error-boundary.tsx index f8763c68e..ab08cd8ca 100644 --- a/src/components/common/error-boundary.tsx +++ b/src/components/common/error-boundary.tsx @@ -5,6 +5,8 @@ */ import { Component, type ReactNode, type ErrorInfo } from 'react' +import i18n from '@/i18n' +import { captureError } from '@/lib/error-capture' export interface ErrorBoundaryProps { /** 子组件 */ @@ -31,7 +33,7 @@ export class ErrorBoundary extends Component

- 出现了一些问题 + {i18n.t('error:boundary.title')}

) diff --git a/src/components/common/provider-fallback-warning.tsx b/src/components/common/provider-fallback-warning.tsx index d6744e5a3..9321fbeba 100644 --- a/src/components/common/provider-fallback-warning.tsx +++ b/src/components/common/provider-fallback-warning.tsx @@ -1,5 +1,6 @@ import { IconAlertTriangle } from '@tabler/icons-react' import { cn } from '@/lib/utils' +import { useTranslation } from 'react-i18next' interface ProviderFallbackWarningProps { feature: string @@ -8,6 +9,7 @@ interface ProviderFallbackWarningProps { } export function ProviderFallbackWarning({ feature, reason, className }: ProviderFallbackWarningProps) { + const { t } = useTranslation('error') return (

- {feature} query failed + {t('providerFallback.title', { feature })}

- All configured providers failed. Showing default value. + {t('providerFallback.description')}

{reason && (
- Technical details + {t('providerFallback.details')}

{reason} diff --git a/src/components/onboarding/chain-selector.tsx b/src/components/onboarding/chain-selector.tsx index 7cdc631ae..e41ba562a 100644 --- a/src/components/onboarding/chain-selector.tsx +++ b/src/components/onboarding/chain-selector.tsx @@ -84,13 +84,16 @@ export function ChainSelector({ .filter((kind) => grouped.has(kind)) .map((kind) => { const config = getChainKindConfig(kind, t); + const groupChains = grouped.get(kind); + if (!groupChains) return null; return { id: kind, name: config.name || kind, description: config.description, - chains: grouped.get(kind)!, + chains: groupChains, }; - }); + }) + .filter((group): group is ChainGroup => group !== null); }, [chains, t]); // 过滤搜索结果 @@ -262,7 +265,10 @@ export function ChainSelector({ {/* 选择计数 */} - {group.chains.filter((c) => selectedChains.includes(c.id)).length}/{group.chains.length} + {t('chainSelector.selectionCount', { + selected: group.chains.filter((c) => selectedChains.includes(c.id)).length, + total: group.chains.length, + })} @@ -284,7 +290,6 @@ export function ChainSelector({ 'hover:bg-muted/30 transition-colors', isSelected && 'bg-primary/5', )} - role="button" tabIndex={0} onClick={() => toggleChain(chain.id)} onKeyDown={(e) => { diff --git a/src/components/transfer/amount-input.tsx b/src/components/transfer/amount-input.tsx index 0d54f512a..42119ab13 100644 --- a/src/components/transfer/amount-input.tsx +++ b/src/components/transfer/amount-input.tsx @@ -16,6 +16,8 @@ interface AmountInputProps { balance?: Amount | undefined; /** Max allowed amount (defaults to balance) */ max?: Amount | undefined; + /** Disable max button */ + maxDisabled?: boolean | undefined; /** Fiat value to display */ fiatValue?: string | undefined; /** Fiat symbol (default: $) */ @@ -62,6 +64,7 @@ const AmountInput = forwardRef( fiatValue, fiatSymbol = '$', max, + maxDisabled = false, error, label, className, @@ -77,6 +80,7 @@ const AmountInput = forwardRef( const decimals = propDecimals ?? balance?.decimals ?? 18; const symbol = propSymbol ?? balance?.symbol ?? ''; const effectiveMax = max ?? balance; + const isMaxDisabled = disabled || maxDisabled; // Sync input value when controlled value changes from parent useEffect(() => { @@ -175,11 +179,15 @@ const AmountInput = forwardRef( />

- {effectiveMax && !disabled && ( + {effectiveMax && ( diff --git a/src/components/wallet/wallet-config.tsx b/src/components/wallet/wallet-config.tsx index 968ac49d7..cdf125baa 100644 --- a/src/components/wallet/wallet-config.tsx +++ b/src/components/wallet/wallet-config.tsx @@ -53,6 +53,8 @@ function getWeightedPresetColors(existingHues: number[]) { type WalletConfigMode = 'edit-only' | 'default' | 'edit'; +const WALLET_CONFIG_PREVIEW_NAME_TEST_ID = 'wallet-config-preview-name'; + interface WalletConfigProps { mode: WalletConfigMode; walletId: string; @@ -231,7 +233,7 @@ export function WalletConfig({ mode, walletId, onEditOnlyComplete, className }: resolveBackgroundStops(themeHue), [themeHue]) const { width, height, radius, iconSize } = sizeStyles[size] @@ -93,7 +95,7 @@ export function WalletMiniCard({ border: '0.5px solid rgba(255,255,255,0.2)', }} role="img" - aria-label="wallet card" + aria-label={t('miniCard.label')} > {/* 三角形纹理层 - 固定 5x3 网格 */} {patternUrl && ( diff --git a/src/hooks/use-pending-transactions.ts b/src/hooks/use-pending-transactions.ts index e4c635df1..37f3befa7 100644 --- a/src/hooks/use-pending-transactions.ts +++ b/src/hooks/use-pending-transactions.ts @@ -10,12 +10,11 @@ import { Effect, Stream, Fiber } from 'effect' import { pendingTxService, pendingTxManager, getPendingTxSource, getPendingTxWalletKey, type PendingTx } from '@/services/transaction' import { useChainConfigState } from '@/stores' import type { Transaction } from '@/services/chain-adapter/providers' -import { isChainDebugEnabled } from '@/services/chain-adapter/debug' +import { logChainDebug } from '@/services/chain-adapter/debug' function pendingTxDebugLog(...args: Array): void { const message = `[chain-effect] pending-tx ${args.join(' ')}` - if (!isChainDebugEnabled(message)) return - console.log('[chain-effect]', 'pending-tx', ...args) + logChainDebug(message, { args }) } export function usePendingTransactions( @@ -35,7 +34,7 @@ export function usePendingTransactions( set.add(tx.hash.toLowerCase()) } } - return Array.from(set).sort() + return Array.from(set).toSorted() }, [txHistory]) const confirmedTxHashKey = useMemo(() => confirmedTxHashes.join('|'), [confirmedTxHashes]) @@ -58,7 +57,7 @@ export function usePendingTransactions( map.set(item.id, item) } } - const merged = Array.from(map.values()).sort((a, b) => b.createdAt - a.createdAt) + const merged = Array.from(map.values()).toSorted((a, b) => b.createdAt - a.createdAt) pendingTxDebugLog('snapshot', walletKey ?? 'none', `len=${merged.length}`) return merged }, [walletKey, legacyWalletId]) @@ -80,8 +79,9 @@ export function usePendingTransactions( if (!mounted) return setTransactions(list) } finally { - if (!mounted) return - setIsLoading(false) + if (mounted) { + setIsLoading(false) + } } })() @@ -156,8 +156,9 @@ export function usePendingTransactions( setTransactions(list) }) .finally(() => { - if (!mounted) return - startStream() + if (mounted) { + startStream() + } }) }).catch(() => { if (!mounted) return @@ -185,7 +186,7 @@ export function usePendingTransactions( await pendingTxService.deleteByTxHash({ walletId: legacyWalletId, txHashes: confirmedTxHashes }) } })() - }, [walletKey, legacyWalletId, confirmedTxHashKey]) + }, [walletKey, legacyWalletId, confirmedTxHashKey, confirmedTxHashes]) // 订阅 pendingTxService 的变化(用于即时更新) useEffect(() => { @@ -200,7 +201,7 @@ export function usePendingTransactions( const map = new Map() for (const item of prev) map.set(item.id, item) map.set(tx.id, tx) - return Array.from(map.values()).sort((a, b) => b.createdAt - a.createdAt) + return Array.from(map.values()).toSorted((a, b) => b.createdAt - a.createdAt) }) } else if (event === 'updated') { setTransactions((prev) => { @@ -210,7 +211,7 @@ export function usePendingTransactions( const map = new Map() for (const item of prev) map.set(item.id, item) map.set(tx.id, tx) - return Array.from(map.values()).sort((a, b) => b.createdAt - a.createdAt) + return Array.from(map.values()).toSorted((a, b) => b.createdAt - a.createdAt) }) } else if (event === 'deleted') { setTransactions((prev) => prev.filter((t) => t.id !== tx.id)) @@ -230,9 +231,7 @@ export function usePendingTransactions( const clearAllFailed = useCallback(async () => { const failedTxs = transactions.filter((tx: PendingTx) => tx.status === 'failed') - for (const tx of failedTxs) { - await pendingTxService.delete({ id: tx.id }) - } + await Promise.all(failedTxs.map((tx) => pendingTxService.delete({ id: tx.id }))) }, [transactions]) return { diff --git a/src/hooks/use-send.bioforest.ts b/src/hooks/use-send.bioforest.ts index 92bdf81ea..73e3bb5de 100644 --- a/src/hooks/use-send.bioforest.ts +++ b/src/hooks/use-send.bioforest.ts @@ -339,7 +339,7 @@ export async function submitSetTwoStepSecret({ } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes('fee') || errorMessage.includes('手续费')) { + if (errorMessage.includes('fee') || errorMessage.includes('\u624b\u7eed\u8d39')) { // i18n-ignore return { status: 'error', message: t('error:insufficientFunds') }; } diff --git a/src/hooks/use-send.ts b/src/hooks/use-send.ts index ac15ab8b2..d836d455b 100644 --- a/src/hooks/use-send.ts +++ b/src/hooks/use-send.ts @@ -1,4 +1,4 @@ -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; import type { AssetInfo } from '@/types/asset'; import { Amount } from '@/types/amount'; import { initialState, MOCK_FEES } from './use-send.constants'; @@ -22,6 +22,8 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn { asset: initialAsset ?? null, }); + const feeInitKeyRef = useRef(null); + const isBioforestChain = chainConfig?.chainKind === 'bioforest'; const isWeb3Chain = chainConfig?.chainKind === 'evm' || chainConfig?.chainKind === 'tron' || chainConfig?.chainKind === 'bitcoin'; @@ -126,6 +128,22 @@ export function useSend(options: UseSendOptions = {}): UseSendReturn { [chainConfig, fromAddress, isBioforestChain, isWeb3Chain, useMock], ); + useEffect(() => { + if (!state.asset) return; + if (state.feeLoading) return; + + const feeKey = `${chainConfig?.id ?? 'unknown'}:${fromAddress ?? ''}:${state.asset.assetType}`; + const feeKeyChanged = feeInitKeyRef.current !== feeKey; + if (feeKeyChanged) { + feeInitKeyRef.current = feeKey; + } + + if (!feeKeyChanged && state.feeAmount) return; + if (!feeKeyChanged && !state.feeAmount) return; + + setAsset(state.asset); + }, [chainConfig?.id, fromAddress, setAsset, state.asset, state.feeAmount, state.feeLoading]); + // Get current balance from external source (single source of truth) const currentBalance = useMemo(() => { if (!state.asset?.assetType || !getBalance) return state.asset?.amount ?? null; diff --git a/src/hooks/use-send.web3.ts b/src/hooks/use-send.web3.ts index ff5e9196b..03472ba00 100644 --- a/src/hooks/use-send.web3.ts +++ b/src/hooks/use-send.web3.ts @@ -124,24 +124,10 @@ export async function submitWeb3Transfer({ // Handle specific error cases if (errorMessage.includes('insufficient') || errorMessage.includes('balance')) { - return { status: 'error', message: t('error:transaction.insufficientBalance') }; + return { status: 'error', message: t('error:insufficientFunds') }; } if (errorMessage.includes('fee') || errorMessage.includes('gas')) { - return { status: 'error', message: t('error:transaction.insufficientFee') }; - } - - 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')) { - // i18n-ignore return { status: 'error', message: t('error:transaction.insufficientGas') }; } @@ -151,7 +137,7 @@ export async function submitWeb3Transfer({ return { status: 'error', - message: errorMessage || t('error:transaction.retryLater'), + message: errorMessage || t('error:transaction.failed'), }; } } diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 11918cc2c..d0d7e9163 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -23,6 +23,7 @@ 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'; +import enDevtools from './locales/en/devtools.json'; // Namespace imports - zh-CN import zhCNCommon from './locales/zh-CN/common.json'; @@ -46,6 +47,7 @@ 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'; +import zhCNDevtools from './locales/zh-CN/devtools.json'; // Namespace imports - zh-TW import zhTWCommon from './locales/zh-TW/common.json'; @@ -69,6 +71,7 @@ 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'; +import zhTWDevtools from './locales/zh-TW/devtools.json'; // Namespace imports - ar import arCommon from './locales/ar/common.json'; @@ -92,6 +95,7 @@ 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'; +import arDevtools from './locales/ar/devtools.json'; // 语言配置 export const languages = { @@ -128,6 +132,7 @@ export const namespaces = [ 'home', 'ecosystem', 'permission', + 'devtools', ] as const; export type Namespace = (typeof namespaces)[number]; @@ -168,6 +173,7 @@ i18n.use(initReactI18next).init({ home: enHome, ecosystem: enEcosystem, permission: enPermission, + devtools: enDevtools, }, 'zh-CN': { common: zhCNCommon, @@ -191,6 +197,7 @@ i18n.use(initReactI18next).init({ home: zhCNHome, ecosystem: zhCNEcosystem, permission: zhCNPermission, + devtools: zhCNDevtools, }, 'zh-TW': { common: zhTWCommon, @@ -214,6 +221,7 @@ i18n.use(initReactI18next).init({ home: zhTWHome, ecosystem: zhTWEcosystem, permission: zhTWPermission, + devtools: zhTWDevtools, }, ar: { common: arCommon, @@ -237,6 +245,7 @@ i18n.use(initReactI18next).init({ home: arHome, ecosystem: arEcosystem, permission: arPermission, + devtools: arDevtools, }, }, lng: defaultLanguage, diff --git a/src/i18n/locales/ar/authorize.json b/src/i18n/locales/ar/authorize.json index 2fcab5d8b..87f53f3bc 100644 --- a/src/i18n/locales/ar/authorize.json +++ b/src/i18n/locales/ar/authorize.json @@ -59,5 +59,6 @@ }, "passwordConfirm": { "title": "Confirm Password" - } + }, + "unknownApp": "Unknown" } diff --git a/src/i18n/locales/ar/common.json b/src/i18n/locales/ar/common.json index 7951f44c3..81d3ede99 100644 --- a/src/i18n/locales/ar/common.json +++ b/src/i18n/locales/ar/common.json @@ -72,6 +72,7 @@ "addressPlaceholder": "Enter wallet address", "balanceTitle": "Address Balance", "chain": "Chain", + "fromLabel": "From:", "error": "Query Failed", "explorerHint": "Transaction history requires block explorer", "noTransactions": "No transactions found", @@ -79,10 +80,15 @@ "openExplorer": "Open {{name}} Explorer", "otherChains": "Other Chains", "queryError": "Failed to query transactions, please try again", + "toLabel": "To:", "transactionsTitle": "Address Transactions", "useExplorerHint": "This chain does not support direct history query, please use the explorer", "viewOnExplorer": "View on {{name}}" }, + "sign": { + "plus": "+", + "minus": "-" + }, "addressPlaceholder": "أدخل أو الصق العنوان", "advancedEncryptionTechnologyDigitalWealthIsMoreSecure": "Advanced encryption technology <br /> Digital wealth is more secure", "afterConfirmation_{appName}WillDeleteAllLocalDataTips": "After confirmation, will delete all local data and all wallets will be removed locally. Are you sure to exit?", @@ -152,7 +158,9 @@ "save": "حفظ", "selectAddress": "اختر العنوان", "selectContact": "اختر جهة اتصال", - "viewAll": "عرض جميع جهات الاتصال" + "viewAll": "عرض جميع جهات الاتصال", + "addressCount": "{{count}}/{{max}}", + "defaultLabel": "افتراضي" }, "contactCard": { "scanToAdd": "امسح لإضافة جهة اتصال", @@ -265,7 +273,9 @@ "selectWallets": "اختر المحفظة", "title": "بطاقتي", "usernamePlaceholder": "أدخل اسم المستخدم", - "walletAddress": "{{wallet}} ({{chain}})" + "walletAddress": "{{wallet}} ({{chain}})", + "chainLabel": "({{chain}})", + "removeWallet": "إزالة {{name}}" }, "name": "Name", "navOverview": "Overview", diff --git a/src/i18n/locales/ar/devtools.json b/src/i18n/locales/ar/devtools.json new file mode 100644 index 000000000..4146fb864 --- /dev/null +++ b/src/i18n/locales/ar/devtools.json @@ -0,0 +1,130 @@ +{ + "title": "Mock DevTools", + "tabs": { + "logs": "Logs", + "breakpoints": "Breakpoints", + "console": "Console", + "settings": "Settings" + }, + "button": { + "open": "Open Mock DevTools (draggable)" + }, + "footer": { + "mode": "Mock Mode", + "logs": "{{count}} logs", + "paused": "{{count}} paused" + }, + "logs": { + "filterPlaceholder": "Filter service.method...", + "clear": "Clear logs", + "empty": "No request logs", + "input": "Input:", + "output": "Output:", + "error": "Error:", + "intercepted": "Intercepted and modified" + }, + "settings": { + "defaultError": "Mock Error", + "globalDelay": "Global delay", + "ms": "ms", + "delayMarks": { + "zero": "0", + "one": "1s", + "two": "2s", + "three": "3s+" + }, + "mockError": "Mock error", + "errorPlaceholder": "Error message (optional)", + "errorNotice": "All requests will return error", + "resetAll": "Reset all settings", + "clearLogs": "Clear logs", + "clearBreakpoints": "Clear breakpoints", + "tipTitle": "Tip", + "tipDelay": "Global delay applies to all mock service requests", + "tipError": "Mock error makes all requests throw the specified error", + "tipBreakpoint": "Breakpoints can be configured in the \"Breakpoints\" panel" + }, + "breakpoints": { + "consoleLabel": "Console:", + "consoleView": "View in Console", + "resume": "Resume", + "abort": "Abort", + "ms": "ms", + "input": "input", + "output": "output", + "inputHint": "($input available)", + "outputHint": "($input, $output available)", + "pause": "Pause", + "pauseConditionPlaceholder": "Condition expression (optional)", + "inject": "Inject", + "injectInputPlaceholder": "$input.text = 'modified'", + "injectOutputPlaceholder": "return { success: true }\n// or throw new Error('test')", + "summary": { + "delay": "delay: {{ms}}ms", + "inputPause": "input:pause", + "inputInject": "input:inject", + "outputPause": "output:pause", + "outputInject": "output:inject" + }, + "addDialogTitle": "Add breakpoint", + "add": "Add", + "title": "Breakpoints", + "pausedCount": "Paused ({{count}})", + "emptyTitle": "No breakpoints", + "emptyHint": "Click \"Add\" to create breakpoints" + }, + "console": { + "help": { + "sections": { + "commands": "Commands", + "variables": "Variables", + "methods": "Methods", + "examples": "Examples" + }, + "commands": { + "help": "Show help", + "clear": "Clear Console", + "vars": "Show available variables", + "copy": "Copy expression result to clipboard" + }, + "variables": { + "paused": "Paused request array", + "indexed": "Access paused requests by index", + "latest": "Latest paused request", + "byId": "Access by ID, e.g. $p1, $p2" + }, + "methods": { + "resume": "Resume execution (optionally with modifications)", + "abort": "Abort request" + }, + "examples": { + "latest": "View latest paused request", + "input": "View request input", + "copy": "Copy input to clipboard", + "resume": "Resume execution" + } + }, + "vars": { + "available": "Available variables", + "pausedCount": "Paused count" + }, + "copy": { + "usage": "Usage: /copy , e.g. /copy $p1.$input", + "success": "Copied to clipboard ({{count}} chars)", + "failure": "Copy failed: {{message}}", + "expressionError": "Expression error: {{message}}" + }, + "unknownCommand": "Unknown command: {{command}}. Type /help for help.", + "title": "Console", + "pausedCount": "{{count}} paused", + "clearTitle": "Clear Console (/clear)", + "empty": { + "titlePrefix": "Console panel - type", + "titleSuffix": "for help", + "varsLabel": "Variables:", + "vars": "Variables: $paused, $0, $_, $p{id}" + }, + "inputPlaceholder": "Enter expression... (↑↓ history)", + "runTitle": "Run (Enter)" + } +} diff --git a/src/i18n/locales/ar/ecosystem.json b/src/i18n/locales/ar/ecosystem.json index a1e7177f6..0cc0e11fd 100644 --- a/src/i18n/locales/ar/ecosystem.json +++ b/src/i18n/locales/ar/ecosystem.json @@ -8,7 +8,8 @@ "title": "إدارة المصادر الموثوقة", "add": "إضافة اشتراك", "remove": "إزالة", - "empty": "لا توجد مصادر اشتراك" + "empty": "لا توجد مصادر اشتراك", + "defaultName": "النظام البيئي الرسمي لـ Bio" }, "permissions": { "title": "طلب إذن", @@ -120,6 +121,7 @@ "unknownDeveloper": "مطور غير معروف", "preview": "معاينة", "supportedChains": "سلاسل الكتل المدعومة", + "beta": "Beta", "privacy": "خصوصية التطبيق", "privacyHint": "يشير المطور إلى أن هذا التطبيق قد يطلب الأذونات التالية", "tags": "العلامات", @@ -141,6 +143,7 @@ "social": "اجتماعي", "exchange": "تبادل", "other": "أخرى" - } + }, + "tagLabel": "#{{tag}}" } } diff --git a/src/i18n/locales/ar/error.json b/src/i18n/locales/ar/error.json index 17a42ded6..44dc290c3 100644 --- a/src/i18n/locales/ar/error.json +++ b/src/i18n/locales/ar/error.json @@ -115,5 +115,14 @@ "mustBeArray": "signaturedata يجب أن يكون مصفوفة JSON", "emptyArray": "signaturedata لا يمكن أن يكون مصفوفة فارغة", "firstMustBeObject": "signaturedata[0] يجب أن يكون كائنًا" + }, + "providerFallback": { + "title": "فشل استعلام {{feature}}", + "description": "فشلت جميع المزوّدات المُكوّنة، يتم عرض القيمة الافتراضية.", + "details": "تفاصيل تقنية" + }, + "boundary": { + "title": "حدث خطأ ما", + "retry": "إعادة المحاولة" } } diff --git a/src/i18n/locales/ar/onboarding.json b/src/i18n/locales/ar/onboarding.json index a7489d3fe..31e4bb4b8 100644 --- a/src/i18n/locales/ar/onboarding.json +++ b/src/i18n/locales/ar/onboarding.json @@ -188,7 +188,8 @@ "searchPlaceholder": "Search chain name or symbol", "noResults": "No chains found", "selectAll": "Select All", - "deselectAll": "Deselect All" + "deselectAll": "Deselect All", + "selectionCount": "{{selected}}/{{total}}" }, "theme": { "walletName": "اسم المحفظة", @@ -199,4 +200,4 @@ "similarExists": "يوجد لون مشابه", "auto": "تلقائي" } -} \ No newline at end of file +} diff --git a/src/i18n/locales/ar/staking.json b/src/i18n/locales/ar/staking.json index 6cde5f26e..eddf50e59 100644 --- a/src/i18n/locales/ar/staking.json +++ b/src/i18n/locales/ar/staking.json @@ -27,6 +27,7 @@ "sourceChain": "السلسلة المصدر", "stakedAmount": "المبلغ المخزن", "targetChain": "السلسلة الهدف", + "targetChainLabel": "→ {{chain}} ({{asset}})", "title": "التخزين", "totalBurned": "إجمالي {{asset}} المستردة", "totalCirculation": "{{asset}} المتداولة", diff --git a/src/i18n/locales/ar/transaction.json b/src/i18n/locales/ar/transaction.json index f82b447cf..eae819dde 100644 --- a/src/i18n/locales/ar/transaction.json +++ b/src/i18n/locales/ar/transaction.json @@ -229,6 +229,8 @@ "continue": "متابعة", "explorerNotImplemented": "ميزة مستكشف البلوك قادمة قريبًا", "fee": "رسوم المعاملة", + "feeEstimating": "جارٍ التقدير...", + "feeUnavailable": "الرسوم غير متاحة", "from": "من", "networkWarning": "يرجى التأكد من أن عنوان المستلم هو عنوان شبكة {{chain}}. لا يمكن استرداد المرسل إلى الشبكة الخاطئة", "resultTitle": "نتيجة الإرسال", diff --git a/src/i18n/locales/ar/wallet.json b/src/i18n/locales/ar/wallet.json index 144e6033e..df331142d 100644 --- a/src/i18n/locales/ar/wallet.json +++ b/src/i18n/locales/ar/wallet.json @@ -5,7 +5,8 @@ "defaultName": "محفظتي", "randomName": "اسم عشوائي", "carousel": { - "walletCount": "{{count}} محافظ" + "walletCount": "{{count}} محافظ", + "walletCount_one": "محفظة واحدة" }, "menu": { "addressBalanceQuery": "استعلام عن رصيد العنوان", @@ -199,5 +200,8 @@ "walletsUnderIdentity": "محافظ الهوية", "willSynchronizeImportIntoThe_{chainname}ChainWithTheSamePrivateKey": "Will synchronize import into the chain with the same private key", "{assetType}Details": "Details", - "{asset}TotalCirculation": "total circulation" -} \ No newline at end of file + "{asset}TotalCirculation": "total circulation", + "miniCard": { + "label": "Wallet card" + } +} diff --git a/src/i18n/locales/en/authorize.json b/src/i18n/locales/en/authorize.json index 1b4a08d6d..ac1dda46a 100644 --- a/src/i18n/locales/en/authorize.json +++ b/src/i18n/locales/en/authorize.json @@ -59,5 +59,6 @@ }, "passwordConfirm": { "title": "Confirm Password" - } + }, + "unknownApp": "Unknown" } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 726be2fcf..f61ab7845 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -119,7 +119,9 @@ "save": "Save", "selectAddress": "Select address", "selectContact": "Select Contact", - "viewAll": "View all contacts" + "viewAll": "View all contacts", + "addressCount": "{{count}}/{{max}}", + "defaultLabel": "Default" }, "continue": "Continue", "contractMethods": "Contract methods", @@ -399,13 +401,17 @@ "noWalletsSelected": "Please select at least one wallet", "scanToAdd": "Scan to add me as contact", "walletAddress": "{{wallet}} ({{chain}})", - "currentChain": "Current chain" + "currentChain": "Current chain", + "chainLabel": "({{chain}})", + "removeWallet": "Remove {{name}}" }, "addressLookup": { "balanceTitle": "Address Balance", "transactionsTitle": "Address Transactions", "chain": "Chain", "address": "Address", + "fromLabel": "From:", + "toLabel": "To:", "addressPlaceholder": "Enter wallet address", "addressOrHash": "Address or Transaction Hash", "addressOrHashPlaceholder": "Enter address or tx hash", @@ -419,6 +425,10 @@ "openExplorer": "Open {{name}} Explorer", "viewOnExplorer": "View on {{name}}" }, + "sign": { + "plus": "+", + "minus": "-" + }, "tabs": { "assets": "Assets", "history": "History" diff --git a/src/i18n/locales/en/devtools.json b/src/i18n/locales/en/devtools.json new file mode 100644 index 000000000..4146fb864 --- /dev/null +++ b/src/i18n/locales/en/devtools.json @@ -0,0 +1,130 @@ +{ + "title": "Mock DevTools", + "tabs": { + "logs": "Logs", + "breakpoints": "Breakpoints", + "console": "Console", + "settings": "Settings" + }, + "button": { + "open": "Open Mock DevTools (draggable)" + }, + "footer": { + "mode": "Mock Mode", + "logs": "{{count}} logs", + "paused": "{{count}} paused" + }, + "logs": { + "filterPlaceholder": "Filter service.method...", + "clear": "Clear logs", + "empty": "No request logs", + "input": "Input:", + "output": "Output:", + "error": "Error:", + "intercepted": "Intercepted and modified" + }, + "settings": { + "defaultError": "Mock Error", + "globalDelay": "Global delay", + "ms": "ms", + "delayMarks": { + "zero": "0", + "one": "1s", + "two": "2s", + "three": "3s+" + }, + "mockError": "Mock error", + "errorPlaceholder": "Error message (optional)", + "errorNotice": "All requests will return error", + "resetAll": "Reset all settings", + "clearLogs": "Clear logs", + "clearBreakpoints": "Clear breakpoints", + "tipTitle": "Tip", + "tipDelay": "Global delay applies to all mock service requests", + "tipError": "Mock error makes all requests throw the specified error", + "tipBreakpoint": "Breakpoints can be configured in the \"Breakpoints\" panel" + }, + "breakpoints": { + "consoleLabel": "Console:", + "consoleView": "View in Console", + "resume": "Resume", + "abort": "Abort", + "ms": "ms", + "input": "input", + "output": "output", + "inputHint": "($input available)", + "outputHint": "($input, $output available)", + "pause": "Pause", + "pauseConditionPlaceholder": "Condition expression (optional)", + "inject": "Inject", + "injectInputPlaceholder": "$input.text = 'modified'", + "injectOutputPlaceholder": "return { success: true }\n// or throw new Error('test')", + "summary": { + "delay": "delay: {{ms}}ms", + "inputPause": "input:pause", + "inputInject": "input:inject", + "outputPause": "output:pause", + "outputInject": "output:inject" + }, + "addDialogTitle": "Add breakpoint", + "add": "Add", + "title": "Breakpoints", + "pausedCount": "Paused ({{count}})", + "emptyTitle": "No breakpoints", + "emptyHint": "Click \"Add\" to create breakpoints" + }, + "console": { + "help": { + "sections": { + "commands": "Commands", + "variables": "Variables", + "methods": "Methods", + "examples": "Examples" + }, + "commands": { + "help": "Show help", + "clear": "Clear Console", + "vars": "Show available variables", + "copy": "Copy expression result to clipboard" + }, + "variables": { + "paused": "Paused request array", + "indexed": "Access paused requests by index", + "latest": "Latest paused request", + "byId": "Access by ID, e.g. $p1, $p2" + }, + "methods": { + "resume": "Resume execution (optionally with modifications)", + "abort": "Abort request" + }, + "examples": { + "latest": "View latest paused request", + "input": "View request input", + "copy": "Copy input to clipboard", + "resume": "Resume execution" + } + }, + "vars": { + "available": "Available variables", + "pausedCount": "Paused count" + }, + "copy": { + "usage": "Usage: /copy , e.g. /copy $p1.$input", + "success": "Copied to clipboard ({{count}} chars)", + "failure": "Copy failed: {{message}}", + "expressionError": "Expression error: {{message}}" + }, + "unknownCommand": "Unknown command: {{command}}. Type /help for help.", + "title": "Console", + "pausedCount": "{{count}} paused", + "clearTitle": "Clear Console (/clear)", + "empty": { + "titlePrefix": "Console panel - type", + "titleSuffix": "for help", + "varsLabel": "Variables:", + "vars": "Variables: $paused, $0, $_, $p{id}" + }, + "inputPlaceholder": "Enter expression... (↑↓ history)", + "runTitle": "Run (Enter)" + } +} diff --git a/src/i18n/locales/en/ecosystem.json b/src/i18n/locales/en/ecosystem.json index 081987ec8..452a32807 100644 --- a/src/i18n/locales/en/ecosystem.json +++ b/src/i18n/locales/en/ecosystem.json @@ -8,7 +8,8 @@ "title": "Trusted Sources", "add": "Add Subscription", "remove": "Remove", - "empty": "No subscription sources" + "empty": "No subscription sources", + "defaultName": "Bio Official Ecosystem" }, "permissions": { "title": "Permission Request", @@ -120,6 +121,7 @@ "unknownDeveloper": "Unknown Developer", "preview": "Preview", "supportedChains": "Supported Blockchains", + "beta": "Beta", "privacy": "App Privacy", "privacyHint": "The developer indicates this app may request the following permissions", "tags": "Tags", @@ -141,6 +143,7 @@ "social": "Social", "exchange": "Exchange", "other": "Other" - } + }, + "tagLabel": "#{{tag}}" } } diff --git a/src/i18n/locales/en/error.json b/src/i18n/locales/en/error.json index 23752b34d..722741ddb 100644 --- a/src/i18n/locales/en/error.json +++ b/src/i18n/locales/en/error.json @@ -115,5 +115,14 @@ "mustBeArray": "signaturedata must be a JSON array", "emptyArray": "signaturedata cannot be an empty array", "firstMustBeObject": "signaturedata[0] must be an object" + }, + "providerFallback": { + "title": "{{feature}} query failed", + "description": "All configured providers failed. Showing default value.", + "details": "Technical details" + }, + "boundary": { + "title": "Something went wrong", + "retry": "Retry" } } diff --git a/src/i18n/locales/en/onboarding.json b/src/i18n/locales/en/onboarding.json index b712be7ed..eb41e48ef 100644 --- a/src/i18n/locales/en/onboarding.json +++ b/src/i18n/locales/en/onboarding.json @@ -188,7 +188,8 @@ "searchPlaceholder": "Search chain name or symbol", "noResults": "No chains found", "selectAll": "Select All", - "deselectAll": "Deselect All" + "deselectAll": "Deselect All", + "selectionCount": "{{selected}}/{{total}}" }, "theme": { "walletName": "Wallet Name", @@ -199,4 +200,4 @@ "similarExists": "Similar color exists", "auto": "Auto" } -} \ No newline at end of file +} diff --git a/src/i18n/locales/en/staking.json b/src/i18n/locales/en/staking.json index 5a04ae251..33a4ffd4a 100644 --- a/src/i18n/locales/en/staking.json +++ b/src/i18n/locales/en/staking.json @@ -17,6 +17,7 @@ "burnDescription": "Convert internal chain tokens to external chain", "sourceChain": "Source Chain", "targetChain": "Target Chain", + "targetChainLabel": "→ {{chain}} ({{asset}})", "estimatedFee": "Estimated Fee", "transactionPending": "Transaction Pending", "transactionConfirming": "Confirming", diff --git a/src/i18n/locales/en/transaction.json b/src/i18n/locales/en/transaction.json index f3baaaed2..89dba09b6 100644 --- a/src/i18n/locales/en/transaction.json +++ b/src/i18n/locales/en/transaction.json @@ -229,6 +229,8 @@ "continue": "Continue", "explorerNotImplemented": "Block explorer feature coming soon", "fee": "Fee", + "feeEstimating": "Estimating...", + "feeUnavailable": "Fee unavailable", "from": "From", "networkWarning": "Please ensure the recipient address is a {{chain}} network address. Sending to the wrong network cannot be recovered", "resultTitle": "Send Result", diff --git a/src/i18n/locales/en/wallet.json b/src/i18n/locales/en/wallet.json index 1b6378f3b..7c11097de 100644 --- a/src/i18n/locales/en/wallet.json +++ b/src/i18n/locales/en/wallet.json @@ -201,5 +201,8 @@ "walletsUnderIdentity": "Identity Wallets", "willSynchronizeImportIntoThe_{chainname}ChainWithTheSamePrivateKey": "Will synchronize import into the chain with the same private key", "{assetType}Details": "Details", - "{asset}TotalCirculation": "total circulation" -} \ No newline at end of file + "{asset}TotalCirculation": "total circulation", + "miniCard": { + "label": "Wallet card" + } +} diff --git a/src/i18n/locales/zh-CN/authorize.json b/src/i18n/locales/zh-CN/authorize.json index 1ad2bf5f1..487bd28c0 100644 --- a/src/i18n/locales/zh-CN/authorize.json +++ b/src/i18n/locales/zh-CN/authorize.json @@ -59,5 +59,6 @@ }, "passwordConfirm": { "title": "确认密码" - } + }, + "unknownApp": "未知应用" } diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index c50e0a0c8..ea1846766 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -120,7 +120,9 @@ "addAddress": "添加地址", "viewAll": "查看全部联系人", "selectContact": "选择联系人", - "selectAddress": "选择地址" + "selectAddress": "选择地址", + "addressCount": "{{count}}/{{max}}", + "defaultLabel": "默认" }, "acceptExchange": "接收交换", "account": "用户", @@ -388,7 +390,9 @@ "noWalletsSelected": "请选择至少一个钱包", "scanToAdd": "扫码添加我为联系人", "walletAddress": "{{wallet}} ({{chain}})", - "currentChain": "当前链" + "currentChain": "当前链", + "chainLabel": "({{chain}})", + "removeWallet": "移除 {{name}}" }, "time": { "justNow": "刚刚", @@ -406,6 +410,8 @@ "transactionsTitle": "地址交易查询", "chain": "链", "address": "地址", + "fromLabel": "来自:", + "toLabel": "发送至:", "addressPlaceholder": "输入钱包地址", "addressOrHash": "地址或交易哈希", "addressOrHashPlaceholder": "输入地址或交易哈希", @@ -419,6 +425,10 @@ "openExplorer": "打开 {{name}} 浏览器", "viewOnExplorer": "在 {{name}} 浏览器中查看" }, + "sign": { + "plus": "+", + "minus": "-" + }, "tabs": { "assets": "资产", "history": "交易" diff --git a/src/i18n/locales/zh-CN/devtools.json b/src/i18n/locales/zh-CN/devtools.json new file mode 100644 index 000000000..5ee1d13a1 --- /dev/null +++ b/src/i18n/locales/zh-CN/devtools.json @@ -0,0 +1,130 @@ +{ + "title": "Mock DevTools", + "tabs": { + "logs": "日志", + "breakpoints": "断点", + "console": "Console", + "settings": "设置" + }, + "button": { + "open": "打开 Mock DevTools(可拖动)" + }, + "footer": { + "mode": "Mock 模式", + "logs": "{{count}} 条日志", + "paused": "暂停 {{count}}" + }, + "logs": { + "filterPlaceholder": "过滤 service.method...", + "clear": "清除日志", + "empty": "暂无请求日志", + "input": "输入:", + "output": "输出:", + "error": "错误:", + "intercepted": "已被拦截修改" + }, + "settings": { + "defaultError": "模拟错误", + "globalDelay": "全局延迟", + "ms": "ms", + "delayMarks": { + "zero": "0", + "one": "1s", + "two": "2s", + "three": "3s+" + }, + "mockError": "模拟错误", + "errorPlaceholder": "错误信息(可选)", + "errorNotice": "所有请求将返回错误", + "resetAll": "重置所有设置", + "clearLogs": "清除日志", + "clearBreakpoints": "清除断点", + "tipTitle": "提示", + "tipDelay": "全局延迟会应用到所有 Mock 服务请求", + "tipError": "模拟错误会让所有请求抛出指定错误", + "tipBreakpoint": "断点可以在“断点”面板中单独配置" + }, + "breakpoints": { + "consoleLabel": "Console:", + "consoleView": "在 Console 中查看", + "resume": "继续", + "abort": "中止", + "ms": "ms", + "input": "input", + "output": "output", + "inputHint": "($input 可用)", + "outputHint": "($input, $output 可用)", + "pause": "暂停", + "pauseConditionPlaceholder": "条件表达式(可选)", + "inject": "注入", + "injectInputPlaceholder": "$input.text = 'modified'", + "injectOutputPlaceholder": "return { success: true }\n// 或 throw new Error('test')", + "summary": { + "delay": "延迟:{{ms}}ms", + "inputPause": "input:暂停", + "inputInject": "input:注入", + "outputPause": "output:暂停", + "outputInject": "output:注入" + }, + "addDialogTitle": "添加断点", + "add": "添加", + "title": "断点", + "pausedCount": "暂停中({{count}})", + "emptyTitle": "暂无断点", + "emptyHint": "点击“添加”创建断点" + }, + "console": { + "help": { + "sections": { + "commands": "命令", + "variables": "变量", + "methods": "方法", + "examples": "示例" + }, + "commands": { + "help": "显示帮助信息", + "clear": "清空 Console", + "vars": "显示可用变量", + "copy": "复制表达式结果到剪贴板" + }, + "variables": { + "paused": "暂停请求数组", + "indexed": "按索引访问暂停请求", + "latest": "最新的暂停请求", + "byId": "按 ID 访问,如 $p1, $p2" + }, + "methods": { + "resume": "继续执行,可传入修改后的值", + "abort": "中止请求" + }, + "examples": { + "latest": "查看最新暂停请求", + "input": "查看请求的输入", + "copy": "复制输入到剪贴板", + "resume": "继续执行" + } + }, + "vars": { + "available": "可用变量", + "pausedCount": "暂停数量" + }, + "copy": { + "usage": "用法: /copy ,例如: /copy $p1.$input", + "success": "已复制到剪贴板 ({{count}} 字符)", + "failure": "复制失败: {{message}}", + "expressionError": "表达式错误: {{message}}" + }, + "unknownCommand": "未知命令: {{command}}。输入 /help 查看帮助。", + "title": "Console", + "pausedCount": "暂停 {{count}}", + "clearTitle": "清空 Console (/clear)", + "empty": { + "titlePrefix": "Console 面板 - 输入", + "titleSuffix": "查看帮助", + "varsLabel": "变量:", + "vars": "变量:$paused, $0, $_, $p{id}" + }, + "inputPlaceholder": "输入表达式... (↑↓ 历史)", + "runTitle": "执行 (Enter)" + } +} diff --git a/src/i18n/locales/zh-CN/ecosystem.json b/src/i18n/locales/zh-CN/ecosystem.json index 09386c930..6e80236d6 100644 --- a/src/i18n/locales/zh-CN/ecosystem.json +++ b/src/i18n/locales/zh-CN/ecosystem.json @@ -8,7 +8,8 @@ "title": "可信源管理", "add": "添加订阅", "remove": "移除", - "empty": "暂无订阅源" + "empty": "暂无订阅源", + "defaultName": "Bio 官方生态" }, "permissions": { "title": "权限请求", @@ -120,6 +121,7 @@ "unknownDeveloper": "未知开发者", "preview": "预览", "supportedChains": "支持的区块链", + "beta": "测试版", "privacy": "应用隐私", "privacyHint": "开发者声明此应用可能会请求以下权限", "tags": "标签", @@ -141,6 +143,7 @@ "social": "社交", "exchange": "交易所", "other": "其他" - } + }, + "tagLabel": "#{{tag}}" } } diff --git a/src/i18n/locales/zh-CN/error.json b/src/i18n/locales/zh-CN/error.json index 1558f4f6a..8270d8bda 100644 --- a/src/i18n/locales/zh-CN/error.json +++ b/src/i18n/locales/zh-CN/error.json @@ -115,5 +115,14 @@ "mustBeArray": "signaturedata 必须是 JSON 数组", "emptyArray": "signaturedata 不能为空数组", "firstMustBeObject": "signaturedata[0] 必须是对象" + }, + "providerFallback": { + "title": "{{feature}} 查询失败", + "description": "所有已配置的提供方均失败,已显示默认值。", + "details": "技术详情" + }, + "boundary": { + "title": "出现了一些问题", + "retry": "重试" } } diff --git a/src/i18n/locales/zh-CN/onboarding.json b/src/i18n/locales/zh-CN/onboarding.json index 037e64acc..f210e76f7 100644 --- a/src/i18n/locales/zh-CN/onboarding.json +++ b/src/i18n/locales/zh-CN/onboarding.json @@ -188,7 +188,8 @@ "searchPlaceholder": "搜索链名称或符号", "noResults": "未找到匹配的链", "selectAll": "全选", - "deselectAll": "取消全选" + "deselectAll": "取消全选", + "selectionCount": "{{selected}}/{{total}}" }, "theme": { "walletName": "钱包名称", @@ -199,4 +200,4 @@ "similarExists": "已有相似颜色", "auto": "自动" } -} \ No newline at end of file +} diff --git a/src/i18n/locales/zh-CN/staking.json b/src/i18n/locales/zh-CN/staking.json index 9495a4892..018331a46 100644 --- a/src/i18n/locales/zh-CN/staking.json +++ b/src/i18n/locales/zh-CN/staking.json @@ -17,6 +17,7 @@ "burnDescription": "将内链代币转换为外链代币", "sourceChain": "源链", "targetChain": "目标链", + "targetChainLabel": "→ {{chain}}({{asset}})", "estimatedFee": "预估手续费", "transactionPending": "交易待处理", "transactionConfirming": "确认中", diff --git a/src/i18n/locales/zh-CN/transaction.json b/src/i18n/locales/zh-CN/transaction.json index 640216379..120099166 100644 --- a/src/i18n/locales/zh-CN/transaction.json +++ b/src/i18n/locales/zh-CN/transaction.json @@ -62,7 +62,9 @@ "twoStepSecretDescription": "该地址已设置安全密码,请输入安全密码确认转账。", "twoStepSecretPlaceholder": "输入安全密码", "twoStepSecretError": "安全密码错误", - "fee": "手续费" + "fee": "手续费", + "feeEstimating": "预估中...", + "feeUnavailable": "手续费不可用" }, "destroyPage": { "title": "销毁", @@ -342,4 +344,4 @@ "viewAll": "查看全部 {{count}} 条待处理交易", "clearAllFailed": "清除失败" } -} \ No newline at end of file +} diff --git a/src/i18n/locales/zh-CN/wallet.json b/src/i18n/locales/zh-CN/wallet.json index 770139a3a..8ac06b2c9 100644 --- a/src/i18n/locales/zh-CN/wallet.json +++ b/src/i18n/locales/zh-CN/wallet.json @@ -3,7 +3,8 @@ "defaultName": "我的钱包", "randomName": "随机名称", "carousel": { - "walletCount": "{{count}} 个钱包" + "walletCount": "{{count}} 个钱包", + "walletCount_one": "{{count}} 个钱包" }, "menu": { "addressBalanceQuery": "地址余额查询", @@ -200,5 +201,8 @@ "walletsUnderIdentity": "身份钱包", "willSynchronizeImportIntoThe_{chainname}ChainWithTheSamePrivateKey": "将同步导入 链", "{assetType}Details": "详情", - "{asset}TotalCirculation": "当前流通量" -} \ No newline at end of file + "{asset}TotalCirculation": "当前流通量", + "miniCard": { + "label": "钱包卡片" + } +} diff --git a/src/i18n/locales/zh-TW/authorize.json b/src/i18n/locales/zh-TW/authorize.json index a2fec4938..a470aad46 100644 --- a/src/i18n/locales/zh-TW/authorize.json +++ b/src/i18n/locales/zh-TW/authorize.json @@ -59,5 +59,6 @@ "title": { "address": "地址授權", "signature": "交易授權" - } + }, + "unknownApp": "未知應用" } diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 9d8f51fda..a79489d45 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -72,6 +72,7 @@ "addressPlaceholder": "輸入錢包地址", "balanceTitle": "地址余额查询", "chain": "鏈", + "fromLabel": "來自:", "error": "查詢失敗", "explorerHint": "交易記錄需要透過區塊瀏覽器查詢", "noTransactions": "暫無交易記錄", @@ -79,10 +80,15 @@ "openExplorer": "開啟 {{name}} 瀏覽器", "otherChains": "其他鏈", "queryError": "查詢交易失敗,請稍後重試", + "toLabel": "發送至:", "transactionsTitle": "地址交易查询", "useExplorerHint": "此鏈不支援直接查詢交易歷史,請使用瀏覽器查看", "viewOnExplorer": "在 {{name}} 瀏覽器中查看" }, + "sign": { + "plus": "+", + "minus": "-" + }, "addressPlaceholder": "輸入或貼上地址", "advancedEncryptionTechnologyDigitalWealthIsMoreSecure": "先進的加密技術 <br /> 數字財富更安全", "afterConfirmation_{appName}WillDeleteAllLocalDataTips": "確定後, 將刪除所有本地數據,所有錢包將從本地移除,確定退出?", @@ -152,7 +158,9 @@ "save": "儲存", "selectAddress": "選擇地址", "selectContact": "選擇聯絡人", - "viewAll": "查看全部聯絡人" + "viewAll": "查看全部聯絡人", + "addressCount": "{{count}}/{{max}}", + "defaultLabel": "預設" }, "contactCard": { "scanToAdd": "掃碼添加聯絡人", @@ -265,7 +273,9 @@ "selectWallets": "選擇錢包", "title": "我的名片", "usernamePlaceholder": "輸入使用者名稱", - "walletAddress": "{{wallet}} ({{chain}})" + "walletAddress": "{{wallet}} ({{chain}})", + "chainLabel": "({{chain}})", + "removeWallet": "移除 {{name}}" }, "name": "名稱", "navOverview": "概覽", diff --git a/src/i18n/locales/zh-TW/devtools.json b/src/i18n/locales/zh-TW/devtools.json new file mode 100644 index 000000000..e87d3ff93 --- /dev/null +++ b/src/i18n/locales/zh-TW/devtools.json @@ -0,0 +1,130 @@ +{ + "title": "Mock DevTools", + "tabs": { + "logs": "日誌", + "breakpoints": "斷點", + "console": "Console", + "settings": "設定" + }, + "button": { + "open": "開啟 Mock DevTools(可拖動)" + }, + "footer": { + "mode": "Mock 模式", + "logs": "{{count}} 條日誌", + "paused": "暫停 {{count}}" + }, + "logs": { + "filterPlaceholder": "過濾 service.method...", + "clear": "清除日誌", + "empty": "暫無請求日誌", + "input": "輸入:", + "output": "輸出:", + "error": "錯誤:", + "intercepted": "已被攔截修改" + }, + "settings": { + "defaultError": "模擬錯誤", + "globalDelay": "全域延遲", + "ms": "ms", + "delayMarks": { + "zero": "0", + "one": "1s", + "two": "2s", + "three": "3s+" + }, + "mockError": "模擬錯誤", + "errorPlaceholder": "錯誤訊息(選填)", + "errorNotice": "所有請求將返回錯誤", + "resetAll": "重置所有設定", + "clearLogs": "清除日誌", + "clearBreakpoints": "清除斷點", + "tipTitle": "提示", + "tipDelay": "全域延遲會套用到所有 Mock 服務請求", + "tipError": "模擬錯誤會讓所有請求拋出指定錯誤", + "tipBreakpoint": "斷點可在「斷點」面板中單獨設定" + }, + "breakpoints": { + "consoleLabel": "Console:", + "consoleView": "在 Console 中查看", + "resume": "繼續", + "abort": "中止", + "ms": "ms", + "input": "input", + "output": "output", + "inputHint": "($input 可用)", + "outputHint": "($input, $output 可用)", + "pause": "暫停", + "pauseConditionPlaceholder": "條件表達式(選填)", + "inject": "注入", + "injectInputPlaceholder": "$input.text = 'modified'", + "injectOutputPlaceholder": "return { success: true }\n// 或 throw new Error('test')", + "summary": { + "delay": "延遲:{{ms}}ms", + "inputPause": "input:暫停", + "inputInject": "input:注入", + "outputPause": "output:暫停", + "outputInject": "output:注入" + }, + "addDialogTitle": "新增斷點", + "add": "新增", + "title": "斷點", + "pausedCount": "暫停中({{count}})", + "emptyTitle": "暫無斷點", + "emptyHint": "點擊「新增」建立斷點" + }, + "console": { + "help": { + "sections": { + "commands": "命令", + "variables": "變數", + "methods": "方法", + "examples": "範例" + }, + "commands": { + "help": "顯示說明", + "clear": "清空 Console", + "vars": "顯示可用變數", + "copy": "複製表達式結果到剪貼簿" + }, + "variables": { + "paused": "暫停請求陣列", + "indexed": "依索引訪問暫停請求", + "latest": "最新的暫停請求", + "byId": "依 ID 訪問,例如 $p1, $p2" + }, + "methods": { + "resume": "繼續執行,可傳入修改後的值", + "abort": "中止請求" + }, + "examples": { + "latest": "查看最新暫停請求", + "input": "查看請求的輸入", + "copy": "複製輸入到剪貼簿", + "resume": "繼續執行" + } + }, + "vars": { + "available": "可用變數", + "pausedCount": "暫停數量" + }, + "copy": { + "usage": "用法: /copy ,例如: /copy $p1.$input", + "success": "已複製到剪貼簿 ({{count}} 字元)", + "failure": "複製失敗: {{message}}", + "expressionError": "表達式錯誤: {{message}}" + }, + "unknownCommand": "未知命令: {{command}}。輸入 /help 查看說明。", + "title": "Console", + "pausedCount": "暫停 {{count}}", + "clearTitle": "清空 Console (/clear)", + "empty": { + "titlePrefix": "Console 面板 - 輸入", + "titleSuffix": "查看說明", + "varsLabel": "變數:", + "vars": "變數:$paused, $0, $_, $p{id}" + }, + "inputPlaceholder": "輸入表達式... (↑↓ 歷史)", + "runTitle": "執行 (Enter)" + } +} diff --git a/src/i18n/locales/zh-TW/ecosystem.json b/src/i18n/locales/zh-TW/ecosystem.json index 7261ebec4..b6a53d825 100644 --- a/src/i18n/locales/zh-TW/ecosystem.json +++ b/src/i18n/locales/zh-TW/ecosystem.json @@ -8,7 +8,8 @@ "title": "可信源管理", "add": "添加訂閱", "remove": "移除", - "empty": "暫無訂閱源" + "empty": "暫無訂閱源", + "defaultName": "Bio 官方生態" }, "permissions": { "title": "權限請求", @@ -120,6 +121,7 @@ "unknownDeveloper": "未知開發者", "preview": "預覽", "supportedChains": "支援的區塊鏈", + "beta": "測試版", "privacy": "應用隱私", "privacyHint": "開發者聲明此應用可能會請求以下權限", "tags": "標籤", @@ -141,6 +143,7 @@ "social": "社交", "exchange": "交易所", "other": "其他" - } + }, + "tagLabel": "#{{tag}}" } } diff --git a/src/i18n/locales/zh-TW/error.json b/src/i18n/locales/zh-TW/error.json index c2bbbbaa8..8854f0437 100644 --- a/src/i18n/locales/zh-TW/error.json +++ b/src/i18n/locales/zh-TW/error.json @@ -115,5 +115,14 @@ "mustBeArray": "signaturedata 必須是 JSON 數組", "emptyArray": "signaturedata 不能為空數組", "firstMustBeObject": "signaturedata[0] 必須是對象" + }, + "providerFallback": { + "title": "{{feature}} 查詢失敗", + "description": "所有已設定的提供方皆失敗,已顯示預設值。", + "details": "技術詳情" + }, + "boundary": { + "title": "發生了一些問題", + "retry": "重試" } } diff --git a/src/i18n/locales/zh-TW/onboarding.json b/src/i18n/locales/zh-TW/onboarding.json index 11a031816..1b1d0dcf8 100644 --- a/src/i18n/locales/zh-TW/onboarding.json +++ b/src/i18n/locales/zh-TW/onboarding.json @@ -188,7 +188,8 @@ "searchPlaceholder": "搜尋鏈名稱或符號", "noResults": "未找到匹配的鏈", "selectAll": "全選", - "deselectAll": "取消全選" + "deselectAll": "取消全選", + "selectionCount": "{{selected}}/{{total}}" }, "theme": { "walletName": "錢包名稱", @@ -199,4 +200,4 @@ "similarExists": "已有相似顏色", "auto": "自動" } -} \ No newline at end of file +} diff --git a/src/i18n/locales/zh-TW/staking.json b/src/i18n/locales/zh-TW/staking.json index 0b1121b47..db9f25177 100644 --- a/src/i18n/locales/zh-TW/staking.json +++ b/src/i18n/locales/zh-TW/staking.json @@ -17,6 +17,7 @@ "burnDescription": "將內鏈代幣轉換為外鏈代幣", "sourceChain": "源鏈", "targetChain": "目標鏈", + "targetChainLabel": "→ {{chain}}({{asset}})", "estimatedFee": "預估手續費", "transactionPending": "交易待處理", "transactionConfirming": "確認中", diff --git a/src/i18n/locales/zh-TW/transaction.json b/src/i18n/locales/zh-TW/transaction.json index 4edcdd4cd..faa318777 100644 --- a/src/i18n/locales/zh-TW/transaction.json +++ b/src/i18n/locales/zh-TW/transaction.json @@ -229,6 +229,8 @@ "continue": "繼續", "explorerNotImplemented": "區塊瀏覽器功能待實現", "fee": "手續費", + "feeEstimating": "預估中...", + "feeUnavailable": "手續費不可用", "from": "發送地址", "networkWarning": "請確保收款地址為 {{chain}} 網絡地址,發送到錯誤網絡將無法找回", "resultTitle": "發送結果", diff --git a/src/i18n/locales/zh-TW/wallet.json b/src/i18n/locales/zh-TW/wallet.json index 5a3ddd57f..66a6256ac 100644 --- a/src/i18n/locales/zh-TW/wallet.json +++ b/src/i18n/locales/zh-TW/wallet.json @@ -5,7 +5,8 @@ "defaultName": "我的錢包", "randomName": "隨機名稱", "carousel": { - "walletCount": "{{count}} 個錢包" + "walletCount": "{{count}} 個錢包", + "walletCount_one": "{{count}} 個錢包" }, "menu": { "addressBalanceQuery": "地址餘額查詢", @@ -199,5 +200,8 @@ "walletsUnderIdentity": "身份錢包", "willSynchronizeImportIntoThe_{chainname}ChainWithTheSamePrivateKey": "將同步導入 鏈", "{assetType}Details": "詳情", - "{asset}TotalCirculation": "當前流通量" -} \ No newline at end of file + "{asset}TotalCirculation": "當前流通量", + "miniCard": { + "label": "錢包卡片" + } +} diff --git a/src/lib/address-format.ts b/src/lib/address-format.ts index 8dc8d28e9..8cb4cc4ac 100644 --- a/src/lib/address-format.ts +++ b/src/lib/address-format.ts @@ -5,6 +5,7 @@ */ import type { ChainType } from '@/stores' +import { chainConfigService } from '@/services/chain-config/service' import { isValidBioforestAddress, isBioforestChain, type BioforestChainType } from './crypto' /** Address format detection result */ @@ -104,6 +105,27 @@ export function detectAddressFormat(address: string): AddressFormatInfo { * Check if address is valid for a specific chain */ export function isValidAddressForChain(address: string, chainType: ChainType): boolean { + const config = chainConfigService.getConfig(chainType) + if (config) { + if (config.chainKind === 'bioforest') { + return isValidBioforestAddress(address) + } + + const info = detectAddressFormat(address) + if (!info.isValid || info.chainType === null) return false + + switch (config.chainKind) { + case 'evm': + return info.chainType === 'ethereum' + case 'tron': + return info.chainType === 'tron' + case 'bitcoin': + return info.chainType === 'bitcoin' + default: + return false + } + } + const info = detectAddressFormat(address) if (!info.isValid) return false if (info.chainType === null) return false diff --git a/src/lib/error-capture.ts b/src/lib/error-capture.ts index 3867c1c06..8afb4706a 100644 --- a/src/lib/error-capture.ts +++ b/src/lib/error-capture.ts @@ -116,6 +116,15 @@ export function clearCapturedErrors(): void { sessionStorage.removeItem(STORAGE_KEY) } +export function captureError(error: Error, errorInfo?: { componentStack?: string }): void { + saveError({ + timestamp: formatTimestamp(), + type: 'error', + message: error.message, + stack: [error.stack, errorInfo?.componentStack].filter(Boolean).join('\n'), + }) +} + export function printCapturedErrors(): void { const errors = getErrors() console.log(`[ErrorCapture] ${errors.length} captured errors:`) diff --git a/src/pages/address-transactions/index.tsx b/src/pages/address-transactions/index.tsx index 60ce2ef7c..072212360 100644 --- a/src/pages/address-transactions/index.tsx +++ b/src/pages/address-transactions/index.tsx @@ -23,11 +23,14 @@ function formatDate(timestamp: number): string { } function TransactionItem({ tx, address }: { tx: Transaction; address: string }) { + const { t } = useTranslation('common') const isOutgoing = tx.direction === 'out' || tx.from.toLowerCase() === address.toLowerCase() const primaryAsset = tx.assets.find((a) => a.assetType === 'native' || a.assetType === 'token') const value = primaryAsset ? primaryAsset.value : '0' const symbol = primaryAsset ? primaryAsset.symbol : '' const decimals = primaryAsset ? primaryAsset.decimals : 0 + const sign = isOutgoing ? t('sign.minus') : t('sign.plus') + const directionLabel = isOutgoing ? t('addressLookup.toLabel') : t('addressLookup.fromLabel') return (
@@ -36,14 +39,15 @@ function TransactionItem({ tx, address }: { tx: Transaction; address: string })
- {isOutgoing ? `To: ${tx.to}` : `From: ${tx.from}`} + {directionLabel} {isOutgoing ? tx.to : tx.from}
{formatDate(tx.timestamp)}
- {isOutgoing ? '-' : '+'}{formatAmount(value, decimals)} {symbol} + {sign} + {formatAmount(value, decimals)} {symbol}
) diff --git a/src/pages/authorize/address.tsx b/src/pages/authorize/address.tsx index b93d459c5..58c2a82db 100644 --- a/src/pages/authorize/address.tsx +++ b/src/pages/authorize/address.tsx @@ -19,27 +19,10 @@ import { type CallerAppInfo, } from '@/services/authorize' import { useToast } from '@/services' -import { walletStore, walletSelectors, type Wallet } from '@/stores' +import { useChainNameMap, walletStore, walletSelectors, type Wallet } from '@/stores' const REQUEST_TIMEOUT_MS = 5 * 60 * 1000 -const CHAIN_NAMES: Record = { - ethereum: 'Ethereum', - bitcoin: 'Bitcoin', - tron: 'Tron', - binance: 'BSC', - bsc: 'BSC', - bfmeta: 'BFMeta', - ccchain: 'CCChain', - pmchain: 'PMChain', - bfchainv2: 'BFChain V2', - btgmeta: 'BTGMeta', - biwmeta: 'BIWMeta', - ethmeta: 'ETHMeta', - malibu: 'Malibu', - ccc: 'CCChain', -} - function toChainIconType(chainName: string | undefined): ChainIconType | undefined { if (!chainName) return undefined return chainName @@ -60,7 +43,11 @@ function toWalletSelectorItems(wallets: Wallet[]): WalletInfo[] { })) } -function buildChainData(wallets: Wallet[], currentWalletId: string | null): ChainData[] { +function buildChainData( + wallets: Wallet[], + currentWalletId: string | null, + chainNameMap: Record +): ChainData[] { const grouped = new Map>() for (const wallet of wallets) { @@ -76,7 +63,7 @@ function buildChainData(wallets: Wallet[], currentWalletId: string | null): Chai return Array.from(grouped.entries()).map(([chain, addresses]) => ({ chain, - name: CHAIN_NAMES[chain] ?? chain, + name: chainNameMap[chain] ?? chain, addresses: addresses.map((a) => ({ address: a.address, isDefault: a.isDefault })), })) } @@ -104,6 +91,7 @@ export function AddressAuthPage() { const currentWalletId = useStore(walletStore, (s) => s.currentWalletId) const wallets = useStore(walletStore, (s) => s.wallets) const currentWallet = useStore(walletStore, walletSelectors.getCurrentWallet) + const chainNameMap = useChainNameMap() const [appInfo, setAppInfo] = useState(null) const [loadError, setLoadError] = useState(null) @@ -189,7 +177,10 @@ export function AddressAuthPage() { return wallets.find((w) => w.id === selectedWalletId) ?? null }, [selectedWalletId, wallets]) - const chains = useMemo(() => buildChainData(wallets, currentWalletId), [wallets, currentWalletId]) + const chains = useMemo( + () => buildChainData(wallets, currentWalletId, chainNameMap), + [chainNameMap, currentWalletId, wallets] + ) // 当前钱包可用的链列表 const availableChains = useMemo(() => { @@ -371,7 +362,7 @@ export function AddressAuthPage() { {type === 'main' && tAuthorize('address.scope.main')} {type === 'network' && tAuthorize('address.scope.network', { - chainName: CHAIN_NAMES[selectedChain ?? ''] ?? (selectedChain ?? ''), + chainName: (selectedChain ? chainNameMap[selectedChain] : undefined) ?? (selectedChain ?? ''), })} {type === 'all' && tAuthorize('address.scope.all')}
@@ -392,7 +383,7 @@ export function AddressAuthPage() { setChainSelectorOpen(true)} @@ -502,7 +493,7 @@ export function AddressAuthPage() { >
-
{CHAIN_NAMES[chain] ?? chain}
+
{chainNameMap[chain] ?? chain}
{chainAddr?.address ? truncateAddress(chainAddr.address) : '---'}
diff --git a/src/pages/destroy/index.tsx b/src/pages/destroy/index.tsx index 2e35b36a5..02e61fa85 100644 --- a/src/pages/destroy/index.tsx +++ b/src/pages/destroy/index.tsx @@ -24,27 +24,13 @@ import { ChainProviderGate, useChainProvider } from '@/contexts'; import { useChainConfigState, chainConfigSelectors, + useChainDisplayName, useCurrentChainAddress, useCurrentWallet, useSelectedChain, type ChainType, } from '@/stores'; -const CHAIN_NAMES: Record = { - ethereum: 'Ethereum', - bitcoin: 'Bitcoin', - tron: 'Tron', - binance: 'BSC', - bfmeta: 'BFMeta', - ccchain: 'CCChain', - pmchain: 'PMChain', - bfchainv2: 'BFChain V2', - btgmeta: 'BTGMeta', - biwmeta: 'BIWMeta', - ethmeta: 'ETHMeta', - malibu: 'Malibu', -}; - /** Convert TokenInfo to AssetInfo */ function tokenToAsset(token: TokenInfo): AssetInfo { return { @@ -89,7 +75,7 @@ function DestroyPageContent() { const chainConfig = chainConfigState.snapshot ? chainConfigSelectors.getChainById(chainConfigState, selectedChain) : null; - const selectedChainName = chainConfig?.name ?? CHAIN_NAMES[selectedChain] ?? selectedChain; + const selectedChainName = useChainDisplayName(selectedChain); // 使用 useChainProvider() 获取确保非空的 provider const chainProvider = useChainProvider(); diff --git a/src/pages/my-card/index.tsx b/src/pages/my-card/index.tsx index 66113c87d..c0f2ab387 100644 --- a/src/pages/my-card/index.tsx +++ b/src/pages/my-card/index.tsx @@ -23,6 +23,7 @@ import { userProfileActions, useWallets, useChainPreferences, + useChainNameMap, type Wallet, type ChainType, } from '@/stores'; @@ -38,31 +39,16 @@ import { WalletPickerSheet } from './wallet-picker-sheet'; import { resolveBackgroundStops } from '@/components/wallet/refraction'; import { useSnapdomShare } from '@/hooks/useSnapdomShare'; -const CHAIN_NAMES: Record = { - ethereum: 'ETH', - bitcoin: 'BTC', - tron: 'TRX', - binance: 'BSC', - bfmeta: 'BFMeta', - ccchain: 'CCChain', - pmchain: 'PMChain', - bfchainv2: 'BFChain V2', - btgmeta: 'BTGMeta', - biwmeta: 'BIWMeta', - ethmeta: 'ETHMeta', - malibu: 'Malibu', -}; - - - export function MyCardPage() { const { t } = useTranslation(['common', 'settings']); const { goBack } = useNavigation(); + const inputRef = useRef(null); const cardRef = useRef(null); const profile = useUserProfile(); const wallets = useWallets(); const chainPreferences = useChainPreferences(); + const chainNameMap = useChainNameMap(); const [isEditingUsername, setIsEditingUsername] = useState(false); const [usernameInput, setUsernameInput] = useState(profile.username); @@ -107,10 +93,12 @@ export function MyCardPage() { // Generate addresses for QR code const addresses: ContactAddressInfo[] = useMemo(() => { - return selectedWalletsWithAddresses.map(({ wallet, address }) => ({ - address: address!, - label: wallet.name, // Only wallet name, color indicates address type - })); + return selectedWalletsWithAddresses + .filter((item): item is { wallet: Wallet; chain: ChainType; address: string } => !!item.address) + .map(({ wallet, address }) => ({ + address, + label: wallet.name, // Only wallet name, color indicates address type + })); }, [selectedWalletsWithAddresses]); // Generate QR content @@ -147,6 +135,12 @@ export function MyCardPage() { } }, [handleUsernameSave]); + useEffect(() => { + if (isEditingUsername) { + inputRef.current?.focus(); + } + }, [isEditingUsername]); + // Snapdom share hook const { isProcessing: isDownloading, download: handleDownload, share: handleShare } = useSnapdomShare( cardRef, @@ -181,13 +175,13 @@ export function MyCardPage() { {isEditingUsername ? (
setUsernameInput(e.target.value)} onBlur={handleUsernameSave} onKeyDown={handleUsernameKeyDown} placeholder={t('myCard.usernamePlaceholder')} className="w-48 text-center" - autoFocus />
) : ( @@ -232,12 +226,14 @@ export function MyCardPage() { style={{ backgroundColor: c0, color: '#FFFFFF' }} > {wallet.name} - ({CHAIN_NAMES[chain] || chain}) + + {t('myCard.chainLabel', { chain: chainNameMap[chain] || chain })} + diff --git a/src/pages/my-card/wallet-picker-sheet.tsx b/src/pages/my-card/wallet-picker-sheet.tsx index c6c3938a2..de12eefeb 100644 --- a/src/pages/my-card/wallet-picker-sheet.tsx +++ b/src/pages/my-card/wallet-picker-sheet.tsx @@ -15,25 +15,10 @@ import { useChainPreferences, useSelectedWalletIds, useCanAddMoreWallets, + useChainNameMap, userProfileActions, - type ChainType, } from '@/stores'; -const CHAIN_NAMES: Record = { - ethereum: 'Ethereum', - bitcoin: 'Bitcoin', - tron: 'Tron', - binance: 'BSC', - bfmeta: 'BFMeta', - ccchain: 'CCChain', - pmchain: 'PMChain', - bfchainv2: 'BFChain V2', - btgmeta: 'BTGMeta', - biwmeta: 'BIWMeta', - ethmeta: 'ETHMeta', - malibu: 'Malibu', -}; - interface WalletPickerSheetProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -45,6 +30,7 @@ export function WalletPickerSheet({ open, onOpenChange }: WalletPickerSheetProps const chainPreferences = useChainPreferences(); const selectedWalletIds = useSelectedWalletIds(); const canAddMore = useCanAddMoreWallets(); + const chainNameMap = useChainNameMap(); const handleWalletToggle = useCallback((walletId: string) => { const isSelected = selectedWalletIds.includes(walletId); @@ -87,7 +73,7 @@ export function WalletPickerSheet({ open, onOpenChange }: WalletPickerSheetProps
{wallet.name}
- {CHAIN_NAMES[selectedChain] || selectedChain} + {chainNameMap[selectedChain] || selectedChain} {chainAddress?.address.slice(0, 8)}...{chainAddress?.address.slice(-6)} diff --git a/src/pages/receive/index.tsx b/src/pages/receive/index.tsx index db281f644..acb21f784 100644 --- a/src/pages/receive/index.tsx +++ b/src/pages/receive/index.tsx @@ -16,22 +16,7 @@ import { IconDownload as Download, IconLoader2 as Loader, } from '@tabler/icons-react'; -import { useCurrentChainAddress, useSelectedChain, useUserProfile, type ChainType } from '@/stores'; - -const CHAIN_NAMES: Record = { - ethereum: 'Ethereum', - bitcoin: 'Bitcoin', - tron: 'Tron', - binance: 'BSC', - bfmeta: 'BFMeta', - ccchain: 'CCChain', - pmchain: 'PMChain', - bfchainv2: 'BFChain V2', - btgmeta: 'BTGMeta', - biwmeta: 'BIWMeta', - ethmeta: 'ETHMeta', - malibu: 'Malibu', -}; +import { useChainDisplayName, useCurrentChainAddress, useSelectedChain, useUserProfile } from '@/stores'; export function ReceivePage() { const { t } = useTranslation(['transaction', 'common']); @@ -42,7 +27,7 @@ export function ReceivePage() { const chainAddress = useCurrentChainAddress(); const selectedChain = useSelectedChain(); - const selectedChainName = CHAIN_NAMES[selectedChain] ?? selectedChain; + const selectedChainName = useChainDisplayName(selectedChain); const profile = useUserProfile(); const [copied, setCopied] = useState(false); diff --git a/src/pages/send/index.tsx b/src/pages/send/index.tsx index 3c42017ff..eb3075585 100644 --- a/src/pages/send/index.tsx +++ b/src/pages/send/index.tsx @@ -17,33 +17,19 @@ import { GradientButton, Alert } from '@/components/common'; import { ChainIcon } from '@/components/wallet/chain-icon'; import { useToast, useHaptics } from '@/services'; import { useSend } from '@/hooks/use-send'; +import { adjustAmountForFee } from '@/hooks/use-send.logic'; import { Amount } from '@/types/amount'; import { IconChevronRight as ArrowRight } from '@tabler/icons-react'; import { ChainProviderGate, useChainProvider } from '@/contexts'; import { useChainConfigState, chainConfigSelectors, + useChainDisplayName, useCurrentChainAddress, useCurrentWallet, useSelectedChain, - type ChainType, } from '@/stores'; -const CHAIN_NAMES: Record = { - ethereum: 'Ethereum', - bitcoin: 'Bitcoin', - tron: 'Tron', - binance: 'BSC', - bfmeta: 'BFMeta', - ccchain: 'CCChain', - pmchain: 'PMChain', - bfchainv2: 'BFChain V2', - btgmeta: 'BTGMeta', - biwmeta: 'BIWMeta', - ethmeta: 'ETHMeta', - malibu: 'Malibu', -}; - // ==================== 内部内容组件 ==================== function SendPageContent() { @@ -76,7 +62,7 @@ function SendPageContent() { const chainConfig = chainConfigState.snapshot ? chainConfigSelectors.getChainById(chainConfigState, selectedChain) : null; - const selectedChainName = chainConfig?.name ?? CHAIN_NAMES[selectedChain] ?? selectedChain; + const selectedChainName = useChainDisplayName(selectedChain); // 使用 useChainProvider() 获取确保非空的 provider const chainProvider = useChainProvider(); @@ -225,6 +211,19 @@ function SendPageContent() { return getBalance(state.asset.assetType); }, [getBalance, state.asset]); const symbol = state.asset?.assetType ?? 'TOKEN'; + const feeSymbol = state.feeSymbol || chainConfig?.symbol; + const isFeeSameAsset = !!feeSymbol && feeSymbol === symbol; + const maxAmount = useMemo(() => { + if (!state.asset || !balance) return undefined; + if (!state.feeAmount) return balance; + const assetForMax = { ...state.asset, amount: balance, decimals: balance.decimals }; + const result = adjustAmountForFee(balance, assetForMax, state.feeAmount); + if (result.status === 'ok') { + return result.adjustedAmount ?? balance; + } + return Amount.zero(balance.decimals, assetForMax.assetType); + }, [balance, state.asset, state.feeAmount]); + const maxDisabled = isFeeSameAsset && (!state.feeAmount || state.feeLoading); const handleOpenScanner = useCallback(() => { // 设置扫描结果回调 @@ -403,11 +402,25 @@ function SendPageContent() { value={state.amount ?? undefined} onChange={setAmount} balance={balance ?? undefined} + max={maxAmount} + maxDisabled={maxDisabled} symbol={symbol} error={state.amountError ?? undefined} fiatValue={state.amount ? state.amount.toNumber().toFixed(2) : undefined} /> + {/* Fee estimate */} +
+ {t('sendPage.fee')} + + {state.feeLoading + ? t('sendPage.feeEstimating') + : state.feeAmount + ? `${state.feeAmount.toFormatted()} ${state.feeSymbol || symbol}` + : t('sendPage.feeUnavailable')} + +
+ {/* Network warning */} {t('sendPage.networkWarning', { chain: selectedChainName })} diff --git a/src/pages/staking/components/burn-form.tsx b/src/pages/staking/components/burn-form.tsx index d680abddd..a6978f415 100644 --- a/src/pages/staking/components/burn-form.tsx +++ b/src/pages/staking/components/burn-form.tsx @@ -14,6 +14,7 @@ import { stakingService } from '@/services/staking'; import { Amount } from '@/types/amount'; import type { ExternalChain, InternalChain, RechargeConfig, BurnRequest } from '@/types/staking'; import { cn } from '@/lib/utils'; +import { useChainNameMap } from '@/stores'; interface BurnFormProps { onSuccess?: (txId: string) => void; @@ -23,15 +24,11 @@ interface BurnFormProps { /** Available source (internal) chain options */ const SOURCE_CHAINS: InternalChain[] = ['BFMeta', 'BFChain', 'CCChain', 'PMChain']; -/** Chain display names */ -const CHAIN_NAMES: Record = { +/** External chain display names */ +const EXTERNAL_CHAIN_NAMES: Record = { ETH: 'Ethereum', BSC: 'BNB Chain', TRON: 'Tron', - BFMeta: 'BFMeta', - BFChain: 'BFChain', - CCChain: 'CCChain', - PMChain: 'PMChain', }; /** Mock internal chain balances for validation */ @@ -80,6 +77,7 @@ function getAvailableTokens(config: RechargeConfig | null, sourceChain: Internal /** Burn form component */ export function BurnForm({ onSuccess, className }: BurnFormProps) { const { t } = useTranslation('staking'); + const chainNameMap = useChainNameMap(); // Form state const [sourceChain, setSourceChain] = useState('BFMeta'); @@ -114,6 +112,16 @@ export function BurnForm({ onSuccess, className }: BurnFormProps) { // Available tokens for selected source chain const availableTokens = useMemo(() => getAvailableTokens(config, sourceChain), [config, sourceChain]); + const getChainName = useCallback( + (chain: string): string => { + const externalName = EXTERNAL_CHAIN_NAMES[chain as ExternalChain]; + if (externalName) return externalName; + const lowerKey = chain.toLowerCase(); + return chainNameMap[lowerKey] ?? chainNameMap[chain] ?? chain; + }, + [chainNameMap], + ); + // Currently selected token's target chains const selectedTokenTargets = useMemo(() => { if (!sourceAsset) return []; @@ -233,7 +241,7 @@ export function BurnForm({ onSuccess, className }: BurnFormProps) {
{sourceChain.charAt(0)}
- {CHAIN_NAMES[sourceChain]} + {getChainName(sourceChain)}
@@ -303,7 +311,7 @@ export function BurnForm({ onSuccess, className }: BurnFormProps) {
{targetChain ? targetChain.charAt(0) : '?'}
- {targetChain ? CHAIN_NAMES[targetChain] : t('selectChain')} + {targetChain ? getChainName(targetChain) : t('selectChain')}
@@ -352,7 +360,7 @@ export function BurnForm({ onSuccess, className }: BurnFormProps) {
{chain.charAt(0)}
- {CHAIN_NAMES[chain]} + {getChainName(chain)} {sourceChain === chain && } ))} @@ -414,7 +422,7 @@ export function BurnForm({ onSuccess, className }: BurnFormProps) {
{chain.charAt(0)}
- {CHAIN_NAMES[chain]} + {getChainName(chain)} {targetChain === chain && } ))} diff --git a/src/pages/staking/components/mint-form.tsx b/src/pages/staking/components/mint-form.tsx index caf88f115..845b4fb7d 100644 --- a/src/pages/staking/components/mint-form.tsx +++ b/src/pages/staking/components/mint-form.tsx @@ -14,6 +14,7 @@ import { stakingService } from '@/services/staking'; import { Amount } from '@/types/amount'; import type { ExternalChain, InternalChain, RechargeConfig, MintRequest } from '@/types/staking'; import { cn } from '@/lib/utils'; +import { useChainNameMap } from '@/stores'; interface MintFormProps { onSuccess?: (txId: string) => void; @@ -23,15 +24,11 @@ interface MintFormProps { /** Available source chain options */ const SOURCE_CHAINS: ExternalChain[] = ['ETH', 'BSC', 'TRON']; -/** Chain display names */ -const CHAIN_NAMES: Record = { +/** External chain display names */ +const EXTERNAL_CHAIN_NAMES: Record = { ETH: 'Ethereum', BSC: 'BNB Chain', TRON: 'Tron', - BFMeta: 'BFMeta', - BFChain: 'BFChain', - CCChain: 'CCChain', - PMChain: 'PMChain', }; /** Mock balances for validation */ @@ -71,6 +68,7 @@ function getAvailableTokens( /** Mint form component */ export function MintForm({ onSuccess, className }: MintFormProps) { const { t } = useTranslation('staking'); + const chainNameMap = useChainNameMap(); // Form state const [sourceChain, setSourceChain] = useState('BSC'); @@ -105,6 +103,16 @@ export function MintForm({ onSuccess, className }: MintFormProps) { // Available tokens for selected source chain const availableTokens = useMemo(() => getAvailableTokens(config, sourceChain), [config, sourceChain]); + const getChainName = useCallback( + (chain: string): string => { + const externalName = EXTERNAL_CHAIN_NAMES[chain as ExternalChain]; + if (externalName) return externalName; + const lowerKey = chain.toLowerCase(); + return chainNameMap[lowerKey] ?? chainNameMap[chain] ?? chain; + }, + [chainNameMap], + ); + // Set default token when chain changes useEffect(() => { const firstToken = availableTokens[0]; @@ -208,7 +216,7 @@ export function MintForm({ onSuccess, className }: MintFormProps) {
{sourceChain.charAt(0)}
- {CHAIN_NAMES[sourceChain]} + {getChainName(sourceChain)}
@@ -273,7 +281,7 @@ export function MintForm({ onSuccess, className }: MintFormProps) {
{targetChain.charAt(0) || '?'}
- {CHAIN_NAMES[targetChain] || t('selectToken')} + {targetChain ? getChainName(targetChain) : t('selectToken')}
@@ -321,7 +329,7 @@ export function MintForm({ onSuccess, className }: MintFormProps) {
{chain.charAt(0)}
- {CHAIN_NAMES[chain]} + {getChainName(chain)} {sourceChain === chain && } ))} @@ -352,7 +360,10 @@ export function MintForm({ onSuccess, className }: MintFormProps) {
{token.asset}
- → {CHAIN_NAMES[token.targetChain]} ({token.targetAsset}) + {t('targetChainLabel', { + chain: getChainName(token.targetChain), + asset: token.targetAsset, + })}
diff --git a/src/pages/staking/components/staking-record-list.tsx b/src/pages/staking/components/staking-record-list.tsx index 08c184cfe..dea7e42c8 100644 --- a/src/pages/staking/components/staking-record-list.tsx +++ b/src/pages/staking/components/staking-record-list.tsx @@ -19,6 +19,7 @@ import { stakingService } from '@/services/staking'; import type { Amount } from '@/types/amount'; import type { StakingTransaction, StakingTxType, StakingTxStatus } from '@/types/staking'; import { cn } from '@/lib/utils'; +import { useChainNameMap } from '@/stores'; interface StakingRecordListProps { /** Filter by transaction type */ @@ -27,15 +28,11 @@ interface StakingRecordListProps { className?: string; } -/** Chain display names */ -const CHAIN_NAMES: Record = { +/** External chain display names */ +const EXTERNAL_CHAIN_NAMES: Record = { ETH: 'Ethereum', BSC: 'BNB Chain', TRON: 'Tron', - BFMeta: 'BFMeta', - BFChain: 'BFChain', - CCChain: 'CCChain', - PMChain: 'PMChain', }; /** Status icon component */ @@ -76,9 +73,19 @@ function formatAmount(amount: Amount): string { return num.toFixed(4); } +function resolveChainName(chain: string, chainNameMap: Record): string { + const externalName = EXTERNAL_CHAIN_NAMES[chain]; + if (externalName) return externalName; + const lowerKey = chain.toLowerCase(); + return chainNameMap[lowerKey] ?? chainNameMap[chain] ?? chain; +} + /** Single record item */ function RecordItem({ record, onClick }: { record: StakingTransaction; onClick?: () => void }) { const { t } = useTranslation('staking'); + const chainNameMap = useChainNameMap(); + const sourceChainName = resolveChainName(record.sourceChain, chainNameMap); + const targetChainName = resolveChainName(record.targetChain, chainNameMap); const isMint = record.type === 'mint'; const TypeIcon = isMint ? ArrowDownRight : ArrowUpRight; @@ -106,9 +113,9 @@ function RecordItem({ record, onClick }: { record: StakingTransaction; onClick?:
- {CHAIN_NAMES[record.sourceChain]} + {sourceChainName} - {CHAIN_NAMES[record.targetChain]} + {targetChainName}
diff --git a/src/services/chain-adapter/debug.ts b/src/services/chain-adapter/debug.ts index 67e6b30a3..14178f232 100644 --- a/src/services/chain-adapter/debug.ts +++ b/src/services/chain-adapter/debug.ts @@ -1,4 +1,5 @@ type DebugSetting = boolean | string | undefined +type DebugLogger = (message: string, detail?: unknown) => void function readLocalStorageSetting(): DebugSetting { if (typeof globalThis === "undefined") return undefined @@ -26,6 +27,14 @@ function readGlobalSetting(): DebugSetting { return undefined } +function readGlobalLogger(): DebugLogger | undefined { + if (typeof globalThis === "undefined") return undefined + const store = globalThis as typeof globalThis & { __CHAIN_EFFECT_LOG__?: unknown } + const value = store.__CHAIN_EFFECT_LOG__ + if (typeof value === "function") return value as DebugLogger + return undefined +} + function parseRegex(pattern: string): RegExp | null { if (!pattern.startsWith("/")) return null const lastSlash = pattern.lastIndexOf("/") @@ -47,3 +56,10 @@ export function isChainDebugEnabled(message: string): boolean { if (regex) return regex.test(message) return message.includes(setting) } + +export function logChainDebug(message: string, detail?: unknown): void { + if (!isChainDebugEnabled(message)) return + const logger = readGlobalLogger() + if (!logger) return + logger(message, detail) +} diff --git a/src/services/mock-devtools/components/BreakpointPanel.tsx b/src/services/mock-devtools/components/BreakpointPanel.tsx index e84212f2e..69db4a9d2 100644 --- a/src/services/mock-devtools/components/BreakpointPanel.tsx +++ b/src/services/mock-devtools/components/BreakpointPanel.tsx @@ -2,7 +2,8 @@ * 断点面板 - Chrome DevTools 风格的断点调试界面 */ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' import { IconPlayerPause as Pause, IconPlayerPlay as Play, @@ -50,6 +51,7 @@ function PausedRequestCard({ request: PausedRequest onOpenConsole?: ((command: string) => void) | undefined }) { + const { t } = useTranslation('devtools') // 从 id 中提取数字,如 "req-5" -> "5" const idNum = request.id.match(/\d+/)?.[0] || request.id const varName = `$p${idNum}` @@ -67,12 +69,12 @@ function PausedRequestCard({
- Console: + {t('breakpoints.consoleLabel')} {varName} @@ -84,14 +86,14 @@ function PausedRequestCard({ className="flex flex-1 items-center justify-center gap-1 rounded bg-green-500 py-1 text-xs text-white hover:bg-green-600" > - 继续 + {t('breakpoints.resume')}
@@ -108,6 +110,7 @@ function BreakpointCard({ onUpdate: (bp: BreakpointConfig) => void onRemove: () => void }) { + const { t } = useTranslation('devtools') const [expanded, setExpanded] = useState(false) const updateInput = (updates: Partial>) => { @@ -156,11 +159,11 @@ function BreakpointCard({ type="number" value={bp.delayMs || ''} onChange={(e) => onUpdate({ ...bp, delayMs: Number(e.target.value) || undefined })} - placeholder="ms" + placeholder={t('breakpoints.ms')} className="w-12 rounded border bg-transparent px-1 py-0.5 text-[10px] dark:border-gray-600" disabled={!bp.delayMs} /> - ms + {t('breakpoints.ms')}