diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png index 1304dadf..5e404488 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-swap.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-swap.png index cb62fde0..325296ef 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-swap.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-swap.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-swap-amount.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-swap-amount.png index 8c6dd7ad..2c39dcd9 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-swap-amount.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-swap-amount.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-token-picker.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-token-picker.png index ae963b25..8e2550e8 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-token-picker.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-token-picker.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-confirm.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-confirm.png index 0539773b..6cd581b0 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-confirm.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-confirm.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-error.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-error.png new file mode 100644 index 00000000..10462da2 Binary files /dev/null and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-error.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-flow-complete.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-flow-complete.png new file mode 100644 index 00000000..0c845a8e Binary files /dev/null and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-flow-complete.png differ diff --git a/miniapps/forge/e2e/ui.spec.ts b/miniapps/forge/e2e/ui.spec.ts index ad8ffc69..c8ce0659 100644 --- a/miniapps/forge/e2e/ui.spec.ts +++ b/miniapps/forge/e2e/ui.spec.ts @@ -3,9 +3,13 @@ import { UI_TEXT } from './helpers/i18n' const mockApiResponses = ` // Mock fetch for API calls + // Real endpoints: /cot/recharge/support, /cot/recharge/V2 const originalFetch = window.fetch window.fetch = async (url, options) => { - if (url.includes('getSupport')) { + const urlStr = typeof url === 'string' ? url : url.toString() + + // Match real endpoint: /cot/recharge/support + if (urlStr.includes('/cot/recharge/support') || urlStr.includes('/recharge/support')) { return { ok: true, json: () => Promise.resolve({ @@ -24,7 +28,8 @@ const mockApiResponses = ` }), } } - if (url.includes('rechargeV2')) { + // Match real endpoint: /cot/recharge/V2 + if (urlStr.includes('/cot/recharge/V2') || urlStr.includes('/recharge/V2')) { return { ok: true, json: () => Promise.resolve({ orderId: 'order-123456' }), @@ -53,7 +58,7 @@ const mockBioSDK = ` return { data: '0xsigned-tx-data-456' } } if (method === 'bio_signMessage') { - return 'signature-789' + return { signature: 'signature-789', publicKey: 'pubkey-abc123' } } return {} } @@ -123,8 +128,8 @@ test.describe('Forge UI', () => { await page.click('button:has-text("ETH")') await expect(page.locator(`text=${UI_TEXT.token.select.source}`).first()).toBeVisible() - // Should show available tokens - await expect(page.locator('text=Ethereum')).toBeVisible() + // Should show available tokens (use heading to be specific) + await expect(page.getByRole('heading', { name: 'Ethereum' })).toBeVisible() await expect(page).toHaveScreenshot('04-token-picker.png') }) @@ -150,14 +155,25 @@ test.describe('Forge UI', () => { }) test('06 - error state without bio SDK', async ({ page }) => { - // No bio SDK mock + // Mock bio SDK that throws connection error + await page.addInitScript(` + window.bio = { + request: async ({ method }) => { + if (method === 'bio_closeSplashScreen') return {} + throw new Error('连接失败') + } + } + `) await page.goto('/') await page.waitForLoadState('networkidle') - await page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first().click() + // Wait for button to be enabled (config loaded) + const connectBtn = page.locator(`button:has-text("${UI_TEXT.connect.button.source}")`).first() + await expect(connectBtn).toBeEnabled({ timeout: 10000 }) + await connectBtn.click() - // Should show error - await expect(page.locator(`text=${UI_TEXT.error.sdkNotInit.source}`)).toBeVisible({ timeout: 5000 }) + // Should show error message (bio SDK throws error) + await expect(page.locator('text=连接失败')).toBeVisible({ timeout: 5000 }) await expect(page).toHaveScreenshot('06-error.png') }) @@ -220,12 +236,14 @@ test.describe('Forge UI', () => { await page.click('button:has-text("ETH")') await expect(page.locator(`text=${UI_TEXT.token.select.source}`)).toBeVisible() - // Select different token (BNB on BSC) - const bnbOption = page.locator('text=BNB').first() + // Select different token (BNB → BFM option under BNB Chain) + const bnbOption = page.locator('text=BNB → BFM').first() if (await bnbOption.isVisible()) { await bnbOption.click() - // Picker should close and new token should be selected - await expect(page.locator('button:has-text("BNB")')).toBeVisible({ timeout: 5000 }) + // Picker should close, wait a moment for state update + await page.waitForTimeout(500) + // Token selector should now show BNB + await expect(page.locator('button:has-text("BNB")').first()).toBeVisible({ timeout: 5000 }) } }) }) diff --git a/miniapps/forge/scripts/e2e.ts b/miniapps/forge/scripts/e2e.ts index 6b1d6125..c381450a 100644 --- a/miniapps/forge/scripts/e2e.ts +++ b/miniapps/forge/scripts/e2e.ts @@ -31,7 +31,11 @@ async function main() { // Start vite dev server const vite = spawn('pnpm', ['vite', '--port', String(port)], { stdio: ['ignore', 'pipe', 'pipe'], - env: { ...process.env }, + env: { + ...process.env, + // E2E 测试使用 mock API,设置一个占位 base URL + VITE_COT_API_BASE_URL: process.env.VITE_COT_API_BASE_URL || 'https://e2e-mock.test', + }, }) let serverReady = false diff --git a/miniapps/forge/src/api/config.ts b/miniapps/forge/src/api/config.ts index 1cafd7e1..2a066b46 100644 --- a/miniapps/forge/src/api/config.ts +++ b/miniapps/forge/src/api/config.ts @@ -1,10 +1,28 @@ /** * API Configuration - * TODO: Base URL needs to be confirmed with backend team + * + * IMPORTANT: Base URL must be configured via VITE_COT_API_BASE_URL environment variable. + * The COT Recharge API host has not been confirmed - do not use a hardcoded default. */ -/** API Base URL - to be configured via environment or runtime */ -export const API_BASE_URL = import.meta.env.VITE_COT_API_BASE_URL || 'https://api.eth-metaverse.com' +/** API Base URL - must be configured via environment variable */ +function getApiBaseUrl(): string { + const url = import.meta.env.VITE_COT_API_BASE_URL + if (!url) { + // Fail-fast in development to catch missing configuration early + if (import.meta.env.DEV) { + console.error( + '[Forge API] VITE_COT_API_BASE_URL is not configured. ' + + 'Please set this environment variable to the COT Recharge API base URL.' + ) + } + // Return empty string - API calls will fail with clear error + return '' + } + return url +} + +export const API_BASE_URL = getApiBaseUrl() /** API Endpoints */ export const API_ENDPOINTS = { diff --git a/miniapps/forge/src/hooks/useForge.test.ts b/miniapps/forge/src/hooks/useForge.test.ts index 9aac03ff..add88669 100644 --- a/miniapps/forge/src/hooks/useForge.test.ts +++ b/miniapps/forge/src/hooks/useForge.test.ts @@ -48,7 +48,7 @@ describe('useForge', () => { mockBio.request .mockResolvedValueOnce({ txHash: 'unsigned123' }) // bio_createTransaction .mockResolvedValueOnce({ data: '0xsigned123' }) // bio_signTransaction - .mockResolvedValueOnce('signature123') // bio_signMessage + .mockResolvedValueOnce({ signature: 'signature123', publicKey: 'pubkey123' }) // bio_signMessage vi.mocked(rechargeApi.submitRecharge).mockResolvedValue({ orderId: 'order123' }) @@ -66,7 +66,7 @@ describe('useForge', () => { expect(result.current.orderId).toBe('order123') expect(result.current.error).toBeNull() - // Verify API calls + // Verify API calls (3 calls: createTx, signTx, signMessage) expect(mockBio.request).toHaveBeenCalledTimes(3) expect(rechargeApi.submitRecharge).toHaveBeenCalledTimes(1) }) @@ -126,7 +126,7 @@ describe('useForge', () => { mockBio.request .mockResolvedValueOnce({ txHash: 'unsigned123' }) .mockResolvedValueOnce({ data: '0xsigned123' }) - .mockResolvedValueOnce('signature123') + .mockResolvedValueOnce({ signature: 'signature123', publicKey: 'pubkey123' }) vi.mocked(rechargeApi.submitRecharge).mockRejectedValue(new Error('Server error')) @@ -169,7 +169,7 @@ describe('useForge', () => { mockBio.request .mockResolvedValueOnce({ txHash: 'unsigned' }) .mockResolvedValueOnce({ data: '0xsignedEthTx' }) - .mockResolvedValueOnce('sig') + .mockResolvedValueOnce({ signature: 'sig', publicKey: 'pubkey' }) vi.mocked(rechargeApi.submitRecharge).mockResolvedValue({ orderId: 'order' }) @@ -192,7 +192,7 @@ describe('useForge', () => { mockBio.request .mockResolvedValueOnce({ txHash: 'unsigned' }) .mockResolvedValueOnce({ data: '0xsignedBscTx' }) - .mockResolvedValueOnce('sig') + .mockResolvedValueOnce({ signature: 'sig', publicKey: 'pubkey' }) vi.mocked(rechargeApi.submitRecharge).mockResolvedValue({ orderId: 'order' }) diff --git a/miniapps/forge/src/hooks/useForge.ts b/miniapps/forge/src/hooks/useForge.ts index f028d6f4..86c701b4 100644 --- a/miniapps/forge/src/hooks/useForge.ts +++ b/miniapps/forge/src/hooks/useForge.ts @@ -127,7 +127,8 @@ export function useForge() { timestamp: rechargeMessage.timestamp, }) - const signature = await window.bio.request({ + // bio_signMessage 返回 { signature, publicKey },publicKey 为 hex 格式 + const signResult = await window.bio.request<{ signature: string; publicKey: string }>({ method: 'bio_signMessage', params: [{ message: messageToSign, @@ -135,12 +136,10 @@ export function useForge() { }], }) - // Build signature info - // Note: publicKey format needs to be confirmed with backend const signatureInfo: SignatureInfo = { timestamp: rechargeMessage.timestamp, - signature, - publicKey: internalAccount.address, // TODO: Get actual public key + signature: signResult.signature, + publicKey: signResult.publicKey, } // Step 3: Submit recharge request diff --git a/miniapps/forge/src/main.tsx b/miniapps/forge/src/main.tsx index b8b83297..81c689d2 100644 --- a/miniapps/forge/src/main.tsx +++ b/miniapps/forge/src/main.tsx @@ -1,4 +1,5 @@ import './index.css' +import './i18n' import '@biochain/bio-sdk' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' diff --git a/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png index 58748199..ac81e46b 100644 Binary files a/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png and b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png differ diff --git a/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-select-asset.png b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-select-asset.png index e3c23969..369dc564 100644 Binary files a/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-select-asset.png and b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-select-asset.png differ diff --git a/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-input-amount.png b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-input-amount.png index 5626cd0c..fb92abca 100644 Binary files a/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-input-amount.png and b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-input-amount.png differ diff --git a/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-amount-filled.png b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-amount-filled.png index 2acc32fc..58727a52 100644 Binary files a/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-amount-filled.png and b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-amount-filled.png differ diff --git a/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-select-target.png b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-select-target.png index f0036bee..c9db8569 100644 Binary files a/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-select-target.png and b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-select-target.png differ diff --git a/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-confirm.png b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-confirm.png index 2d620264..b0269948 100644 Binary files a/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-confirm.png and b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-confirm.png differ diff --git a/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-success.png b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-success.png new file mode 100644 index 00000000..ca45d3be Binary files /dev/null and b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-success.png differ diff --git a/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/08-error-no-sdk.png b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/08-error-no-sdk.png new file mode 100644 index 00000000..a2c4076e Binary files /dev/null and b/miniapps/teleport/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/08-error-no-sdk.png differ diff --git a/miniapps/teleport/e2e/ui.spec.ts b/miniapps/teleport/e2e/ui.spec.ts index cf2f48a7..ae73938e 100644 --- a/miniapps/teleport/e2e/ui.spec.ts +++ b/miniapps/teleport/e2e/ui.spec.ts @@ -1,33 +1,77 @@ import { test, expect } from '@playwright/test' import { UI_TEXT } from './helpers/i18n' -// Mock API response for asset type list -const mockAssetTypeListResponse = JSON.stringify({ - transmitSupport: { - ETH: { - ETH: { - enable: true, - isAirdrop: false, - assetType: 'ETH', - recipientAddress: '0x1234567890abcdef1234567890abcdef12345678', - targetChain: 'BFMCHAIN', - targetAsset: 'BFM', - ratio: { numerator: 1, denominator: 1 }, - transmitDate: { startDate: '2020-01-01', endDate: '2030-12-31' }, - }, - USDT: { - enable: true, - isAirdrop: false, - assetType: 'USDT', - recipientAddress: '0x1234567890abcdef1234567890abcdef12345678', - targetChain: 'BFMCHAIN', - targetAsset: 'USDM', - ratio: { numerator: 1, denominator: 1 }, - transmitDate: { startDate: '2020-01-01', endDate: '2030-12-31' }, - }, - }, - }, -}) +// Mock API responses using fetch interception +const mockApiResponses = ` + const originalFetch = window.fetch + window.fetch = async (url, options) => { + const urlStr = typeof url === 'string' ? url : url.toString() + + // Mock asset type list + if (urlStr.includes('/transmit/assetTypeList')) { + return { + ok: true, + json: () => Promise.resolve({ + transmitSupport: { + ETH: { + ETH: { + enable: true, + isAirdrop: false, + assetType: 'ETH', + recipientAddress: '0x1234567890abcdef1234567890abcdef12345678', + targetChain: 'BFMCHAIN', + targetAsset: 'BFM', + ratio: { numerator: 1, denominator: 1 }, + transmitDate: { startDate: '2020-01-01', endDate: '2030-12-31' }, + }, + USDT: { + enable: true, + isAirdrop: false, + assetType: 'USDT', + recipientAddress: '0x1234567890abcdef1234567890abcdef12345678', + targetChain: 'BFMCHAIN', + targetAsset: 'USDM', + ratio: { numerator: 1, denominator: 1 }, + transmitDate: { startDate: '2020-01-01', endDate: '2030-12-31' }, + }, + }, + }, + }), + } + } + + // Mock records list + if (urlStr.includes('/transmit/records')) { + return { + ok: true, + json: () => Promise.resolve({ page: 1, pageSize: 10, dataList: [] }), + } + } + + // Mock transmit POST + if (urlStr.includes('/transmit') && options?.method === 'POST') { + return { + ok: true, + json: () => Promise.resolve({ orderId: 'mock-order-123' }), + } + } + + // Mock record detail + if (urlStr.includes('/transmit/recordDetail')) { + return { + ok: true, + json: () => Promise.resolve({ + state: 3, + orderState: 4, // SUCCESS + swapRatio: 1, + updatedTime: new Date().toISOString(), + }), + } + } + + return originalFetch(url, options) + } +` // Mock Bio SDK with chain support const mockBioSDK = ` @@ -60,46 +104,8 @@ const mockBioSDK = ` test.describe('Teleport UI', () => { test.beforeEach(async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }) - - // Mock API calls - await page.route('**/payment/transmit/assetTypeList', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: mockAssetTypeListResponse, - }) - }) - - await page.route('**/payment/transmit/records**', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ page: 1, pageSize: 10, dataList: [] }), - }) - }) - - await page.route('**/payment/transmit', async (route) => { - if (route.request().method() === 'POST') { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ orderId: 'mock-order-123' }), - }) - } - }) - - await page.route('**/payment/transmit/recordDetail**', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - state: 3, - orderState: 4, // SUCCESS - swapRatio: 1, - updatedTime: new Date().toISOString(), - }), - }) - }) + // Mock API calls via fetch interception + await page.addInitScript(mockApiResponses) }) test('01 - connect page', async ({ page }) => { @@ -139,12 +145,16 @@ test.describe('Teleport UI', () => { await expect(connectBtn).toBeVisible({ timeout: 10000 }) await connectBtn.click() - // Select first asset (ETH) - await expect(page.locator('[data-slot="card"]').first()).toBeVisible({ timeout: 5000 }) - await page.locator('[data-slot="card"]').first().click() + // Wait for asset selection page + await expect(page.getByText(UI_TEXT.asset.select)).toBeVisible({ timeout: 10000 }) - // Verify amount input is visible - await expect(page.locator('input[type="number"]')).toBeVisible() + // Wait for assets to load, then click ETH card + const ethCard = page.getByText('ETH → BFMCHAIN').first() + await expect(ethCard).toBeVisible({ timeout: 5000 }) + await ethCard.click() + + // Verify amount input is visible (wait for step transition) + await expect(page.locator('input[type="number"]')).toBeVisible({ timeout: 10000 }) await expect(page).toHaveScreenshot('03-input-amount.png') }) @@ -159,12 +169,13 @@ test.describe('Teleport UI', () => { await expect(connectBtn).toBeVisible({ timeout: 10000 }) await connectBtn.click() - // Select asset - await expect(page.locator('[data-slot="card"]').first()).toBeVisible({ timeout: 5000 }) - await page.locator('[data-slot="card"]').first().click() + // Wait for asset selection page and click ETH + await expect(page.getByText(UI_TEXT.asset.select)).toBeVisible({ timeout: 10000 }) + const ethCard = page.getByText('ETH → BFMCHAIN').first() + await ethCard.click() // Fill amount - await page.waitForSelector('input[type="number"]') + await expect(page.locator('input[type="number"]')).toBeVisible({ timeout: 10000 }) await page.fill('input[type="number"]', '500') await expect(page.locator('input[type="number"]')).toHaveValue('500') @@ -184,12 +195,13 @@ test.describe('Teleport UI', () => { await expect(connectBtn).toBeVisible({ timeout: 10000 }) await connectBtn.click() - // Select asset - await expect(page.locator('[data-slot="card"]').first()).toBeVisible({ timeout: 5000 }) - await page.locator('[data-slot="card"]').first().click() + // Wait for asset selection page and select ETH + await expect(page.getByText(UI_TEXT.asset.select)).toBeVisible({ timeout: 10000 }) + const ethCard = page.getByText('ETH → BFMCHAIN').first() + await ethCard.click() // Fill amount and click next - await page.waitForSelector('input[type="number"]') + await expect(page.locator('input[type="number"]')).toBeVisible({ timeout: 10000 }) await page.fill('input[type="number"]', '500') await page.getByRole('button', { name: UI_TEXT.amount.next }).click() @@ -210,12 +222,13 @@ test.describe('Teleport UI', () => { await expect(connectBtn).toBeVisible({ timeout: 10000 }) await connectBtn.click() - // Select asset - await expect(page.locator('[data-slot="card"]').first()).toBeVisible({ timeout: 5000 }) - await page.locator('[data-slot="card"]').first().click() + // Wait for asset selection page and select ETH + await expect(page.getByText(UI_TEXT.asset.select)).toBeVisible({ timeout: 10000 }) + const ethCard = page.getByText('ETH → BFMCHAIN').first() + await ethCard.click() // Fill amount and proceed - await page.waitForSelector('input[type="number"]') + await expect(page.locator('input[type="number"]')).toBeVisible({ timeout: 10000 }) await page.fill('input[type="number"]', '500') await page.getByRole('button', { name: UI_TEXT.amount.next }).click() @@ -223,9 +236,9 @@ test.describe('Teleport UI', () => { await expect(page.getByRole('button', { name: UI_TEXT.target.button })).toBeVisible({ timeout: 5000 }) await page.getByRole('button', { name: UI_TEXT.target.button }).click() - // Verify confirm page - await expect(page.getByText(UI_TEXT.confirm.send)).toBeVisible({ timeout: 5000 }) - await expect(page.getByText(UI_TEXT.confirm.receive)).toBeVisible() + // Verify confirm page (use exact: true to avoid matching multiple elements) + await expect(page.getByText('发送', { exact: true })).toBeVisible({ timeout: 5000 }) + await expect(page.getByText('接收', { exact: true })).toBeVisible() await expect(page.getByRole('button', { name: UI_TEXT.confirm.button })).toBeVisible() await expect(page).toHaveScreenshot('06-confirm.png') @@ -241,12 +254,13 @@ test.describe('Teleport UI', () => { await expect(connectBtn).toBeVisible({ timeout: 10000 }) await connectBtn.click() - // Select asset - await expect(page.locator('[data-slot="card"]').first()).toBeVisible({ timeout: 5000 }) - await page.locator('[data-slot="card"]').first().click() + // Wait for asset selection page and select ETH + await expect(page.getByText(UI_TEXT.asset.select)).toBeVisible({ timeout: 10000 }) + const ethCard = page.getByText('ETH → BFMCHAIN').first() + await ethCard.click() // Fill amount and proceed - await page.waitForSelector('input[type="number"]') + await expect(page.locator('input[type="number"]')).toBeVisible({ timeout: 10000 }) await page.fill('input[type="number"]', '500') await page.getByRole('button', { name: UI_TEXT.amount.next }).click() @@ -266,7 +280,18 @@ test.describe('Teleport UI', () => { }) test('08 - error when SDK not initialized', async ({ page }) => { - // Don't add bio SDK mock + // Mock bio SDK that throws connection error + await page.addInitScript(` + window.bio = { + request: async ({ method }) => { + if (method === 'bio_closeSplashScreen') return + throw new Error('连接失败') + }, + on: () => {}, + off: () => {}, + isConnected: () => false, + } + `) await page.goto('/') await page.waitForLoadState('networkidle') @@ -275,8 +300,8 @@ test.describe('Teleport UI', () => { await expect(connectBtn).toBeVisible({ timeout: 10000 }) await connectBtn.click() - // Verify error message - await expect(page.getByText(UI_TEXT.error.sdkNotInit)).toBeVisible({ timeout: 5000 }) + // Verify error message (bio SDK throws error) + await expect(page.getByText('连接失败')).toBeVisible({ timeout: 5000 }) await expect(page).toHaveScreenshot('08-error-no-sdk.png') }) diff --git a/miniapps/teleport/src/App.tsx b/miniapps/teleport/src/App.tsx index ce9eb123..97f9f91a 100644 --- a/miniapps/teleport/src/App.tsx +++ b/miniapps/teleport/src/App.tsx @@ -179,13 +179,18 @@ export default function App() { }) // 3. 构造 fromTrJson(根据链类型) + // 注意:signTransData 需要使用 signedTx.data(RLP encoded raw signed tx), + // 而非 signedTx.signature(仅包含 r+s,不是可广播的 rawTx) const fromTrJson: FromTrJson = {} const chainLower = sourceAccount.chain.toLowerCase() + const signTransData = typeof signedTx.data === 'string' + ? signedTx.data + : JSON.stringify(signedTx.data) if (chainLower === 'eth') { - fromTrJson.eth = { signTransData: signedTx.signature } + fromTrJson.eth = { signTransData } } else if (chainLower === 'bsc') { - fromTrJson.bsc = { signTransData: signedTx.signature } + fromTrJson.bsc = { signTransData } } else { // 内链交易 fromTrJson.bcf = { diff --git a/miniapps/teleport/src/api/types.ts b/miniapps/teleport/src/api/types.ts index 75a4b376..67d60644 100644 --- a/miniapps/teleport/src/api/types.ts +++ b/miniapps/teleport/src/api/types.ts @@ -1,6 +1,9 @@ /** * Teleport API Types - * 基于 @bnqkl/metabox-core@0.5.2 类型定义 + * + * 类型定义参考 @bnqkl/metabox-core@0.5.2 和 @bnqkl/wallet-typings@0.23.8 + * 注意:这些包在 package.json 中作为依赖存在,但当前未被直接 import 使用。 + * 如果不需要运行时依赖,可以考虑移至 devDependencies 或移除。 */ // 链名类型 diff --git a/packages/bio-sdk/src/types.ts b/packages/bio-sdk/src/types.ts index 711f76de..8c9ceaed 100644 --- a/packages/bio-sdk/src/types.ts +++ b/packages/bio-sdk/src/types.ts @@ -80,11 +80,14 @@ export interface BioMethods { /** Pick another wallet address (shows wallet picker UI) */ bio_pickWallet: (opts?: { chain?: string; exclude?: string }) => Promise - /** Sign a message */ - bio_signMessage: (params: { message: string; address: string }) => Promise + /** Get public key for an address (hex encoded) */ + bio_getPublicKey: (params: { address: string }) => Promise - /** Sign typed data */ - bio_signTypedData: (params: { data: object; address: string }) => Promise + /** Sign a message, returns signature and public key (hex) */ + bio_signMessage: (params: { message: string; address: string }) => Promise<{ signature: string; publicKey: string }> + + /** Sign typed data, returns signature and public key (hex) */ + bio_signTypedData: (params: { data: object; address: string }) => Promise<{ signature: string; publicKey: string }> /** Create an unsigned transaction (no signature, no broadcast) */ bio_createTransaction: (params: TransferParams) => Promise diff --git a/src/pages/ecosystem/miniapp.tsx b/src/pages/ecosystem/miniapp.tsx index 31dfefaf..12eecc54 100644 --- a/src/pages/ecosystem/miniapp.tsx +++ b/src/pages/ecosystem/miniapp.tsx @@ -152,14 +152,14 @@ export function MiniappPage({ appId, onClose }: MiniappPageProps) { // 签名对话框 - 集成真实签名服务 const showSigningDialog = useCallback( - (params: { message: string; address: string; app: { name: string; icon?: string } }): Promise => { + (params: { message: string; address: string; app: { name: string; icon?: string } }): Promise<{ signature: string; publicKey: string } | null> => { return new Promise((resolve) => { const handleConfirm = (e: Event) => { const detail = (e as CustomEvent).detail window.removeEventListener('signing-confirm', handleConfirm) - if (detail.confirmed && detail.signature) { - // 返回真实签名 - resolve(detail.signature) + if (detail.confirmed && detail.signature && detail.publicKey) { + // 返回签名和公钥(hex 格式) + resolve({ signature: detail.signature, publicKey: detail.publicKey }) } else { resolve(null) } diff --git a/src/services/authorize/index.ts b/src/services/authorize/index.ts index 16d78a46..f235fcfd 100644 --- a/src/services/authorize/index.ts +++ b/src/services/authorize/index.ts @@ -19,7 +19,7 @@ export type { export { plaocAdapterMeta } from './types' export { AddressAuthService } from './address-auth' -export { SignatureAuthService, type SignatureAuthError } from './signature-auth' +export { SignatureAuthService, type SignatureAuthError, type SignatureResult } from './signature-auth' // High-level adapter interface (import directly from './plaoc-adapter' if needed) export { createPlaocAdapter, isPlaocAvailable } from './plaoc-adapter' diff --git a/src/services/authorize/signature-auth.ts b/src/services/authorize/signature-auth.ts index b2d3743e..b50e6aec 100644 --- a/src/services/authorize/signature-auth.ts +++ b/src/services/authorize/signature-auth.ts @@ -7,6 +7,12 @@ import { createBioforestKeypair, decrypt, isBioforestChain, signMessage, verifyP export type SignatureAuthError = 'rejected' | 'timeout' | 'insufficient_balance' +/** 签名结果(包含公钥用于验签) */ +export interface SignatureResult { + signature: string + publicKey: string +} + /** * Signature authorization service (mock-first) * @@ -39,12 +45,12 @@ export class SignatureAuthService { * Handle message signing (mock-first). * * - Always verifies password before proceeding. - * - For BioForest chains: decrypts secret and returns a real Ed25519 signature (hex). - * - For non-BioForest chains: returns a deterministic mock signature (hex). + * - For BioForest chains: decrypts secret and returns a real Ed25519 signature (hex) with public key. + * - For non-BioForest chains: returns a deterministic mock signature (hex) with mock public key. * * NOTE: This method does not call `approve()` automatically to keep UI control explicit. */ - async handleMessageSign(payload: MessagePayload, encryptedSecret: EncryptedData, password: string): Promise { + async handleMessageSign(payload: MessagePayload, encryptedSecret: EncryptedData, password: string): Promise { const chainName = payload.chainName.trim().toLowerCase() if (isBioforestChain(chainName)) { @@ -57,7 +63,10 @@ export class SignatureAuthService { const keypair = createBioforestKeypair(secret) const signature = signMessage(payload.message, keypair.secretKey) - return `0x${bytesToHex(signature)}` + return { + signature: `0x${bytesToHex(signature)}`, + publicKey: bytesToHex(keypair.publicKey), + } } const ok = await verifyPassword(encryptedSecret, password) @@ -71,7 +80,12 @@ export class SignatureAuthService { const sig = new Uint8Array(64) sig.set(partA, 0) sig.set(partB, 32) - return `0x${bytesToHex(sig)}` + // Mock public key for non-bioforest chains (32 bytes => 64 hex chars) + const mockPubKey = sha256(encoder.encode(`pubkey:${payload.senderAddress}`)) + return { + signature: `0x${bytesToHex(sig)}`, + publicKey: bytesToHex(mockPubKey), + } } } diff --git a/src/services/ecosystem/handlers/context.ts b/src/services/ecosystem/handlers/context.ts index 559d7cdd..f157d27a 100644 --- a/src/services/ecosystem/handlers/context.ts +++ b/src/services/ecosystem/handlers/context.ts @@ -18,6 +18,12 @@ export interface SigningParams { app: MiniappInfo } +/** 签名结果 */ +export interface SigningResult { + signature: string + publicKey: string +} + /** 交易签名参数 */ export interface SignTransactionParams { from: string @@ -30,7 +36,7 @@ export interface SignTransactionParams { export interface HandlerCallbacks { showWalletPicker: (opts?: { chain?: string; exclude?: string; app?: MiniappInfo }) => Promise getConnectedAccounts: () => BioAccount[] - showSigningDialog: (params: SigningParams) => Promise + showSigningDialog: (params: SigningParams) => Promise showTransferDialog: (params: TransferParams & { app: MiniappInfo }) => Promise<{ txHash: string } | null> showSignTransactionDialog: (params: SignTransactionParams) => Promise } diff --git a/src/services/ecosystem/handlers/index.ts b/src/services/ecosystem/handlers/index.ts index c136076c..4f81684e 100644 --- a/src/services/ecosystem/handlers/index.ts +++ b/src/services/ecosystem/handlers/index.ts @@ -12,6 +12,7 @@ export { handlePickWallet, handleChainId, handleGetBalance, + handleGetPublicKey, setWalletPicker, setGetAccounts, } from './wallet' diff --git a/src/services/ecosystem/handlers/signing.ts b/src/services/ecosystem/handlers/signing.ts index 72fddc78..a87a9bb1 100644 --- a/src/services/ecosystem/handlers/signing.ts +++ b/src/services/ecosystem/handlers/signing.ts @@ -4,10 +4,10 @@ import type { MethodHandler } from '../types' import { BioErrorCodes } from '../types' -import { HandlerContext, type SigningParams } from './context' +import { HandlerContext, type SigningParams, type SigningResult } from './context' -// 兼容旧 API -let _showSigningDialog: ((params: SigningParams) => Promise) | null = null +// 兼容旧 API(现在返回 SigningResult) +let _showSigningDialog: ((params: SigningParams) => Promise) | null = null /** @deprecated 使用 HandlerContext.register 替代 */ export function setSigningDialog(dialog: typeof _showSigningDialog): void { @@ -20,7 +20,7 @@ function getSigningDialog(appId: string) { return callbacks?.showSigningDialog ?? _showSigningDialog } -/** bio_signMessage - Sign a message */ +/** bio_signMessage - Sign a message, returns { signature, publicKey } */ export const handleSignMessage: MethodHandler = async (params, context) => { const opts = params as { message?: string; address?: string } | undefined if (!opts?.message || !opts?.address) { @@ -32,20 +32,21 @@ export const handleSignMessage: MethodHandler = async (params, context) => { throw Object.assign(new Error('Signing dialog not available'), { code: BioErrorCodes.INTERNAL_ERROR }) } - const signature = await showSigningDialog({ + const result = await showSigningDialog({ message: opts.message, address: opts.address, app: { name: context.appName }, }) - if (!signature) { + if (!result) { throw Object.assign(new Error('User rejected'), { code: BioErrorCodes.USER_REJECTED }) } - return signature + // 返回 { signature, publicKey },公钥为 hex 格式 + return result } -/** bio_signTypedData - Sign typed data */ +/** bio_signTypedData - Sign typed data, returns { signature, publicKey } */ export const handleSignTypedData: MethodHandler = async (params, context) => { const opts = params as { data?: object; address?: string } | undefined if (!opts?.data || !opts?.address) { @@ -60,15 +61,16 @@ export const handleSignTypedData: MethodHandler = async (params, context) => { // Convert typed data to readable message const message = JSON.stringify(opts.data, null, 2) - const signature = await showSigningDialog({ + const result = await showSigningDialog({ message, address: opts.address, app: { name: context.appName }, }) - if (!signature) { + if (!result) { throw Object.assign(new Error('User rejected'), { code: BioErrorCodes.USER_REJECTED }) } - return signature + // 返回 { signature, publicKey },公钥为 hex 格式 + return result } diff --git a/src/services/ecosystem/handlers/wallet.ts b/src/services/ecosystem/handlers/wallet.ts index 6ebc2ace..9996140e 100644 --- a/src/services/ecosystem/handlers/wallet.ts +++ b/src/services/ecosystem/handlers/wallet.ts @@ -109,3 +109,31 @@ export const handleGetBalance: MethodHandler = async (params, _context) => { // TODO: Query actual balance from chain adapter return '0' } + +/** bio_getPublicKey - Get public key for an address */ +export const handleGetPublicKey: MethodHandler = async (params, context) => { + const opts = params as { address?: string } | undefined + if (!opts?.address) { + throw Object.assign(new Error('Missing address'), { code: BioErrorCodes.INVALID_PARAMS }) + } + + const getConnectedAccounts = getAccountsGetter(context.appId) + if (!getConnectedAccounts) { + throw Object.assign(new Error('Not connected'), { code: BioErrorCodes.UNAUTHORIZED }) + } + + const accounts = getConnectedAccounts() + const account = accounts.find(a => a.address.toLowerCase() === opts.address!.toLowerCase()) + if (!account) { + throw Object.assign(new Error('Address not found in connected accounts'), { code: BioErrorCodes.UNAUTHORIZED }) + } + + // TODO: Retrieve actual public key from wallet store + // For BioForest chains, the public key can be derived from the mnemonic + // For now, this returns a placeholder - needs to be implemented with wallet integration + // The public key format should be confirmed with the backend (hex, base58, etc.) + throw Object.assign( + new Error('bio_getPublicKey not yet implemented - requires wallet store integration'), + { code: BioErrorCodes.UNSUPPORTED_METHOD } + ) +} diff --git a/src/services/ecosystem/provider.ts b/src/services/ecosystem/provider.ts index f9dc80af..15c8ff5d 100644 --- a/src/services/ecosystem/provider.ts +++ b/src/services/ecosystem/provider.ts @@ -13,6 +13,7 @@ import { handlePickWallet, handleChainId, handleGetBalance, + handleGetPublicKey, handleSignMessage, handleSignTypedData, handleCreateTransaction, @@ -30,6 +31,7 @@ export function initBioProvider(): void { bridge.registerHandler('bio_pickWallet', handlePickWallet) bridge.registerHandler('bio_chainId', handleChainId) bridge.registerHandler('bio_getBalance', handleGetBalance) + bridge.registerHandler('bio_getPublicKey', handleGetPublicKey) // Signing methods bridge.registerHandler('bio_signMessage', handleSignMessage) @@ -50,6 +52,7 @@ export function initBioProvider(): void { 'bio_pickWallet', 'bio_chainId', 'bio_getBalance', + 'bio_getPublicKey', 'bio_signMessage', 'bio_signTypedData', 'bio_createTransaction', diff --git a/src/stackflow/activities/sheets/SigningConfirmJob.tsx b/src/stackflow/activities/sheets/SigningConfirmJob.tsx index 3b3c329d..fe76edfd 100644 --- a/src/stackflow/activities/sheets/SigningConfirmJob.tsx +++ b/src/stackflow/activities/sheets/SigningConfirmJob.tsx @@ -54,8 +54,8 @@ function SigningConfirmJobContent() { const eventId = `miniapp_sign_${Date.now()}` const authService = new SignatureAuthService(plaocAdapter, eventId) - // 执行真实签名 - const signature = await authService.handleMessageSign( + // 执行真实签名(返回 { signature, publicKey }) + const signResult = await authService.handleMessageSign( { chainName: chainName || 'bioforest', senderAddress: address, @@ -65,9 +65,13 @@ function SigningConfirmJobContent() { password ) - // 发送成功事件 + // 发送成功事件(包含 signature 和 publicKey) const event = new CustomEvent('signing-confirm', { - detail: { confirmed: true, signature }, + detail: { + confirmed: true, + signature: signResult.signature, + publicKey: signResult.publicKey, + }, }) window.dispatchEvent(event)