diff --git a/miniapps/biobridge/src/App.test.tsx b/miniapps/biobridge/src/App.test.tsx index a14e2a891..df106b90f 100644 --- a/miniapps/biobridge/src/App.test.tsx +++ b/miniapps/biobridge/src/App.test.tsx @@ -215,4 +215,44 @@ describe('Forge App', () => { expect.objectContaining({ method: 'eth_requestAccounts' }) ) }) + + it('should allow reconnecting wallets from confirm step', async () => { + mockBio.request.mockImplementation(({ method, params }: { method: string; params?: Array<{ chain?: string }> }) => { + if (method === 'bio_closeSplashScreen') return Promise.resolve(null) + if (method === 'bio_selectAccount') { + const chain = params?.[0]?.chain + if (chain === 'ethereum') { + return Promise.resolve({ address: '0xexternal-bio', chain: 'ethereum' }) + } + return Promise.resolve({ address: 'bfmeta123', chain: 'bfmeta' }) + } + return Promise.resolve(null) + }) + + render() + + await waitFor(() => { + expect(screen.getByTestId('connect-button')).toBeInTheDocument() + }) + + fireEvent.click(screen.getByTestId('connect-button')) + + await waitFor(() => { + expect(screen.getByTestId('amount-input')).toBeInTheDocument() + }) + + fireEvent.change(screen.getByTestId('amount-input'), { target: { value: '1' } }) + fireEvent.click(screen.getByTestId('preview-button')) + + await waitFor(() => { + expect(screen.getByText('0xexternal-bio')).toBeInTheDocument() + expect(screen.getByText('bfmeta123')).toBeInTheDocument() + }) + + fireEvent.click(screen.getByTestId('reconnect-button')) + + await waitFor(() => { + expect(screen.getByTestId('connect-button')).toBeInTheDocument() + }) + }) }) diff --git a/miniapps/biobridge/src/App.tsx b/miniapps/biobridge/src/App.tsx index 9854fa771..0304d71f0 100644 --- a/miniapps/biobridge/src/App.tsx +++ b/miniapps/biobridge/src/App.tsx @@ -39,6 +39,10 @@ import type { BridgeMode } from '@/api/types'; type RechargeStep = 'connect' | 'swap' | 'confirm' | 'processing' | 'success'; +function normalizeIdForCompare(value: string | undefined): string { + return value?.trim().toLowerCase() ?? ''; +} + const TOKEN_COLORS: Record = { ETH: 'bg-indigo-600', BSC: 'bg-yellow-600', @@ -229,6 +233,17 @@ export default function App() { const handleConfirm = useCallback(async () => { if (!externalAccount || !internalAccount || !selectedOption) return; + const expectedExternalChain = normalizeChainId(selectedOption.externalChain); + if (normalizeIdForCompare(externalAccount.chain) !== normalizeIdForCompare(expectedExternalChain)) { + setError(t('error.accountChainMismatch')); + setRechargeStep('connect'); + return; + } + if (normalizeIdForCompare(internalAccount.chain) !== normalizeIdForCompare(selectedOption.internalChain)) { + setError(t('error.accountChainMismatch')); + setRechargeStep('connect'); + return; + } const tokenAddress = selectedOption.externalInfo.contract?.trim(); let effectiveExternalDecimals = resolvedExternalDecimals; if (tokenAddress && effectiveExternalDecimals === undefined) { @@ -283,6 +298,15 @@ export default function App() { forgeHook.reset(); }, [forgeHook]); + const handleReconnectAccounts = useCallback(() => { + setExternalAccount(null); + setInternalAccount(null); + setAmount(''); + setError(null); + forgeHook.reset(); + setRechargeStep('connect'); + }, [forgeHook]); + // Group options by external chain for picker const groupedOptions = useMemo(() => { const groups: Record = {}; @@ -577,14 +601,24 @@ export default function App() { )}
- +
+ + +
)} @@ -636,6 +670,20 @@ export default function App() { +
+ {t('forge.sender')} + + {externalAccount?.address} + +
+ +
+ {t('forge.receiver')} + + {internalAccount?.address} + +
+
{t('forge.ratio')} {t('forge.ratioValue')} @@ -677,14 +725,24 @@ export default function App() {
- +
+ + +
)} diff --git a/miniapps/biobridge/src/i18n/locales/en.json b/miniapps/biobridge/src/i18n/locales/en.json index 8083cd93b..c79d29304 100644 --- a/miniapps/biobridge/src/i18n/locales/en.json +++ b/miniapps/biobridge/src/i18n/locales/en.json @@ -28,7 +28,10 @@ "decimalsLoading": "Loading decimals...", "decimalsFromTokenInfo": "token info", "decimalsFallback": "default", - "contractHint": "Contract: {{address}}" + "contractHint": "Contract: {{address}}", + "changeWallets": "Change wallets", + "sender": "Sender", + "receiver": "Receiver" }, "redemption": { "title": "Redemption", @@ -80,6 +83,7 @@ "invalidAmount": "Please enter a valid amount", "decimalsLoading": "Token decimals are loading", "missingDecimals": "Missing token decimals configuration", + "accountChainMismatch": "Wallet account does not match selected chain, reconnect wallets", "forgeFailed": "Operation failed" }, "picker": { diff --git a/miniapps/biobridge/src/i18n/locales/zh-CN.json b/miniapps/biobridge/src/i18n/locales/zh-CN.json index 19a3553d5..f92a1a0c5 100644 --- a/miniapps/biobridge/src/i18n/locales/zh-CN.json +++ b/miniapps/biobridge/src/i18n/locales/zh-CN.json @@ -28,7 +28,10 @@ "decimalsLoading": "精度读取中...", "decimalsFromTokenInfo": "TokenInfo", "decimalsFallback": "默认", - "contractHint": "合约:{{address}}" + "contractHint": "合约:{{address}}", + "changeWallets": "重新连接账户", + "sender": "发送地址", + "receiver": "接收地址" }, "redemption": { "title": "赎回", @@ -80,6 +83,7 @@ "invalidAmount": "请输入有效金额", "decimalsLoading": "精度信息加载中,请稍候", "missingDecimals": "缺少资产精度配置", + "accountChainMismatch": "账户与当前链不匹配,请重新连接账户", "forgeFailed": "操作失败" }, "picker": { diff --git a/miniapps/biobridge/src/i18n/locales/zh-TW.json b/miniapps/biobridge/src/i18n/locales/zh-TW.json index faf8d70ce..8374d6afc 100644 --- a/miniapps/biobridge/src/i18n/locales/zh-TW.json +++ b/miniapps/biobridge/src/i18n/locales/zh-TW.json @@ -28,7 +28,10 @@ "decimalsLoading": "精度讀取中...", "decimalsFromTokenInfo": "TokenInfo", "decimalsFallback": "預設", - "contractHint": "合約:{{address}}" + "contractHint": "合約:{{address}}", + "changeWallets": "重新連接帳戶", + "sender": "發送地址", + "receiver": "接收地址" }, "redemption": { "title": "贖回", @@ -80,6 +83,7 @@ "invalidAmount": "請輸入有效金額", "decimalsLoading": "精度資訊載入中,請稍候", "missingDecimals": "缺少資產精度配置", + "accountChainMismatch": "帳戶與當前鏈不匹配,請重新連接帳戶", "forgeFailed": "操作失敗" }, "picker": { diff --git a/miniapps/biobridge/src/i18n/locales/zh.json b/miniapps/biobridge/src/i18n/locales/zh.json index 19a3553d5..f92a1a0c5 100644 --- a/miniapps/biobridge/src/i18n/locales/zh.json +++ b/miniapps/biobridge/src/i18n/locales/zh.json @@ -28,7 +28,10 @@ "decimalsLoading": "精度读取中...", "decimalsFromTokenInfo": "TokenInfo", "decimalsFallback": "默认", - "contractHint": "合约:{{address}}" + "contractHint": "合约:{{address}}", + "changeWallets": "重新连接账户", + "sender": "发送地址", + "receiver": "接收地址" }, "redemption": { "title": "赎回", @@ -80,6 +83,7 @@ "invalidAmount": "请输入有效金额", "decimalsLoading": "精度信息加载中,请稍候", "missingDecimals": "缺少资产精度配置", + "accountChainMismatch": "账户与当前链不匹配,请重新连接账户", "forgeFailed": "操作失败" }, "picker": { diff --git a/src/services/chain-adapter/evm/transaction-mixin.test.ts b/src/services/chain-adapter/evm/transaction-mixin.test.ts new file mode 100644 index 000000000..2a95f462b --- /dev/null +++ b/src/services/chain-adapter/evm/transaction-mixin.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest' +import { hexToBytes } from '@noble/hashes/utils.js' +import { privateKeyToAccount } from 'viem/accounts' +import { recoverTransactionAddress } from 'viem' + +import { EvmTransactionMixin } from './transaction-mixin' +import type { UnsignedTransaction } from '../types' + +class EvmSignTestBase { + constructor(public readonly chainId: string) {} +} + +class EvmSignTestService extends EvmTransactionMixin(EvmSignTestBase) {} + +describe('EvmTransactionMixin.signTransaction', () => { + it('signs token tx with value=0x0 and keeps sender address consistent', async () => { + const service = new EvmSignTestService('binance') + const privateKeyHex = '0x59c6995e998f97a5a0044976f5d8f17f4b10df9589ef5f8a7f6f3f6db74ad5a4' + const expectedAddress = privateKeyToAccount(privateKeyHex).address.toLowerCase() + + const unsignedTx: UnsignedTransaction = { + chainId: 'binance', + intentType: 'transfer', + data: { + nonce: 137, + gasPrice: '0x2faf080', + gasLimit: '0x249f0', + to: '0x55d398326f99059ff775485246999027b3197955', + value: '0x0', + data: '0xa9059cbb000000000000000000000000063096cbc147d5170e1b10fa4895bfa882c1d45e0000000000000000000000000000000000000000000000008ac7230489e80000', + chainId: 56, + }, + } + + const signed = await service.signTransaction(unsignedTx, { + privateKey: hexToBytes(privateKeyHex.slice(2)), + }) + + const serialized = signed.data as `0x${string}` + const recoveredAddress = (await recoverTransactionAddress({ serializedTransaction: serialized })).toLowerCase() + expect(recoveredAddress).toBe(expectedAddress) + }) +}) diff --git a/src/services/chain-adapter/evm/transaction-mixin.ts b/src/services/chain-adapter/evm/transaction-mixin.ts index f860fcc01..fa23afb79 100644 --- a/src/services/chain-adapter/evm/transaction-mixin.ts +++ b/src/services/chain-adapter/evm/transaction-mixin.ts @@ -202,13 +202,16 @@ export function EvmTransactionMixin { if (item === '0x' || item === '') {