Browse Source

充值更新

jean 1 tháng trước cách đây
mục cha
commit
4aec1d88ac

+ 2 - 4
code/.env.development

@@ -16,7 +16,5 @@ VITE_FEATURE_TRADE=true
 
 VITE_CUSTOMER_SERVICE_ENTRIES_VISIBLE=false
 
-# WalletConnect(链上充值):在 https://dashboard.reown.com 创建 Project ID,
-# 写入本目录下的 .env.local(推荐,已 gitignore):VITE_WALLETCONNECT_PROJECT_ID=你的id
-# 或直接在此处取消下行注释并填入(勿将真实 ID 提交到公共仓库)
-# VITE_WALLETCONNECT_PROJECT_ID=
+# Reown AppKit(链上充值);构建时写入,修改后须重新 pnpm build
+VITE_WALLETCONNECT_PROJECT_ID=768d376c0fa6af1e4902c6756d6edf42

+ 1 - 1
code/.env.production

@@ -16,5 +16,5 @@ VITE_FEATURE_TRADE=true
 
 VITE_CUSTOMER_SERVICE_ENTRIES_VISIBLE=false
 
-# WalletConnect(链上充值);部署时在 CI/服务器环境注入真实 Project ID
+# Reown AppKit(链上充值);Vite 在 pnpm build 时注入,修改后须重新构建并部署
 VITE_WALLETCONNECT_PROJECT_ID=768d376c0fa6af1e4902c6756d6edf42

+ 8 - 3
code/package.json

@@ -10,11 +10,16 @@
   },
   "dependencies": {
     "@iconify/vue": "^5.0.0",
+    "@reown/appkit": "^1.8.20",
+    "@reown/appkit-adapter-tron": "^1.8.20",
+    "@reown/appkit-adapter-wagmi": "^1.8.20",
+    "@tanstack/vue-query": "^5.101.0",
+    "@tronweb3/tronwallet-adapter-metamask-tron": "^1.0.4",
+    "@tronweb3/tronwallet-adapter-tronlink": "^1.1.16",
     "@vueuse/core": "^14.2.1",
     "@vueuse/motion": "^3.0.3",
-    "@walletconnect/ethereum-provider": "^2.21.10",
-    "@walletconnect/modal": "^2.7.0",
-    "@walletconnect/universal-provider": "^2.23.9",
+    "@wagmi/core": "^3.5.0",
+    "@wagmi/vue": "^0.5.17",
     "axios": "^1.13.6",
     "buffer": "^6.0.3",
     "dayjs": "^1.11.20",

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 611 - 201
code/pnpm-lock.yaml


+ 64 - 184
code/src/composables/useWalletConnectDeposit.ts

@@ -1,184 +1,72 @@
 import { ref, computed } from 'vue'
-import EthereumProvider from '@walletconnect/ethereum-provider'
 import {
-  encodeFunctionData,
+  disconnect,
+  getAccount,
+  sendTransaction,
+  switchChain,
+  writeContract,
+} from '@wagmi/core'
+import {
   erc20Abi,
   getAddress,
   parseEther,
   parseUnits,
-  toHex,
 } from 'viem'
 import {
-  chainIdHex,
   erc20DecimalsForChain,
   resolveEvmChainId,
 } from '@/utils/evmRecharge'
+import {
+  abortActiveEvmConnectWait,
+  waitForEvmConnectAfterModalOpen,
+} from '@/utils/reownConnectWait'
+import {
+  getWagmiConfig,
+  reownAppKit,
+  reownEnabled,
+} from '@/config/reownAppKit'
 import type { RechargeFlatNetworkOption, RechargeOrderDetail } from '@/stores/deposit'
 
-/** 公共 RPC(仅用于 wallet_addEthereumChain;实际交易由钱包自选节点) */
-const RPC_FALLBACK: Record<number, string> = {
-  1: 'https://eth.llamarpc.com',
-  56: 'https://bsc-dataseed.binance.org',
-  137: 'https://polygon-rpc.com',
-  42161: 'https://arb1.arbitrum.io/rpc',
-  8453: 'https://mainnet.base.org',
-  10: 'https://mainnet.optimism.io',
-}
-
-const CHAIN_META: Record<
-  number,
-  { chainName: string; nativeCurrency: { name: string; symbol: string; decimals: number } }
-> = {
-  1: { chainName: 'Ethereum Mainnet', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 } },
-  56: { chainName: 'BNB Smart Chain', nativeCurrency: { name: 'BNB', symbol: 'BNB', decimals: 18 } },
-  137: { chainName: 'Polygon Mainnet', nativeCurrency: { name: 'MATIC', symbol: 'MATIC', decimals: 18 } },
-  42161: { chainName: 'Arbitrum One', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 } },
-  8453: { chainName: 'Base', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 } },
-  10: { chainName: 'Optimism', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 } },
-}
-
-let providerInit: Promise<InstanceType<typeof EthereumProvider>> | null = null
-type EvmProvider = InstanceType<typeof EthereumProvider>
-
-const WALLET_CONNECT_TIMEOUT_MS = 45_000
-
-function getProjectId(): string {
-  return String(import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? '').trim()
-}
-
-function parseWalletAccount(accounts: string[]): string {
-  const raw = accounts[0]
-  if (!raw) {
-    throw new Error('未获取到钱包地址')
-  }
-  if (raw.includes(':')) {
-    const parts = raw.split(':')
-    return getAddress(parts[parts.length - 1] as `0x${string}`)
+async function ensureEvmConnected(config: ReturnType<typeof getWagmiConfig>) {
+  const account = getAccount(config)
+  if (account.isConnected && account.address) {
+    return
   }
-  return getAddress(raw as `0x${string}`)
+  await reownAppKit?.open({ namespace: 'eip155', view: 'Connect' })
+  await waitForEvmConnectAfterModalOpen()
 }
 
-function hasEip155Session(provider: EvmProvider): boolean {
-  return Boolean(provider.session?.namespaces?.eip155?.accounts?.length)
-}
-
-async function getProvider() {
-  const projectId = getProjectId()
-  if (!projectId) {
-    throw new Error('未配置 VITE_WALLETCONNECT_PROJECT_ID')
-  }
-  if (!providerInit) {
-    const optionalChains = [56, 137, 42161, 8453, 10]
-    const rpcMap: Record<number, string> = {}
-    for (const id of [1, ...optionalChains]) {
-      const u = RPC_FALLBACK[id]
-      if (u) {
-        rpcMap[id] = u
-      }
-    }
-    providerInit = EthereumProvider.init({
-      projectId,
-      chains: [1],
-      optionalChains,
-      showQrModal: true,
-      rpcMap,
-      metadata: {
-        name: 'iBit',
-        description: '链上充值',
-        url: typeof window !== 'undefined' ? window.location.origin : 'https://ibit.com',
-        icons: [],
-      },
-    })
+async function ensureChain(config: ReturnType<typeof getWagmiConfig>, chainId: number) {
+  const account = getAccount(config)
+  if (account.chainId === chainId) {
+    return
   }
-  return providerInit
+  await switchChain(config, { chainId })
 }
 
-async function resetWalletConnectProvider() {
-  const pendingProvider = providerInit
-  providerInit = null
-  if (!pendingProvider) {
+async function resetWalletConnectSession() {
+  abortActiveEvmConnectWait()
+  if (!reownEnabled) {
     return
   }
   try {
-    const provider = await pendingProvider
-    provider.modal?.closeModal?.()
-    if (provider.session || provider.connected || provider.connecting) {
-      await provider.disconnect()
-    }
+    await reownAppKit?.close()
+    await reownAppKit?.disconnect('eip155')
+    await disconnect(getWagmiConfig())
   } catch {
-    // Reset is best-effort; the next click will create a fresh provider.
-  }
-}
-
-function withTimeout<T>(promise: Promise<T>, message: string): Promise<T> {
-  let timer: number | undefined
-  const timeout = new Promise<never>((_, reject) => {
-    timer = window.setTimeout(() => {
-      reject(new Error(message))
-    }, WALLET_CONNECT_TIMEOUT_MS)
-  })
-  return Promise.race([promise, timeout]).finally(() => {
-    if (timer) {
-      window.clearTimeout(timer)
-    }
-  })
-}
-
-async function ensureEvmConnected(provider: EvmProvider, chainId: number) {
-  if (provider.session && !hasEip155Session(provider)) {
-    await provider.disconnect()
-  }
-  if (!provider.session || !provider.connected || !hasEip155Session(provider) || provider.accounts.length === 0) {
-    await withTimeout(provider.connect({ chains: [chainId] }), '钱包连接超时,请重新点击连接钱包')
-  }
-  if (provider.accounts.length === 0) {
-    await withTimeout(provider.enable(), '钱包授权超时,请重新点击连接钱包')
-  }
-  if (provider.accounts.length === 0) {
-    throw new Error('未获取到钱包地址')
+    // Reset is best-effort; the next click will create a fresh session.
   }
 }
 
-async function ensureChain(provider: EvmProvider, chainId: number) {
-  const hex = chainIdHex(chainId)
-  try {
-    await provider.request({
-      method: 'wallet_switchEthereumChain',
-      params: [{ chainId: hex }],
-    })
-  } catch (e: unknown) {
-    const err = e as { code?: number }
-    if (err?.code === 4902) {
-      const meta = CHAIN_META[chainId]
-      const rpcUrl = RPC_FALLBACK[chainId]
-      if (!meta || !rpcUrl) {
-        throw new Error(`不支持自动添加链 chainId=${chainId},请在钱包中手动切换网络`)
-      }
-      await provider.request({
-        method: 'wallet_addEthereumChain',
-        params: [
-          {
-            chainId: hex,
-            chainName: meta.chainName,
-            nativeCurrency: meta.nativeCurrency,
-            rpcUrls: [rpcUrl],
-            blockExplorerUrls: [],
-          },
-        ],
-      })
-    } else {
-      throw e
-    }
-  }
-}
+export { resetWalletConnectSession }
 
 export function useWalletConnectDeposit() {
   const wcBusy = ref(false)
-  const wcConfigured = computed(() => !!getProjectId())
+  const wcConfigured = computed(() => reownEnabled && reownAppKit != null)
 
   function resetWalletBusy() {
     wcBusy.value = false
-    void resetWalletConnectProvider()
+    void resetWalletConnectSession()
   }
 
   function canPayWithWallet(
@@ -192,7 +80,7 @@ export function useWalletConnectDeposit() {
   }
 
   /**
-   * 连接 WalletConnect 并在当前 EVM 链上发起 USDT/代币 transfer(或主币转账)
+   * 通过 Reown AppKit(Injected + WalletConnect)连接 EVM 钱包并发起转账
    * @returns 交易 hash(0x…)
    */
   async function connectAndPay(params: {
@@ -202,20 +90,24 @@ export function useWalletConnectDeposit() {
     const { network, order } = params
     const chainId = resolveEvmChainId(network.protocol, network.networkName)
     if (chainId == null) {
-      throw new Error('当前网络不支持 WalletConnect,请使用对应链钱包手动转账')
+      throw new Error('当前网络不支持钱包支付,请使用对应链钱包手动转账')
     }
+    if (!reownEnabled) {
+      throw new Error('未配置 VITE_WALLETCONNECT_PROJECT_ID')
+    }
+
+    const config = getWagmiConfig()
     wcBusy.value = true
     try {
-      let provider = await getProvider()
-      if (provider.connecting && !provider.session) {
-        await resetWalletConnectProvider()
-        provider = await getProvider()
+      await ensureEvmConnected(config)
+      await ensureChain(config, chainId)
+
+      const account = getAccount(config)
+      const from = account.address
+      if (!from) {
+        throw new Error('未获取到钱包地址')
       }
-      await ensureEvmConnected(provider, chainId)
-      await ensureChain(provider, chainId)
 
-      const accounts = provider.accounts
-      const from = parseWalletAccount(accounts)
       const to = getAddress(order.rechargeAddress as `0x${string}`)
       const amountStr = String(order.amount).trim()
       const contractAddr = network.contractAddress?.trim()
@@ -224,36 +116,23 @@ export function useWalletConnectDeposit() {
         const token = getAddress(contractAddr as `0x${string}`)
         const decimals = erc20DecimalsForChain(chainId)
         const amountWei = parseUnits(amountStr, decimals)
-        const data = encodeFunctionData({
+        return await writeContract(config, {
+          account: from,
+          chainId,
+          address: token,
           abi: erc20Abi,
           functionName: 'transfer',
           args: [to, amountWei],
         })
-        const hash = await provider.request<`0x${string}`>({
-          method: 'eth_sendTransaction',
-          params: [
-            {
-              from,
-              to: token,
-              data,
-            },
-          ],
-        })
-        return hash
-      } else {
-        const valueWei = parseEther(amountStr)
-        const hash = await provider.request<`0x${string}`>({
-          method: 'eth_sendTransaction',
-          params: [
-            {
-              from,
-              to,
-              value: toHex(valueWei),
-            },
-          ],
-        })
-        return hash
       }
+
+      const valueWei = parseEther(amountStr)
+      return await sendTransaction(config, {
+        account: from,
+        chainId,
+        to,
+        value: valueWei,
+      })
     } finally {
       wcBusy.value = false
     }
@@ -263,6 +142,7 @@ export function useWalletConnectDeposit() {
     wcBusy,
     wcConfigured,
     resetWalletBusy,
+    clearWalletSession: resetWalletConnectSession,
     canPayWithWallet,
     connectAndPay,
   }

+ 83 - 140
code/src/composables/useWalletConnectTronDeposit.ts

@@ -1,12 +1,9 @@
 import { ref, computed } from 'vue'
-import UniversalProvider from '@walletconnect/universal-provider'
-import { WalletConnectModal } from '@walletconnect/modal'
 import { TronWeb, Types } from 'tronweb'
+import { tronMainnet } from '@reown/appkit/networks'
 import {
   TRON_MAINNET_CAIP2,
-  TRON_MAINNET_CHAIN_ID_HEX,
   isTronDepositNetwork,
-  parseTronCaip10Address,
   tronMainnetCaip10Account,
   isTronRechargeUsdtSymbol,
   TRC20_USDT_OFFICIAL_MAINNET,
@@ -14,16 +11,25 @@ import {
   trc20AmountToMinUnits,
   trxToSunAmount,
 } from '@/utils/tronRecharge'
+import { getReownAppKit, reownAppKit, reownEnabled } from '@/config/reownAppKit'
+import {
+  abortActiveTronConnectWait,
+  waitForTronConnectAfterModalOpen,
+} from '@/utils/reownConnectWait'
 import type { RechargeFlatNetworkOption, RechargeOrderDetail } from '@/stores/deposit'
 
 const FEE_LIMIT_SUN = 150_000_000
 
-let tronProviderInit: Promise<UniversalProvider> | null = null
-let wcModal: WalletConnectModal | null = null
-let displayUriHandler: ((uri: string) => void) | null = null
+type TronProviderRequest = (args: { method: string; params?: unknown }) => Promise<unknown>
 
-function getProjectId(): string {
-  return String(import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? '').trim()
+interface TronWalletConnector {
+  request?: TronProviderRequest
+  provider?: {
+    session?: {
+      sessionProperties?: Record<string, unknown>
+    }
+    request?: (args: { method: string; params?: unknown }, chainId?: string) => Promise<unknown>
+  }
 }
 
 function tronFullHost(): string {
@@ -39,74 +45,42 @@ function tronHeaders(): Record<string, string> | undefined {
   return { 'TRON-PRO-API-KEY': key }
 }
 
-function closeTronWcModal() {
-  wcModal?.closeModal()
-}
-
-function ensureWcModal(projectId: string): WalletConnectModal {
-  if (!wcModal && typeof window !== 'undefined') {
-    wcModal = new WalletConnectModal({
-      projectId,
-      themeMode: 'dark',
-      themeVariables: { '--wcm-z-index': '3000' },
-    })
+async function ensureTronConnected(): Promise<string> {
+  const appKit = getReownAppKit()
+  try {
+    await appKit.switchNetwork(tronMainnet)
+  } catch {
+    // 已处于波场网络时可忽略
   }
-  return wcModal!
-}
 
-function attachDisplayUri(provider: UniversalProvider, projectId: string) {
-  const modal = ensureWcModal(projectId)
-  if (displayUriHandler) {
-    provider.off('display_uri', displayUriHandler)
+  const existing = appKit.getAccount('tron')
+  if (existing?.isConnected && existing.address) {
+    return existing.address
   }
-  displayUriHandler = (uri: string) => {
-    void modal.openModal({ uri, chains: [TRON_MAINNET_CAIP2] })
-  }
-  provider.on('display_uri', displayUriHandler)
-}
 
-async function getTronProvider(): Promise<UniversalProvider> {
-  const projectId = getProjectId()
-  if (!projectId) {
-    throw new Error('未配置 VITE_WALLETCONNECT_PROJECT_ID')
-  }
-  if (!tronProviderInit) {
-    tronProviderInit = UniversalProvider.init({
-      projectId,
-      metadata: {
-        name: 'iBit',
-        description: '链上充值(波场)',
-        url: typeof window !== 'undefined' ? window.location.origin : 'https://ibit.com',
-        icons: [],
-      },
-    })
-  }
-  const provider = await tronProviderInit
-  attachDisplayUri(provider, projectId)
-  return provider
+  await appKit.open({ namespace: 'tron', view: 'Connect' })
+  return waitForTronConnectAfterModalOpen()
 }
 
-function firstTronAddress(provider: UniversalProvider): string {
-  const raw = provider.session?.namespaces?.tron?.accounts ?? []
-  for (const a of raw) {
-    const addr = parseTronCaip10Address(String(a))
-    if (addr) {
-      return addr
-    }
+export async function resetTronWalletSession() {
+  abortActiveTronConnectWait()
+  if (!reownEnabled) {
+    return
+  }
+  try {
+    await reownAppKit?.close()
+    await reownAppKit?.disconnect('tron')
+  } catch {
+    // Reset is best-effort; the next click will create a fresh session.
   }
-  throw new Error('未获取到波场钱包地址')
 }
 
-/** 钱包校验时常要求 address 与 session.accounts 中某条完全一致(CAIP-10) */
-function tronSessionAccountString(provider: UniversalProvider, base58: string): string {
-  const accounts = provider.session?.namespaces?.tron?.accounts ?? []
-  for (const a of accounts) {
-    const s = String(a)
-    if (parseTronCaip10Address(s) === base58) {
-      return s
-    }
+function getTronConnector(): TronWalletConnector {
+  const connector = getReownAppKit().getProvider<TronWalletConnector>('tron')
+  if (!connector) {
+    throw new Error('波场钱包未连接')
   }
-  return tronMainnetCaip10Account(base58)
+  return connector
 }
 
 function sanitizeUnsignedTxForWc(tx: Types.Transaction): Record<string, unknown> {
@@ -115,9 +89,6 @@ function sanitizeUnsignedTxForWc(tx: Types.Transaction): Record<string, unknown>
   return plain
 }
 
-/**
- * 补全链上常见缺省、减少钱包端参数校验失败
- */
 function prepareUnsignedForTronWalletConnect(tx: Types.Transaction): Record<string, unknown> {
   const o = sanitizeUnsignedTxForWc(tx)
   const rd = o.raw_data
@@ -210,45 +181,56 @@ function normalizeTronSignedResult(raw: unknown): Types.SignedTransaction {
   throw new Error('钱包未返回有效签名(signature)')
 }
 
-/**
- * 与 @tronweb3/walletconnect-tron 一致:v1 为扁平 transaction;非 v1 为 transaction: { transaction }。
- * address 须为 Base58(T…),不要用整条 CAIP-10 作为 address。
- */
-function tronSessionUsesSignV1(provider: UniversalProvider): boolean {
-  const v = provider.session?.sessionProperties?.tron_method_version
+function tronSessionUsesSignV1(connector: TronWalletConnector): boolean {
+  const v = connector.provider?.session?.sessionProperties?.tron_method_version
   return v === 'v1'
 }
 
 async function requestTronSignTransaction(
-  provider: UniversalProvider,
   fromBase58: string,
   unsigned: Types.Transaction,
 ): Promise<Types.SignedTransaction> {
+  const connector = getTronConnector()
   const prepared = prepareUnsignedForTronWalletConnect(unsigned)
   const noTxId = cloneWithoutTxId(prepared)
-  const sessionAcct = tronSessionAccountString(provider, fromBase58)
-  const useV1 = tronSessionUsesSignV1(provider)
+  const sessionAcct = tronMainnetCaip10Account(fromBase58)
+  const useV1 = tronSessionUsesSignV1(connector)
 
-  const paramVariants: unknown[] = []
+  const requestViaConnector = async (args: { method: string; params?: unknown }) => {
+    if (connector.request) {
+      return connector.request(args)
+    }
+    if (connector.provider?.request) {
+      return connector.provider.request(args, TRON_MAINNET_CAIP2)
+    }
+    throw new Error('波场钱包不支持签名请求')
+  }
 
+  try {
+    const injectedSigned = await requestViaConnector({
+      method: 'tron_sendTransaction',
+      params: { transaction: prepared },
+    })
+    return normalizeTronSignedResult(injectedSigned)
+  } catch (injectedErr) {
+    console.debug('[tron_sendTransaction] 注入钱包签名失败:', formatWalletConnectError(injectedErr))
+  }
+
+  const paramVariants: unknown[] = []
   if (useV1) {
-    // v1(session 明确声明):扁平结构,address = Base58
     paramVariants.push({ address: fromBase58, transaction: prepared })
     paramVariants.push({ transaction: prepared })
     paramVariants.push({ address: fromBase58, transaction: { transaction: prepared } })
     paramVariants.push({ address: sessionAcct, transaction: prepared })
-    // 兜底:不带 txID
     paramVariants.push({ address: fromBase58, transaction: noTxId })
     paramVariants.push({ transaction: noTxId })
   } else {
-    // 非 v1(默认):嵌套结构优先,与 @tronweb3/walletconnect-tron adapter 一致
     paramVariants.push({ address: fromBase58, transaction: { transaction: prepared } })
     paramVariants.push({ address: fromBase58, transaction: prepared })
     paramVariants.push({ transaction: { transaction: prepared } })
     paramVariants.push({ transaction: prepared })
     paramVariants.push({ address: sessionAcct, transaction: { transaction: prepared } })
     paramVariants.push({ address: sessionAcct, transaction: prepared })
-    // 兜底:不带 txID(少数钱包拒绝 txID 校验)
     paramVariants.push({ address: fromBase58, transaction: { transaction: noTxId } })
     paramVariants.push({ address: fromBase58, transaction: noTxId })
     paramVariants.push({ transaction: { transaction: noTxId } })
@@ -259,25 +241,16 @@ async function requestTronSignTransaction(
   for (let i = 0; i < paramVariants.length; i++) {
     const params = paramVariants[i]
     try {
-      const out = await provider.request(
-        {
-          method: 'tron_signTransaction',
-          params,
-        } as never,
-        TRON_MAINNET_CAIP2,
-      )
+      const out = await requestViaConnector({
+        method: 'tron_signTransaction',
+        params,
+      })
       return normalizeTronSignedResult(out)
     } catch (e) {
       lastErr = e
       console.debug(
         `[tron_signTransaction] variant ${i} 失败:`,
         formatWalletConnectError(e),
-        '| params keys:',
-        params && typeof params === 'object' && !Array.isArray(params)
-          ? Object.keys(params as object).join(',')
-          : Array.isArray(params)
-            ? `Array[${(params as unknown[]).length}]`
-            : typeof params,
       )
     }
   }
@@ -285,42 +258,13 @@ async function requestTronSignTransaction(
   throw new Error(`tron_signTransaction 失败(已尝试多种参数格式)。最后错误:${msg}`)
 }
 
-/**
- * 若当前已恢复的会话只有 eip155 等非 tron 命名空间,UniversalProvider 不会自动再连;
- * 若无可用 tron 账户则断开并重新用 tron 命名空间建连。
- */
-async function ensureTronWalletConnectSession(provider: UniversalProvider): Promise<void> {
-  const raw = provider.session?.namespaces?.tron?.accounts ?? []
-  const hasParsed = raw.some((a) => parseTronCaip10Address(String(a)) != null)
-
-  if (provider.session && !hasParsed) {
-    await provider.disconnect()
-  }
-
-  if (!provider.session) {
-    await provider.connect({
-      namespaces: {
-        tron: {
-          methods: ['tron_signTransaction', 'tron_signMessage'],
-          chains: [TRON_MAINNET_CAIP2],
-          events: [],
-          rpcMap: {
-            [TRON_MAINNET_CHAIN_ID_HEX]: tronFullHost(),
-          },
-        },
-      },
-      // 是否 v1 由钱包在回包里写入 sessionProperties;强行写 v1 可能与部分钱包参数形状不一致
-    } as Parameters<UniversalProvider['connect']>[0])
-  }
-}
-
 export function useWalletConnectTronDeposit() {
   const wcTronBusy = ref(false)
-  const wcTronConfigured = computed(() => !!getProjectId())
+  const wcTronConfigured = computed(() => reownEnabled && reownAppKit != null)
 
   function resetTronWalletBusy() {
     wcTronBusy.value = false
-    closeTronWcModal()
+    void resetTronWalletSession()
   }
 
   function canPayTronWithWallet(
@@ -330,14 +274,11 @@ export function useWalletConnectTronDeposit() {
     if (!network || !order || Number(order.status) !== 0) {
       return false
     }
-    if (!isTronDepositNetwork(network.protocol, network.networkName)) {
-      return false
-    }
-    return true
+    return isTronDepositNetwork(network.protocol, network.networkName)
   }
 
   /**
-   * WalletConnect(tron 命名空间)签名并广播:TRC20 transfer 或原生 TRX 转账
+   * 通过 Reown AppKit(TronLink / MetaMask Tron / WalletConnect)签名并广播。
    * @returns 链上 txid(与手动填写的 TxID 一致,非 0x 以太坊 hash)
    */
   async function connectAndPayTron(params: {
@@ -346,19 +287,20 @@ export function useWalletConnectTronDeposit() {
   }): Promise<string> {
     const { network, order } = params
     if (!canPayTronWithWallet(network, order)) {
-      throw new Error('当前订单或网络不支持波场 WalletConnect')
+      throw new Error('当前订单或网络不支持波场钱包支付')
+    }
+    if (!reownEnabled) {
+      throw new Error('未配置 VITE_WALLETCONNECT_PROJECT_ID')
     }
+
     wcTronBusy.value = true
     try {
-      const provider = await getTronProvider()
+      const from = await ensureTronConnected()
       const tronWeb = new TronWeb({
         fullHost: tronFullHost(),
         headers: tronHeaders(),
       })
 
-      await ensureTronWalletConnectSession(provider)
-
-      const from = firstTronAddress(provider)
       const to = order.rechargeAddress.trim()
       if (!to || !to.startsWith('T')) {
         throw new Error('收款地址不是有效的波场地址')
@@ -395,8 +337,8 @@ export function useWalletConnectTronDeposit() {
         unsigned = await tronWeb.transactionBuilder.sendTrx(to, trxToSunAmount(amountStr), from)
       }
 
-      const signed = await requestTronSignTransaction(provider, from, unsigned)
-      closeTronWcModal()
+      const signed = await requestTronSignTransaction(from, unsigned)
+      await reownAppKit?.close()
 
       const broadcast = await tronWeb.trx.broadcast(signed)
       if (!broadcast.result || !broadcast.txid) {
@@ -412,6 +354,7 @@ export function useWalletConnectTronDeposit() {
     wcTronBusy,
     wcTronConfigured,
     resetTronWalletBusy,
+    clearTronWalletSession: resetTronWalletSession,
     canPayTronWithWallet,
     connectAndPayTron,
   }

+ 99 - 0
code/src/config/reownAppKit.ts

@@ -0,0 +1,99 @@
+import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'
+import { TronAdapter } from '@reown/appkit-adapter-tron'
+import { createAppKit, type AppKit } from '@reown/appkit/vue'
+import { TronLinkAdapter } from '@tronweb3/tronwallet-adapter-tronlink'
+import { MetaMaskAdapter } from '@tronweb3/tronwallet-adapter-metamask-tron'
+import {
+  arbitrum,
+  base,
+  bsc,
+  mainnet,
+  optimism,
+  polygon,
+  tronMainnet,
+  type AppKitNetwork,
+} from '@reown/appkit/networks'
+import { TRON_MAINNET_CAIP2 } from '@/utils/tronRecharge'
+
+export const reownProjectId = String(import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? '').trim()
+export const reownEnabled = reownProjectId.length > 0
+
+function tronRpcUrl(): string {
+  return String(import.meta.env.VITE_TRON_FULL_HOST ?? '').trim() || 'https://api.trongrid.io'
+}
+
+export const evmNetworks: [AppKitNetwork, ...AppKitNetwork[]] = [
+  mainnet,
+  bsc,
+  polygon,
+  arbitrum,
+  base,
+  optimism,
+]
+
+export const reownNetworks: [AppKitNetwork, ...AppKitNetwork[]] = [
+  ...evmNetworks,
+  tronMainnet,
+]
+
+const metadata = {
+  name: 'iBit',
+  description: '链上充值',
+  url: typeof window !== 'undefined' ? window.location.origin : 'https://ibit.com',
+  icons: [] as string[],
+}
+
+export let wagmiAdapter: WagmiAdapter | null = null
+export let tronAdapter: TronAdapter | null = null
+export let reownAppKit: AppKit | null = null
+
+if (reownEnabled) {
+  wagmiAdapter = new WagmiAdapter({
+    networks: evmNetworks,
+    projectId: reownProjectId,
+  })
+
+  tronAdapter = new TronAdapter({
+    walletAdapters: [
+      new TronLinkAdapter({
+        openUrlWhenWalletNotFound: false,
+        checkTimeout: 3000,
+      }),
+      new MetaMaskAdapter(),
+    ],
+  })
+
+  reownAppKit = createAppKit({
+    adapters: [wagmiAdapter, tronAdapter],
+    networks: reownNetworks,
+    projectId: reownProjectId,
+    metadata,
+    themeMode: 'dark',
+    themeVariables: {
+      '--w3m-z-index': 3000,
+    },
+    features: {
+      analytics: false,
+      email: false,
+      socials: false,
+    },
+    enableWallets: true,
+    customRpcUrls: {
+      [TRON_MAINNET_CAIP2]: [{ url: tronRpcUrl() }],
+    },
+  })
+}
+
+export function getWagmiConfig() {
+  if (!wagmiAdapter) {
+    throw new Error('未配置 VITE_WALLETCONNECT_PROJECT_ID')
+  }
+  return wagmiAdapter.wagmiConfig
+}
+
+export function getReownAppKit() {
+  if (!reownAppKit) {
+    throw new Error('未配置 VITE_WALLETCONNECT_PROJECT_ID')
+  }
+  return reownAppKit
+}

+ 4 - 4
code/src/locales/zh-CN.ts

@@ -776,13 +776,13 @@ export default {
     qrAlt: '收款地址二维码',
     loadNetworksFailed: '加载主网失败',
     wcHintEvmConfigured:
-      '可使用 WalletConnect 连接 EVM 钱包,将按订单金额向收款地址发起链上转账(请确认网络与币种一致)。',
+      '可连接浏览器钱包插件(如 MetaMask)或 WalletConnect 发起链上转账(请确认网络与币种一致)。',
     wcHintEvmUnconfigured:
-      'EVM WalletConnect 未配置:请在环境变量中设置 VITE_WALLETCONNECT_PROJECT_ID,或切换到手工充值提交交易哈希。',
+      '链上钱包未就绪:构建时需配置 VITE_WALLETCONNECT_PROJECT_ID 并重新部署;或切换到手工充值复制地址转账后提交哈希。',
     wcHintTronConfigured:
-      '可使用 WalletConnect(波场命名空间)连接兼容钱包,签名后将广播交易;提交内容为波场 TxID(非 0x 以太坊哈希)。',
+      '可连接 TronLink、MetaMask(波场)或 WalletConnect 兼容钱包,签名后将广播交易;提交内容为波场 TxID(非 0x 以太坊哈希)。',
     wcHintTronUnconfigured:
-      '波场 WalletConnect 需 VITE_WALLETCONNECT_PROJECT_ID;未配置时请切换到手工充值提交 TxID。',
+      '波场 Reown AppKit 需 VITE_WALLETCONNECT_PROJECT_ID;未配置时请切换到手工充值提交 TxID。',
     wcHintTronManualOnly: '当前网络暂不支持唤醒钱包,请切换到手工充值提交 TxID。',
     walletPayFallback: '钱包支付失败',
     tronWalletPayFallback: '波场钱包支付失败',

+ 9 - 0
code/src/main.ts

@@ -2,9 +2,12 @@ import './polyfills/node-globals'
 import { createApp } from 'vue'
 import { createPinia } from 'pinia'
 import { createI18n } from 'vue-i18n'
+import { WagmiPlugin } from '@wagmi/vue'
+import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'
 import App from './App.vue'
 import router from './router'
 import { messages, DEFAULT_LOCALE, LOCALE_STORAGE_KEY, type SupportedLocale } from '@/locales'
+import { reownEnabled, wagmiAdapter } from '@/config/reownAppKit'
 
 import '@/assets/styles/variables.css'
 import '@/assets/styles/reset.css'
@@ -40,11 +43,17 @@ export const i18n = createI18n({
 document.documentElement.lang = locale
 
 const app = createApp(App)
+const queryClient = new QueryClient()
 
 app.use(createPinia())
 app.use(router)
 app.use(i18n)
 
+if (reownEnabled && wagmiAdapter) {
+  app.use(WagmiPlugin, { config: wagmiAdapter.wagmiConfig })
+  app.use(VueQueryPlugin, { queryClient })
+}
+
 // 防止第三方库(klinecharts)内部 watcher 错误触发 Vite HMR 冻结组件更新
 app.config.errorHandler = (err, _vm, info) => {
   console.error('[Vue]', info, err)

+ 164 - 0
code/src/utils/reownConnectWait.ts

@@ -0,0 +1,164 @@
+import { getAccount, watchAccount, type GetAccountReturnType } from '@wagmi/core'
+import type { AppKit } from '@reown/appkit/vue'
+import { getWagmiConfig, reownAppKit } from '@/config/reownAppKit'
+
+const WALLET_CONNECT_TIMEOUT_MS = 45_000
+
+let activeEvmWaitAbort: (() => void) | null = null
+let activeTronWaitAbort: (() => void) | null = null
+
+export class WalletFlowCancelledError extends Error {
+  constructor(message = 'modal closed') {
+    super(message)
+    this.name = 'WalletFlowCancelledError'
+  }
+}
+
+export function abortActiveEvmConnectWait() {
+  activeEvmWaitAbort?.()
+  activeEvmWaitAbort = null
+}
+
+export function abortActiveTronConnectWait() {
+  activeTronWaitAbort?.()
+  activeTronWaitAbort = null
+}
+
+export function abortAllReownConnectWaits() {
+  abortActiveEvmConnectWait()
+  abortActiveTronConnectWait()
+}
+
+function isModalOpen(appKit: AppKit): boolean {
+  return Boolean(appKit.getState().open)
+}
+
+/** 弹窗关闭:已连接则继续;未连接则视为取消。 */
+export function waitForEvmConnectAfterModalOpen(): Promise<void> {
+  const appKit = reownAppKit
+  if (!appKit) {
+    return Promise.reject(new Error('未配置 VITE_WALLETCONNECT_PROJECT_ID'))
+  }
+
+  abortActiveEvmConnectWait()
+  const config = getWagmiConfig()
+
+  return new Promise((resolve, reject) => {
+    let settled = false
+
+    const finish = (action: 'ok' | 'cancel', error?: Error) => {
+      if (settled) {
+        return
+      }
+      settled = true
+      cleanup()
+      activeEvmWaitAbort = null
+      if (action === 'ok') {
+        resolve()
+      } else {
+        reject(error ?? new WalletFlowCancelledError())
+      }
+    }
+
+    const cleanup = () => {
+      unsubState?.()
+      unwatch?.()
+      window.clearTimeout(timer)
+    }
+
+    activeEvmWaitAbort = () => finish('cancel', new WalletFlowCancelledError())
+
+    const timer = window.setTimeout(
+      () => finish('cancel', new Error('钱包连接超时,请重新点击连接钱包')),
+      WALLET_CONNECT_TIMEOUT_MS,
+    )
+
+    const hasAddress = () => Boolean(getAccount(config).address)
+
+    const unsubState = appKit.subscribeState((state) => {
+      if (settled || state.open) {
+        return
+      }
+      if (hasAddress()) {
+        finish('ok')
+      } else {
+        finish('cancel', new WalletFlowCancelledError())
+      }
+    })
+
+    const unwatch = watchAccount(config, {
+      onChange(account: GetAccountReturnType) {
+        if (settled) {
+          return
+        }
+        if (account.isConnected && account.address && !isModalOpen(appKit)) {
+          finish('ok')
+        }
+      },
+    })
+  })
+}
+
+export function waitForTronConnectAfterModalOpen(): Promise<string> {
+  const appKit = reownAppKit
+  if (!appKit) {
+    return Promise.reject(new Error('未配置 VITE_WALLETCONNECT_PROJECT_ID'))
+  }
+
+  abortActiveTronConnectWait()
+
+  return new Promise((resolve, reject) => {
+    let settled = false
+
+    const finish = (action: 'ok' | 'cancel', address?: string, error?: Error) => {
+      if (settled) {
+        return
+      }
+      settled = true
+      cleanup()
+      activeTronWaitAbort = null
+      if (action === 'ok' && address) {
+        resolve(address)
+      } else {
+        reject(error ?? new WalletFlowCancelledError())
+      }
+    }
+
+    const cleanup = () => {
+      unsubState?.()
+      unsubAccount?.()
+      window.clearTimeout(timer)
+    }
+
+    activeTronWaitAbort = () => finish('cancel', undefined, new WalletFlowCancelledError())
+
+    const timer = window.setTimeout(
+      () => finish('cancel', undefined, new Error('钱包连接超时,请重新点击连接钱包')),
+      WALLET_CONNECT_TIMEOUT_MS,
+    )
+
+    const currentAddress = () => appKit.getAccount('tron')?.address?.trim() || null
+
+    const unsubState = appKit.subscribeState((state) => {
+      if (settled || state.open) {
+        return
+      }
+      const addr = currentAddress()
+      if (addr) {
+        finish('ok', addr)
+      } else {
+        finish('cancel', undefined, new WalletFlowCancelledError())
+      }
+    })
+
+    const unsubAccount = appKit.subscribeAccount((state) => {
+      if (settled || !state.isConnected || !state.address) {
+        return
+      }
+      const addr = currentAddress()
+      if (addr && !isModalOpen(appKit)) {
+        finish('ok', addr)
+      }
+    }, 'tron')
+  })
+}

+ 56 - 3
code/src/views/assets/DepositView.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ref, onMounted, computed, watch } from 'vue'
+import { ref, onMounted, computed, watch, onBeforeUnmount } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useI18n } from 'vue-i18n'
 import { useDepositStore } from '@/stores/deposit'
@@ -9,6 +9,29 @@ import { useWalletConnectDeposit } from '@/composables/useWalletConnectDeposit'
 import { useWalletConnectTronDeposit } from '@/composables/useWalletConnectTronDeposit'
 import { resolveEvmChainId } from '@/utils/evmRecharge'
 import { qrCodeImageUrl } from '@/utils/qr'
+import { WalletFlowCancelledError } from '@/utils/reownConnectWait'
+
+const DEPOSIT_MODE_KEY = 'ibit-deposit-mode'
+
+function loadDepositMode(): 'manual' | 'wallet' {
+  try {
+    const stored = sessionStorage.getItem(DEPOSIT_MODE_KEY)
+    if (stored === 'manual' || stored === 'wallet') {
+      return stored
+    }
+  } catch {
+    // ignore
+  }
+  return 'manual'
+}
+
+function saveDepositMode(mode: 'manual' | 'wallet') {
+  try {
+    sessionStorage.setItem(DEPOSIT_MODE_KEY, mode)
+  } catch {
+    // ignore
+  }
+}
 
 const { t } = useI18n()
 const store = useDepositStore()
@@ -61,9 +84,17 @@ const amountError = ref('')
 const txHashInput = ref('')
 const txHashError = ref('')
 const rechargeQrUrl = ref<string | null>(null)
-const depositMode = ref<'manual' | 'wallet'>('manual')
+const depositMode = ref<'manual' | 'wallet'>(loadDepositMode())
+/** 创建订单后锁定充值方式,链上/手工不可再切换 */
+const depositModeLocked = ref(false)
 const walletAttemptSeq = ref(0)
 
+watch(depositMode, (mode) => {
+  if (!depositModeLocked.value) {
+    saveDepositMode(mode)
+  }
+})
+
 watch(
   () => currentOrder.value?.rechargeAddress?.trim() ?? '',
   async (addr) => {
@@ -81,6 +112,9 @@ watch(
 )
 
 onMounted(() => {
+  if (currentOrder.value) {
+    depositModeLocked.value = true
+  }
   store.loadParentCoins().catch(() => {
     toast.error(store.error ?? t('common.loadFailed'))
   })
@@ -136,6 +170,8 @@ async function onCreateOrder() {
   resetPendingWalletUi()
   try {
     await store.createRechargeOrder(amount.value)
+    depositModeLocked.value = true
+    saveDepositMode(depositMode.value)
     txHashInput.value = ''
     txHashError.value = ''
     toast.success(t('deposit.orderCreated'))
@@ -174,16 +210,30 @@ function validateTxHash(): boolean {
   return true
 }
 
+onBeforeUnmount(() => {
+  resetPendingWalletUi()
+})
+
 function isWalletConnectCancelError(e: unknown): boolean {
+  if (e instanceof WalletFlowCancelledError) {
+    return true
+  }
   const msg = e instanceof Error ? e.message : typeof e === 'string' ? e : ''
   const normalized = msg.toLowerCase()
+  const code = e && typeof e === 'object' && 'code' in e ? Number((e as { code?: unknown }).code) : NaN
   return (
+    code === 4001 ||
     normalized.includes('connection request reset') ||
     normalized.includes('user rejected') ||
     normalized.includes('user denied') ||
     normalized.includes('user closed') ||
+    normalized.includes('user cancelled') ||
+    normalized.includes('user canceled') ||
+    normalized.includes('rejected the request') ||
+    normalized.includes('rejected transaction') ||
     normalized.includes('modal closed') ||
-    normalized.includes('proposal expired')
+    normalized.includes('proposal expired') ||
+    normalized.includes('action_rejected')
   )
 }
 
@@ -224,6 +274,7 @@ async function onWalletPay() {
     toast.success(t('deposit.broadcastSuccess'))
   } catch (e) {
     if (isWalletConnectCancelError(e)) {
+      resetWalletBusy()
       return
     }
     const msg = e instanceof Error ? e.message : t('deposit.walletPayFallback')
@@ -248,6 +299,7 @@ async function onTronWalletPay() {
     toast.success(t('deposit.tronBroadcastSuccess'))
   } catch (e) {
     if (isWalletConnectCancelError(e)) {
+      resetTronWalletBusy()
       return
     }
     const msg = e instanceof Error ? e.message : t('deposit.tronWalletPayFallback')
@@ -268,6 +320,7 @@ function onNewDeposit() {
   amountError.value = ''
   txHashInput.value = ''
   txHashError.value = ''
+  depositModeLocked.value = false
   depositMode.value = 'manual'
 }
 

+ 10 - 0
code/src/vite-env.d.ts

@@ -1 +1,11 @@
 /// <reference types="vite/client" />
+
+interface ImportMetaEnv {
+  readonly VITE_WALLETCONNECT_PROJECT_ID?: string
+  readonly VITE_TRON_FULL_HOST?: string
+  readonly VITE_TRONGRID_API_KEY?: string
+}
+
+interface ImportMeta {
+  readonly env: ImportMetaEnv
+}

+ 1 - 1
code/vite.config.ts

@@ -8,7 +8,7 @@ export default defineConfig(({ mode }) => {
   return {
     plugins: [vue()],
     optimizeDeps: {
-      include: ['buffer'],
+      include: ['buffer', '@reown/appkit', '@reown/appkit-adapter-wagmi', '@wagmi/vue', '@tanstack/vue-query'],
     },
     // 默认 assetsDir 为 `assets`,与 Vue 路由 `/assets`(我的资产)在 Nginx 上冲突——会优先匹配磁盘目录导致 403。
     build: {

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác