diff --git "a/docs/white-book/10-\347\224\237\346\200\201\347\257\207/02-BioSDK\345\274\200\345\217\221\346\214\207\345\215\227.md" "b/docs/white-book/10-\347\224\237\346\200\201\347\257\207/02-BioSDK\345\274\200\345\217\221\346\214\207\345\215\227.md" index 9f9e58772..9b4e57bfa 100644 --- "a/docs/white-book/10-\347\224\237\346\200\201\347\257\207/02-BioSDK\345\274\200\345\217\221\346\214\207\345\215\227.md" +++ "b/docs/white-book/10-\347\224\237\346\200\201\347\257\207/02-BioSDK\345\274\200\345\217\221\346\214\207\345\215\227.md" @@ -13,6 +13,11 @@ pnpm add @biochain/bio-sdk ```typescript import '@biochain/bio-sdk' +// KeyApp Miniapp 环境会注入: +// - window.bio (KeyApp/BioChain 专用 API) +// - window.ethereum (EIP-1193 兼容,面向 EVM: Ethereum/BSC) +// - window.tronWeb / window.tronLink (TronLink 兼容,面向 TRON) + // window.bio 现在可用 async function connect() { const accounts = await window.bio.request({ @@ -22,6 +27,16 @@ async function connect() { } ``` +## 链标识(非常重要) + +KeyApp 内部链 ID 与常见外部标识存在差异: + +- `ETH` / `eth` / `0x1` → `ethereum` +- `BSC` / `bsc` / `0x38` → `binance` +- `TRON` / `tron` → `tron` + +建议在调用 `window.bio` 的 `chain` 参数时使用 KeyApp 内部 ID(`ethereum` / `binance` / `tron`),避免出现“暂无支持 bsc 的钱包”这类匹配失败。 + ## API 参考 ### request(args) @@ -161,7 +176,7 @@ const result = await window.bio.request<{ txHash: string }>({ ```typescript type BioUnsignedTransaction = { - chain: string + chainId: string data: unknown } @@ -182,9 +197,9 @@ const unsignedTx = await window.bio.request({ ```typescript type BioSignedTransaction = { - chain: string - raw: string // 链特定的 raw tx(例如 EVM 的 RLP hex) - signature?: string + chainId: string + data: unknown // 链特定的签名产物(例如 EVM 的 raw tx hex) + signature: string } const signedTx = await window.bio.request({ @@ -197,6 +212,33 @@ const signedTx = await window.bio.request({ }) ``` +## EVM Provider(window.ethereum) + +面向传统 dApp(EIP-1193 规范)的注入对象:`window.ethereum`。 + +常用示例: + +```typescript +const chainId = await window.ethereum.request({ method: 'eth_chainId' }) +const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }) +``` + +当前支持(按需逐步完善): + +- `eth_chainId` / `eth_accounts` / `eth_requestAccounts` +- `wallet_switchEthereumChain` +- `personal_sign` / `eth_sign` / `eth_signTypedData_v4` + +## TRON Provider(window.tronLink / window.tronWeb) + +面向 TronLink 生态的注入对象:`window.tronLink` 与 `window.tronWeb`(兼容常见调用路径)。 + +常用示例: + +```typescript +const result = await window.tronLink.request({ method: 'tron_requestAccounts' }) +``` + ## 事件 ### accountsChanged diff --git a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-01-connect.png b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-01-connect.png index 36ff3dc5d..03517901a 100644 Binary files a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-01-connect.png and b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-01-connect.png differ diff --git a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-02-connect-dark.png b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-02-connect-dark.png index c803f2d1e..03517901a 100644 Binary files a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-02-connect-dark.png and b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-02-connect-dark.png differ diff --git a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-03-green-theme.png b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-03-green-theme.png index 744071afe..03517901a 100644 Binary files a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-03-green-theme.png and b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-03-green-theme.png differ diff --git a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-04-dark-purple-theme.png b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-04-dark-purple-theme.png index d324fcffe..03517901a 100644 Binary files a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-04-dark-purple-theme.png and b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/forge-04-dark-purple-theme.png differ diff --git a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-01-connect.png b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-01-connect.png index 36ff3dc5d..ac81e46bb 100644 Binary files a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-01-connect.png and b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-01-connect.png differ diff --git a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-02-connect-dark.png b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-02-connect-dark.png index c803f2d1e..ac81e46bb 100644 Binary files a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-02-connect-dark.png and b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-02-connect-dark.png differ diff --git a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-03-connect-blue-theme.png b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-03-connect-blue-theme.png index 6b9ae99b8..ac81e46bb 100644 Binary files a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-03-connect-blue-theme.png and b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-03-connect-blue-theme.png differ diff --git a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-04-green-theme.png b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-04-green-theme.png index 62ab03f5f..ac81e46bb 100644 Binary files a/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-04-green-theme.png and b/e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts/teleport-04-green-theme.png differ diff --git a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-01-connect.png b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-01-connect.png index 36ff3dc5d..03517901a 100644 Binary files a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-01-connect.png and b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-01-connect.png differ diff --git a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-02-connect-dark.png b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-02-connect-dark.png index c803f2d1e..03517901a 100644 Binary files a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-02-connect-dark.png and b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-02-connect-dark.png differ diff --git a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-03-green-theme.png b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-03-green-theme.png index 744071afe..03517901a 100644 Binary files a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-03-green-theme.png and b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-03-green-theme.png differ diff --git a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-04-dark-purple-theme.png b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-04-dark-purple-theme.png index d324fcffe..03517901a 100644 Binary files a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-04-dark-purple-theme.png and b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/forge-04-dark-purple-theme.png differ diff --git a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-01-connect.png b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-01-connect.png index 36ff3dc5d..ac81e46bb 100644 Binary files a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-01-connect.png and b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-01-connect.png differ diff --git a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-02-connect-dark.png b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-02-connect-dark.png index c803f2d1e..ac81e46bb 100644 Binary files a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-02-connect-dark.png and b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-02-connect-dark.png differ diff --git a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-03-connect-blue-theme.png b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-03-connect-blue-theme.png index 6b9ae99b8..ac81e46bb 100644 Binary files a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-03-connect-blue-theme.png and b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-03-connect-blue-theme.png differ diff --git a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-04-green-theme.png b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-04-green-theme.png index 62ab03f5f..ac81e46bb 100644 Binary files a/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-04-green-theme.png and b/e2e/__screenshots__/Mobile-Chrome/miniapp-ui.mock.spec.ts/teleport-04-green-theme.png differ diff --git a/e2e/miniapp-ui.mock.spec.ts b/e2e/miniapp-ui.mock.spec.ts index a8f96c115..e60e29653 100644 --- a/e2e/miniapp-ui.mock.spec.ts +++ b/e2e/miniapp-ui.mock.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test' +import { test, expect, type Page } from '@playwright/test' /** * 小程序 UI 截图测试 @@ -6,6 +6,26 @@ import { test, expect } from '@playwright/test' * 直接测试小程序界面,验证共享组件和主题正常工作 */ +async function getMiniappBaseUrl(page: Page, appId: string): Promise { + const res = await page.request.get('/miniapps/ecosystem.json') + expect(res.ok()).toBeTruthy() + + const data = (await res.json()) as { apps?: Array<{ id: string; url: string }> } + const app = data.apps?.find((a) => a.id === appId) + expect(app?.url).toBeTruthy() + + return app!.url +} + +async function gotoMiniapp(page: Page, appId: string, query?: string): Promise { + const baseUrl = await getMiniappBaseUrl(page, appId) + const url = query ? `${baseUrl}${query}` : baseUrl + + await page.goto(url) + await expect(page.getByTestId('connect-button')).toBeVisible() + await expect(page.getByTestId('connect-button')).toBeEnabled() +} + test.describe('Teleport 小程序 UI', () => { test.beforeEach(async ({ page }) => { // 设置视口为移动端尺寸 @@ -13,36 +33,30 @@ test.describe('Teleport 小程序 UI', () => { }) test('连接页面 - 初始状态', async ({ page }) => { - await page.goto('/miniapps/teleport/index.html') - await page.waitForLoadState('networkidle') - await page.waitForTimeout(500) - + await gotoMiniapp(page, 'xin.dweb.teleport') await expect(page).toHaveScreenshot('teleport-01-connect.png') }) test('连接页面 - 暗色主题', async ({ page }) => { - await page.goto('/miniapps/teleport/index.html?colorMode=dark') - await page.waitForLoadState('networkidle') - await page.waitForTimeout(500) + await gotoMiniapp(page, 'xin.dweb.teleport', '?colorMode=dark') // 手动添加 dark class(因为小程序需要接收 context) await page.evaluate(() => { document.documentElement.classList.add('dark') }) - await page.waitForTimeout(300) + await expect(page.getByTestId('connect-button')).toBeVisible() await expect(page).toHaveScreenshot('teleport-02-connect-dark.png') }) test('连接页面 - 不同主题色', async ({ page }) => { - await page.goto('/miniapps/teleport/index.html?primaryHue=200') - await page.waitForLoadState('networkidle') + await gotoMiniapp(page, 'xin.dweb.teleport', '?primaryHue=200') // 设置蓝色主题 await page.evaluate(() => { document.documentElement.style.setProperty('--primary-hue', '200') }) - await page.waitForTimeout(300) + await expect(page.getByTestId('connect-button')).toBeVisible() await expect(page).toHaveScreenshot('teleport-03-connect-blue-theme.png') }) @@ -54,34 +68,28 @@ test.describe('Forge 小程序 UI', () => { }) test('连接页面 - 初始状态', async ({ page }) => { - await page.goto('/miniapps/forge/index.html') - await page.waitForLoadState('networkidle') - await page.waitForTimeout(500) - + await gotoMiniapp(page, 'xin.dweb.forge') await expect(page).toHaveScreenshot('forge-01-connect.png') }) test('连接页面 - 暗色主题', async ({ page }) => { - await page.goto('/miniapps/forge/index.html?colorMode=dark') - await page.waitForLoadState('networkidle') - await page.waitForTimeout(500) + await gotoMiniapp(page, 'xin.dweb.forge', '?colorMode=dark') await page.evaluate(() => { document.documentElement.classList.add('dark') }) - await page.waitForTimeout(300) + await expect(page.getByTestId('connect-button')).toBeVisible() await expect(page).toHaveScreenshot('forge-02-connect-dark.png') }) test('连接页面 - 自定义主题色', async ({ page }) => { - await page.goto('/miniapps/forge/index.html?primaryHue=145') - await page.waitForLoadState('networkidle') + await gotoMiniapp(page, 'xin.dweb.forge', '?primaryHue=145') await page.evaluate(() => { document.documentElement.style.setProperty('--primary-hue', '145') }) - await page.waitForTimeout(300) + await expect(page.getByTestId('connect-button')).toBeVisible() await expect(page).toHaveScreenshot('forge-03-green-theme.png') }) @@ -92,14 +100,13 @@ test.describe('小程序主题同步', () => { await page.setViewportSize({ width: 375, height: 667 }) // 使用绿色主题 (hue=145) - await page.goto('/miniapps/teleport/index.html?primaryHue=145&primarySaturation=0.2') - await page.waitForLoadState('networkidle') + await gotoMiniapp(page, 'xin.dweb.teleport', '?primaryHue=145&primarySaturation=0.2') await page.evaluate(() => { document.documentElement.style.setProperty('--primary-hue', '145') document.documentElement.style.setProperty('--primary-saturation', '0.2') }) - await page.waitForTimeout(300) + await expect(page.getByTestId('connect-button')).toBeVisible() await expect(page).toHaveScreenshot('teleport-04-green-theme.png') }) @@ -107,14 +114,13 @@ test.describe('小程序主题同步', () => { test('Forge - 暗色 + 自定义主题色', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }) - await page.goto('/miniapps/forge/index.html?colorMode=dark&primaryHue=280') - await page.waitForLoadState('networkidle') + await gotoMiniapp(page, 'xin.dweb.forge', '?colorMode=dark&primaryHue=280') await page.evaluate(() => { document.documentElement.classList.add('dark') document.documentElement.style.setProperty('--primary-hue', '280') }) - await page.waitForTimeout(300) + await expect(page.getByTestId('connect-button')).toBeVisible() await expect(page).toHaveScreenshot('forge-04-dark-purple-theme.png') }) diff --git a/miniapps/forge/src/App.stories.tsx b/miniapps/forge/src/App.stories.tsx index 384c3c9a0..d51da13a4 100644 --- a/miniapps/forge/src/App.stories.tsx +++ b/miniapps/forge/src/App.stories.tsx @@ -47,6 +47,23 @@ const setupMockApi = () => { }) } +type EthereumRequest = (args: { method: string; params?: unknown[] }) => Promise + +function setupMockEthereumProvider(opts?: { + accounts?: string[] +}): void { + const accounts = opts?.accounts ?? ['0x1111111111111111111111111111111111111111'] + + const request: EthereumRequest = fn().mockImplementation(({ method }) => { + if (method === 'wallet_switchEthereumChain') return Promise.resolve(null) + if (method === 'eth_requestAccounts') return Promise.resolve(accounts) + if (method === 'eth_chainId') return Promise.resolve('0x1') + return Promise.resolve(null) + }) + + window.ethereum = { request } as unknown as typeof window.ethereum +} + const meta = { title: 'App/ForgeApp', component: App, @@ -59,6 +76,7 @@ const meta = { decorators: [ (Story) => { setupMockApi() + setupMockEthereumProvider() return (
@@ -103,14 +121,17 @@ export const SwapStep: Story = { decorators: [ (Story) => { setupMockApi() + setupMockEthereumProvider() // Mock bio SDK with connected wallet // @ts-expect-error - mock global window.bio = { - request: fn().mockImplementation(({ method }: { method: string }) => { + request: fn().mockImplementation(({ method, params }: { method: string; params?: unknown[] }) => { if (method === 'bio_selectAccount') { + const chain = (params?.[0] as { chain?: string } | undefined)?.chain ?? 'bfmeta' return Promise.resolve({ - address: '0x1234567890abcdef1234567890abcdef12345678', - chain: 'eth', + address: chain === 'bfmeta' ? 'bfmeta123' : '0x1234567890abcdef1234567890abcdef12345678', + chain, + publicKey: '0x', }) } if (method === 'bio_closeSplashScreen') { @@ -222,13 +243,16 @@ export const TokenPicker: Story = { decorators: [ (Story) => { setupMockApi() + setupMockEthereumProvider() // @ts-expect-error - mock global window.bio = { - request: fn().mockImplementation(({ method }: { method: string }) => { + request: fn().mockImplementation(({ method, params }: { method: string; params?: unknown[] }) => { if (method === 'bio_selectAccount') { + const chain = (params?.[0] as { chain?: string } | undefined)?.chain ?? 'bfmeta' return Promise.resolve({ - address: '0x1234567890abcdef1234567890abcdef12345678', - chain: 'eth', + address: chain === 'bfmeta' ? 'bfmeta123' : '0x1234567890abcdef1234567890abcdef12345678', + chain, + publicKey: '0x', }) } if (method === 'bio_closeSplashScreen') { @@ -286,17 +310,19 @@ export const LoadingState: Story = { decorators: [ (Story) => { setupMockApi() + setupMockEthereumProvider() // Mock slow bio SDK // @ts-expect-error - mock global window.bio = { - request: fn().mockImplementation(({ method }: { method: string }) => { + request: fn().mockImplementation(({ method, params }: { method: string; params?: unknown[] }) => { if (method === 'bio_selectAccount') { // Simulate slow connection return new Promise((resolve) => { setTimeout(() => { resolve({ - address: '0x123', - chain: 'eth', + address: ((params?.[0] as { chain?: string } | undefined)?.chain ?? 'bfmeta') === 'bfmeta' ? 'bfmeta123' : '0x123', + chain: (params?.[0] as { chain?: string } | undefined)?.chain ?? 'bfmeta', + publicKey: '0x', }) }, 10000) }) diff --git a/miniapps/forge/src/App.test.tsx b/miniapps/forge/src/App.test.tsx index 9a49be4a9..de4903287 100644 --- a/miniapps/forge/src/App.test.tsx +++ b/miniapps/forge/src/App.test.tsx @@ -39,10 +39,27 @@ const mockBio = { isConnected: vi.fn(() => true), } +const mockEthereum = { + request: vi.fn(), +} + describe('Forge App', () => { beforeEach(() => { vi.clearAllMocks() ;(window as unknown as { bio: typeof mockBio }).bio = mockBio + + mockBio.request.mockImplementation(({ method }: { method: string }) => { + if (method === 'bio_closeSplashScreen') return Promise.resolve(null) + if (method === 'bio_selectAccount') return Promise.resolve({ address: 'bfmeta123', chain: 'bfmeta' }) + return Promise.resolve(null) + }) + + // Default EVM provider mock (ETH in test config) + ;(window as unknown as { ethereum: typeof mockEthereum }).ethereum = mockEthereum + mockEthereum.request.mockImplementation(({ method }: { method: string }) => { + if (method === 'eth_requestAccounts') return Promise.resolve(['0xexternal123']) + return Promise.resolve(null) + }) }) it('should render initial connect step after config loads', async () => { @@ -63,26 +80,33 @@ describe('Forge App', () => { render() await waitFor(() => { - expect(screen.getByRole('button', { name: '连接钱包' })).toBeInTheDocument() + expect(screen.getByTestId('connect-button')).toBeInTheDocument() }) - fireEvent.click(screen.getByRole('button', { name: '连接钱包' })) + // Ensure option auto-selected and button enabled + await waitFor(() => { + expect(screen.getByTestId('connect-button')).not.toBeDisabled() + }) + + fireEvent.click(screen.getByTestId('connect-button')) await waitFor(() => { - expect(screen.getByRole('button', { name: '连接中...' })).toBeInTheDocument() + expect(screen.getByTestId('connect-button')).toHaveTextContent('连接中...') }) }) it('should proceed to swap step after selecting wallet', async () => { - mockBio.request.mockResolvedValue({ address: '0x123', chain: 'ethereum' }) - render() await waitFor(() => { - expect(screen.getByRole('button', { name: '连接钱包' })).toBeInTheDocument() + expect(screen.getByTestId('connect-button')).toBeInTheDocument() }) - fireEvent.click(screen.getByRole('button', { name: '连接钱包' })) + await waitFor(() => { + expect(screen.getByTestId('connect-button')).not.toBeDisabled() + }) + + fireEvent.click(screen.getByTestId('connect-button')) await waitFor(() => { expect(screen.getByText(/支付/)).toBeInTheDocument() @@ -95,10 +119,14 @@ describe('Forge App', () => { render() await waitFor(() => { - expect(screen.getByRole('button', { name: '连接钱包' })).toBeInTheDocument() + expect(screen.getByTestId('connect-button')).toBeInTheDocument() + }) + + await waitFor(() => { + expect(screen.getByTestId('connect-button')).not.toBeDisabled() }) - fireEvent.click(screen.getByRole('button', { name: '连接钱包' })) + fireEvent.click(screen.getByTestId('connect-button')) await waitFor(() => { expect(screen.getByText('Bio SDK 未初始化')).toBeInTheDocument() @@ -106,15 +134,22 @@ describe('Forge App', () => { }) it('should show error when connection fails', async () => { - mockBio.request.mockRejectedValue(new Error('Connection failed')) + mockBio.request.mockImplementation(({ method }: { method: string }) => { + if (method === 'bio_closeSplashScreen') return Promise.resolve(null) + return Promise.reject(new Error('Connection failed')) + }) render() await waitFor(() => { - expect(screen.getByRole('button', { name: '连接钱包' })).toBeInTheDocument() + expect(screen.getByTestId('connect-button')).toBeInTheDocument() + }) + + await waitFor(() => { + expect(screen.getByTestId('connect-button')).not.toBeDisabled() }) - fireEvent.click(screen.getByRole('button', { name: '连接钱包' })) + fireEvent.click(screen.getByTestId('connect-button')) await waitFor(() => { expect(screen.getByText('Connection failed')).toBeInTheDocument() @@ -122,18 +157,20 @@ describe('Forge App', () => { }) it('should call bio_selectAccount on connect', async () => { - mockBio.request.mockResolvedValue({ address: '0x123', chain: 'eth' }) - render() await waitFor(() => { - expect(screen.getByRole('button', { name: '连接钱包' })).toBeInTheDocument() + expect(screen.getByTestId('connect-button')).toBeInTheDocument() + }) + + await waitFor(() => { + expect(screen.getByTestId('connect-button')).not.toBeDisabled() }) - fireEvent.click(screen.getByRole('button', { name: '连接钱包' })) + fireEvent.click(screen.getByTestId('connect-button')) await waitFor(() => { - // Should call bio_selectAccount at least once (for external and internal accounts) + // Should call bio_selectAccount at least once (internal account) expect(mockBio.request).toHaveBeenCalledWith( expect.objectContaining({ method: 'bio_selectAccount', diff --git a/miniapps/forge/src/App.tsx b/miniapps/forge/src/App.tsx index 0a4d7db6d..18da485ea 100644 --- a/miniapps/forge/src/App.tsx +++ b/miniapps/forge/src/App.tsx @@ -1,6 +1,8 @@ import { useState, useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import type { BioAccount } from '@biochain/bio-sdk' +import { normalizeChainId } from '@biochain/bio-sdk' +import { getChainType, getEvmChainIdFromApi } from '@/lib/chain' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' import { Input } from '@/components/ui/input' @@ -77,20 +79,72 @@ export default function App() { setError('Bio SDK 未初始化') return } + if (!selectedOption) { + setError('请选择兑换选项') + return + } setLoading(true) setError(null) try { - // Select external chain account (for payment) - const extAcc = await window.bio.request({ - method: 'bio_selectAccount', - params: [{ chain: selectedOption?.externalChain?.toLowerCase() }], - }) + const externalChain = selectedOption.externalChain + const chainType = getChainType(externalChain) + + let extAcc: BioAccount + + if (chainType === 'evm') { + // Use window.ethereum for EVM chains (ETH, BSC) + if (!window.ethereum) { + throw new Error('Ethereum provider not available') + } + const evmChainId = getEvmChainIdFromApi(externalChain) + if (evmChainId) { + // Switch to the correct chain first + await window.ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: evmChainId }], + }) + } + // Request accounts + const accounts = await window.ethereum.request({ + method: 'eth_requestAccounts', + }) + if (!accounts || accounts.length === 0) { + throw new Error('No accounts returned') + } + extAcc = { + address: accounts[0], + chain: normalizeChainId(externalChain), + publicKey: '', // EVM doesn't expose public key directly + } + } else if (chainType === 'tron') { + // Use window.tronLink for TRON + if (!window.tronLink) { + throw new Error('TronLink provider not available') + } + const result = await window.tronLink.request<{ code: number; message: string; data: { base58: string } }>({ + method: 'tron_requestAccounts', + }) + if (!result || result.code !== 200) { + throw new Error('TRON connection failed') + } + extAcc = { + address: result.data.base58, + chain: 'tron', + publicKey: '', + } + } else { + // Use bio_selectAccount for BioChain + extAcc = await window.bio.request({ + method: 'bio_selectAccount', + params: [{ chain: normalizeChainId(externalChain) }], + }) + } setExternalAccount(extAcc) - // Select internal chain account (for receiving) + // Select internal chain account (for receiving) - always use bio const intAcc = await window.bio.request({ method: 'bio_selectAccount', - params: [{ chain: selectedOption?.internalChain }], + params: [{ chain: selectedOption.internalChain }], }) setInternalAccount(intAcc) @@ -136,11 +190,6 @@ export default function App() { forgeHook.reset() }, [forgeHook]) - const handleSelectOption = (option: ForgeOption) => { - setSelectedOption(option) - setPickerOpen(false) - } - // Group options by external chain for picker const groupedOptions = useMemo(() => { const groups: Record = {} @@ -152,6 +201,19 @@ export default function App() { return groups }, [forgeOptions]) + const handleSelectOption = (option: ForgeOption) => { + setSelectedOption(option) + setPickerOpen(false) + } + + const handleSelectExternalChain = useCallback((externalChain: string) => { + const options = groupedOptions[externalChain] + const first = options?.[0] + if (first) { + setSelectedOption(first) + } + }, [groupedOptions]) + return (
@@ -232,8 +294,18 @@ export default function App() { {forgeOptions.length > 0 && (
{Object.keys(groupedOptions).map((chain) => ( - - {getChainName(chain)} + + ))}
@@ -244,7 +316,7 @@ export default function App() { size="lg" className="w-full max-w-xs h-12" onClick={handleConnect} - disabled={loading || forgeOptions.length === 0} + disabled={loading || forgeOptions.length === 0 || !selectedOption} > {loading && } {loading ? t('connect.loading') : t('connect.button')} diff --git a/miniapps/forge/src/hooks/useForge.ts b/miniapps/forge/src/hooks/useForge.ts index 86c701b41..e6e06a988 100644 --- a/miniapps/forge/src/hooks/useForge.ts +++ b/miniapps/forge/src/hooks/useForge.ts @@ -4,6 +4,7 @@ import { useState, useCallback } from 'react' import type { BioAccount, BioSignedTransaction } from '@biochain/bio-sdk' +import { normalizeChainId } from '@biochain/bio-sdk' import { rechargeApi } from '@/api' import { encodeRechargeV2ToTrInfoData, createRechargeMessage } from '@/api/helpers' import type { @@ -44,8 +45,8 @@ export interface ForgeParams { * Build FromTrJson from signed transaction */ function buildFromTrJson(chain: ExternalChainName, signedTx: BioSignedTransaction): FromTrJson { - const signTransData = typeof signedTx.data === 'string' - ? signedTx.data + const signTransData = typeof signedTx.data === 'string' + ? signedTx.data : JSON.stringify(signedTx.data) switch (chain) { @@ -92,13 +93,15 @@ export function useForge() { // Step 1: Create and sign external chain transaction setState({ step: 'signing_external', orderId: null, error: null }) + const externalKeyAppChainId = normalizeChainId(externalChain) + const unsignedTx = await window.bio.request({ method: 'bio_createTransaction', params: [{ from: externalAccount.address, to: depositAddress, amount, - chain: externalChain.toLowerCase(), + chain: externalKeyAppChainId, asset: externalAsset, }], }) @@ -107,7 +110,7 @@ export function useForge() { method: 'bio_signTransaction', params: [{ from: externalAccount.address, - chain: externalChain.toLowerCase(), + chain: externalKeyAppChainId, unsignedTx, }], }) diff --git a/miniapps/forge/src/lib/chain.ts b/miniapps/forge/src/lib/chain.ts new file mode 100644 index 000000000..478d8de41 --- /dev/null +++ b/miniapps/forge/src/lib/chain.ts @@ -0,0 +1,45 @@ +/** + * Chain utilities for Forge miniapp + */ + +import { toHexChainId, EVM_CHAIN_IDS, API_CHAIN_TO_KEYAPP } from '@biochain/bio-sdk' + +/** Chain types */ +export type ChainType = 'evm' | 'tron' | 'bio' + +/** Map API chain names to EVM hex chainId */ +export const API_CHAIN_TO_HEX: Record = { + ETH: toHexChainId(EVM_CHAIN_IDS.ethereum), + BSC: toHexChainId(EVM_CHAIN_IDS.binance), +} + +/** + * Get chain type from API chain name + */ +export function getChainType(apiChainName: string): ChainType { + const upper = apiChainName.toUpperCase() + if (upper === 'ETH' || upper === 'BSC') return 'evm' + if (upper === 'TRON') return 'tron' + return 'bio' +} + +/** + * Get EVM hex chainId from API chain name + */ +export function getEvmChainIdFromApi(apiChainName: string): string | null { + return API_CHAIN_TO_HEX[apiChainName.toUpperCase()] ?? null +} + +/** + * Check if chain is EVM compatible + */ +export function isEvmChain(apiChainName: string): boolean { + return getChainType(apiChainName) === 'evm' +} + +/** + * Check if chain is TRON + */ +export function isTronChain(apiChainName: string): boolean { + return getChainType(apiChainName) === 'tron' +} diff --git a/miniapps/forge/src/vite-env.d.ts b/miniapps/forge/src/vite-env.d.ts index 4e934f5dc..b9a38e161 100644 --- a/miniapps/forge/src/vite-env.d.ts +++ b/miniapps/forge/src/vite-env.d.ts @@ -1,5 +1,12 @@ /// +import type { + BioProvider, + EthereumProvider, + TronLinkProvider, + TronWebProvider, +} from '@biochain/bio-sdk' + interface ImportMetaEnv { readonly VITE_COT_API_BASE_URL?: string } @@ -7,3 +14,14 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv } + +declare global { + interface Window { + bio?: BioProvider + ethereum?: EthereumProvider + tronLink?: TronLinkProvider + tronWeb?: TronWebProvider + } +} + +export {} diff --git a/package.json b/package.json index 3b8b06432..3822043d4 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@base-ui/react": "^1.0.0", "@bfchain/util": "^5.0.0", "@bfmeta/sign-util": "^1.3.10", + "@biochain/bio-sdk": "workspace:*", "@biochain/plugin-navigation-sync": "workspace:*", "@bnqkl/server-util": "^1.3.4", "@bnqkl/wallet-sdk": "^0.23.8", diff --git a/packages/bio-sdk/dist/index.cjs b/packages/bio-sdk/dist/index.cjs index c26a8a31a..2cd333fb9 100644 --- a/packages/bio-sdk/dist/index.cjs +++ b/packages/bio-sdk/dist/index.cjs @@ -146,6 +146,425 @@ class BioProviderImpl { return this.connected; } } +class EthereumProvider { + events = new EventEmitter(); + pendingRequests = /* @__PURE__ */ new Map(); + requestIdCounter = 0; + connected = false; + currentChainId = null; + accounts = []; + targetOrigin; + // EIP-1193 required properties + isMetaMask = false; + isKeyApp = true; + constructor(targetOrigin = "*") { + this.targetOrigin = targetOrigin; + this.setupMessageListener(); + } + setupMessageListener() { + window.addEventListener("message", this.handleMessage.bind(this)); + } + handleMessage(event) { + const data = event.data; + if (!data || typeof data !== "object") return; + if (data.type === "eth_response") { + this.handleResponse(data); + } else if (data.type === "eth_event") { + this.handleEvent(data); + } + } + handleResponse(message) { + const pending = this.pendingRequests.get(message.id); + if (!pending) return; + this.pendingRequests.delete(message.id); + if (message.success) { + pending.resolve(message.result); + } else { + const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: "Unknown error" }; + pending.reject(createProviderError(error.code, error.message, error.data)); + } + } + handleEvent(message) { + this.events.emit(message.event, ...message.args); + if (message.event === "connect") { + this.connected = true; + const info = message.args[0]; + this.currentChainId = info?.chainId ?? null; + } else if (message.event === "disconnect") { + this.connected = false; + this.accounts = []; + } else if (message.event === "chainChanged") { + this.currentChainId = message.args[0]; + } else if (message.event === "accountsChanged") { + this.accounts = message.args[0]; + } + } + generateId() { + return `eth_${Date.now()}_${++this.requestIdCounter}`; + } + postMessage(message) { + if (window.parent === window) { + console.warn("[EthereumProvider] Not running in iframe, cannot communicate with host"); + return; + } + window.parent.postMessage(message, this.targetOrigin); + } + /** + * EIP-1193 request method + */ + async request(args) { + const { method, params } = args; + const paramsArray = Array.isArray(params) ? params : params ? [params] : []; + const id = this.generateId(); + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { + resolve, + reject + }); + this.postMessage({ + type: "eth_request", + id, + method, + params: paramsArray + }); + setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id); + reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, "Request timeout")); + } + }, 5 * 60 * 1e3); + }); + } + /** + * Subscribe to an event + */ + on(event, handler) { + this.events.on(event, handler); + return this; + } + /** + * Unsubscribe from an event + */ + off(event, handler) { + this.events.off(event, handler); + return this; + } + /** + * Alias for off (Node.js EventEmitter compatibility) + */ + removeListener(event, handler) { + return this.off(event, handler); + } + /** + * Add listener that fires only once + */ + once(event, handler) { + const wrapper = (...args) => { + this.off(event, wrapper); + handler(...args); + }; + this.on(event, wrapper); + return this; + } + /** + * EIP-1193 isConnected method + */ + isConnected() { + return this.connected; + } + /** + * Get current chain ID (cached) + */ + get chainId() { + return this.currentChainId; + } + /** + * Get selected address (first account) + */ + get selectedAddress() { + return this.accounts[0] ?? null; + } + // ============================================ + // Legacy methods (for backwards compatibility) + // ============================================ + /** + * @deprecated Use request({ method: 'eth_requestAccounts' }) + */ + async enable() { + return this.request({ method: "eth_requestAccounts" }); + } + /** + * @deprecated Use request() + */ + send(method, params) { + return this.request({ method, params }); + } + /** + * @deprecated Use request() + */ + sendAsync(payload, callback) { + this.request({ method: payload.method, params: payload.params }).then((result) => callback(null, { result })).catch((error) => callback(error)); + } +} +function initEthereumProvider(targetOrigin = "*") { + if (typeof window === "undefined") { + throw new Error("[EthereumProvider] Cannot initialize: window is not defined"); + } + if (window.ethereum) { + console.warn("[EthereumProvider] Provider already exists, returning existing instance"); + return window.ethereum; + } + const provider = new EthereumProvider(targetOrigin); + window.ethereum = provider; + console.log("[EthereumProvider] Provider initialized"); + return provider; +} +class TronLinkProvider { + events = new EventEmitter(); + pendingRequests = /* @__PURE__ */ new Map(); + requestIdCounter = 0; + targetOrigin; + constructor(targetOrigin = "*") { + this.targetOrigin = targetOrigin; + this.setupMessageListener(); + } + setupMessageListener() { + window.addEventListener("message", this.handleMessage.bind(this)); + } + handleMessage(event) { + const data = event.data; + if (!data || typeof data !== "object") return; + if (data.type === "tron_response") { + this.handleResponse(data); + } else if (data.type === "tron_event") { + this.handleEvent(data); + } + } + handleResponse(message) { + const pending = this.pendingRequests.get(message.id); + if (!pending) return; + this.pendingRequests.delete(message.id); + if (message.success) { + pending.resolve(message.result); + } else { + const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: "Unknown error" }; + pending.reject(createProviderError(error.code, error.message, error.data)); + } + } + handleEvent(message) { + this.events.emit(message.event, ...message.args); + } + generateId() { + return `tron_${Date.now()}_${++this.requestIdCounter}`; + } + postMessage(message) { + if (window.parent === window) { + console.warn("[TronLinkProvider] Not running in iframe, cannot communicate with host"); + return; + } + window.parent.postMessage(message, this.targetOrigin); + } + /** + * TronLink request method (EIP-1193 style) + */ + async request(args) { + const { method, params } = args; + const paramsArray = Array.isArray(params) ? params : params !== void 0 ? [params] : []; + const id = this.generateId(); + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { + resolve, + reject + }); + this.postMessage({ + type: "tron_request", + id, + method, + params: paramsArray + }); + setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id); + reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, "Request timeout")); + } + }, 5 * 60 * 1e3); + }); + } + on(event, handler) { + this.events.on(event, handler); + return this; + } + off(event, handler) { + this.events.off(event, handler); + return this; + } +} +class TronWebProvider { + tronLink; + _ready = false; + _defaultAddress = { base58: "", hex: "" }; + /** TRX operations */ + trx; + constructor(tronLink) { + this.tronLink = tronLink; + this.trx = new TronWebTrx(tronLink); + tronLink.on("accountsChanged", (accounts) => { + if (Array.isArray(accounts) && accounts.length > 0) { + const addr = accounts[0]; + this._defaultAddress = addr; + this._ready = true; + } else { + this._defaultAddress = { base58: "", hex: "" }; + this._ready = false; + } + }); + } + /** Whether TronWeb is ready (connected) */ + get ready() { + return this._ready; + } + /** Current default address */ + get defaultAddress() { + return this._defaultAddress; + } + /** + * Set default address (called by host after connection) + */ + setAddress(address) { + this._defaultAddress = address; + this._ready = true; + } + /** + * Check if an address is valid + */ + isAddress(address) { + if (address.startsWith("T")) { + return address.length === 34; + } + if (address.startsWith("41")) { + return address.length === 42; + } + return false; + } + /** + * Convert address to hex format + */ + address = { + toHex: (base58) => { + return base58; + }, + fromHex: (hex) => { + return hex; + } + }; +} +class TronWebTrx { + tronLink; + constructor(tronLink) { + this.tronLink = tronLink; + } + /** + * Sign a transaction + */ + async sign(transaction) { + return this.tronLink.request({ + method: "tron_signTransaction", + params: transaction + }); + } + /** + * Send raw transaction (broadcast) + */ + async sendRawTransaction(signedTransaction) { + return this.tronLink.request({ + method: "tron_sendRawTransaction", + params: signedTransaction + }); + } + /** + * Get account balance + */ + async getBalance(address) { + return this.tronLink.request({ + method: "tron_getBalance", + params: address + }); + } + /** + * Get account info + */ + async getAccount(address) { + return this.tronLink.request({ + method: "tron_getAccount", + params: address + }); + } +} +function initTronProvider(targetOrigin = "*") { + if (typeof window === "undefined") { + throw new Error("[TronProvider] Cannot initialize: window is not defined"); + } + if (window.tronLink && window.tronWeb) { + console.warn("[TronProvider] Providers already exist, returning existing instances"); + return { tronLink: window.tronLink, tronWeb: window.tronWeb }; + } + const tronLink = new TronLinkProvider(targetOrigin); + const tronWeb = new TronWebProvider(tronLink); + window.tronLink = tronLink; + window.tronWeb = tronWeb; + console.log("[TronProvider] Providers initialized"); + return { tronLink, tronWeb }; +} +const EVM_CHAIN_IDS = { + ethereum: 1, + binance: 56 + // Future chains + // polygon: 137, + // arbitrum: 42161, + // optimism: 10, +}; +const EVM_CHAIN_ID_TO_KEYAPP = Object.fromEntries( + Object.entries(EVM_CHAIN_IDS).map(([key, value]) => [value, key]) +); +const API_CHAIN_TO_KEYAPP = { + ETH: "ethereum", + BSC: "binance", + TRON: "tron", + // Lowercase variants + eth: "ethereum", + bsc: "binance", + tron: "tron" +}; +const CHAIN_DISPLAY_NAMES = { + ethereum: "Ethereum", + binance: "BNB Smart Chain", + tron: "Tron", + bfmeta: "BFMeta", + bfchain: "BFChain" +}; +function toHexChainId(chainId) { + return `0x${chainId.toString(16)}`; +} +function parseHexChainId(hexChainId) { + if (!hexChainId.startsWith("0x")) { + throw new Error(`Invalid hex chain ID: ${hexChainId}`); + } + return parseInt(hexChainId, 16); +} +function getKeyAppChainId(hexChainId) { + const decimal = parseHexChainId(hexChainId); + return EVM_CHAIN_ID_TO_KEYAPP[decimal] ?? null; +} +function getEvmChainId(keyAppChainId) { + const decimal = EVM_CHAIN_IDS[keyAppChainId]; + return decimal ? toHexChainId(decimal) : null; +} +function isEvmChain(chainId) { + return chainId in EVM_CHAIN_IDS; +} +function normalizeChainId(chainName) { + return API_CHAIN_TO_KEYAPP[chainName] ?? chainName.toLowerCase(); +} function initBioProvider(targetOrigin = "*") { if (typeof window === "undefined") { throw new Error("[BioSDK] Cannot initialize: window is not defined"); @@ -159,16 +578,43 @@ function initBioProvider(targetOrigin = "*") { console.log("[BioSDK] Provider initialized"); return provider; } +function initAllProviders(targetOrigin = "*") { + const bio = initBioProvider(targetOrigin); + const ethereum = initEthereumProvider(targetOrigin); + const { tronLink, tronWeb } = initTronProvider(targetOrigin); + return { bio, ethereum, tronLink, tronWeb }; +} if (typeof window !== "undefined") { + const init = () => { + initBioProvider(); + initEthereumProvider(); + initTronProvider(); + }; if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", () => initBioProvider()); + document.addEventListener("DOMContentLoaded", init); } else { - initBioProvider(); + init(); } } +exports.API_CHAIN_TO_KEYAPP = API_CHAIN_TO_KEYAPP; exports.BioErrorCodes = BioErrorCodes; exports.BioProviderImpl = BioProviderImpl; +exports.CHAIN_DISPLAY_NAMES = CHAIN_DISPLAY_NAMES; +exports.EVM_CHAIN_IDS = EVM_CHAIN_IDS; +exports.EVM_CHAIN_ID_TO_KEYAPP = EVM_CHAIN_ID_TO_KEYAPP; +exports.EthereumProvider = EthereumProvider; exports.EventEmitter = EventEmitter; +exports.TronLinkProvider = TronLinkProvider; +exports.TronWebProvider = TronWebProvider; exports.createProviderError = createProviderError; +exports.getEvmChainId = getEvmChainId; +exports.getKeyAppChainId = getKeyAppChainId; +exports.initAllProviders = initAllProviders; exports.initBioProvider = initBioProvider; +exports.initEthereumProvider = initEthereumProvider; +exports.initTronProvider = initTronProvider; +exports.isEvmChain = isEvmChain; +exports.normalizeChainId = normalizeChainId; +exports.parseHexChainId = parseHexChainId; +exports.toHexChainId = toHexChainId; //# sourceMappingURL=index.cjs.map diff --git a/packages/bio-sdk/dist/index.cjs.map b/packages/bio-sdk/dist/index.cjs.map index e6b4df822..c79280637 100644 --- a/packages/bio-sdk/dist/index.cjs.map +++ b/packages/bio-sdk/dist/index.cjs.map @@ -1 +1 @@ -{"version":3,"file":"index.cjs","sources":["../src/types.ts","../src/events.ts","../src/provider.ts","../src/index.ts"],"sourcesContent":["/**\n * Bio SDK Types\n * EIP-1193 style provider interface for Bio ecosystem\n */\n\n/** Account information */\nexport interface BioAccount {\n address: string\n chain: string\n name?: string\n /** Public key (hex encoded) */\n publicKey: string\n}\n\n/** Transfer parameters */\nexport interface TransferParams {\n from: string\n to: string\n amount: string\n chain: string\n asset?: string\n}\n\n/** Unsigned transaction payload (chain-specific) */\nexport interface BioUnsignedTransaction {\n chainId: string\n data: unknown\n}\n\n/** Signed transaction payload (chain-specific) */\nexport interface BioSignedTransaction {\n chainId: string\n data: unknown\n signature: string\n}\n\n/** Provider request arguments */\nexport interface RequestArguments {\n method: string\n params?: unknown[]\n}\n\n/** Provider RPC error */\nexport interface ProviderRpcError extends Error {\n code: number\n data?: unknown\n}\n\n/** Event handler type */\nexport type EventHandler = (...args: T[]) => void\n\n/**\n * Bio Provider Interface (EIP-1193 style)\n */\nexport interface BioProvider {\n /** Make a request to the provider */\n request(args: RequestArguments): Promise\n\n /** Subscribe to an event */\n on(event: string, handler: EventHandler): void\n\n /** Unsubscribe from an event */\n off(event: string, handler: EventHandler): void\n\n /** Check if connected */\n isConnected(): boolean\n}\n\n/**\n * Bio method definitions\n */\nexport interface BioMethods {\n /** Request wallet accounts (shows connection UI) */\n bio_requestAccounts: () => Promise\n\n /** Get connected accounts (no UI) */\n bio_accounts: () => Promise\n\n /** Select an account (shows account picker UI) */\n bio_selectAccount: (opts?: { chain?: string }) => Promise\n\n /** Pick another wallet address (shows wallet picker UI) */\n bio_pickWallet: (opts?: { chain?: string; exclude?: string }) => Promise\n\n /** Sign a message, returns signature and public key (hex) */\n bio_signMessage: (params: { message: string; address: string }) => Promise<{ signature: string; publicKey: string }>\n\n /** Sign typed data, returns signature and public key (hex) */\n bio_signTypedData: (params: { data: object; address: string }) => Promise<{ signature: string; publicKey: string }>\n\n /** Create an unsigned transaction (no signature, no broadcast) */\n bio_createTransaction: (params: TransferParams) => Promise\n\n /** Sign an unsigned transaction (requires user confirmation) */\n bio_signTransaction: (params: { from: string; chain: string; unsignedTx: BioUnsignedTransaction }) => Promise\n\n /** Send a transaction */\n bio_sendTransaction: (params: TransferParams) => Promise<{ txHash: string }>\n\n /** Get current chain ID */\n bio_chainId: () => Promise\n\n /** Get balance */\n bio_getBalance: (params: { address: string; chain: string }) => Promise\n\n /** Close splash screen (indicates app is ready) */\n bio_closeSplashScreen: () => Promise\n}\n\n/**\n * Bio event definitions\n */\nexport interface BioEvents {\n /** Emitted when accounts change */\n accountsChanged: (accounts: BioAccount[]) => void\n\n /** Emitted when chain changes */\n chainChanged: (chainId: string) => void\n\n /** Emitted when connected */\n connect: (info: { chainId: string }) => void\n\n /** Emitted when disconnected */\n disconnect: (error: { code: number; message: string }) => void\n}\n\n/** Method names */\nexport type BioMethodName = keyof BioMethods\n\n/** Event names */\nexport type BioEventName = keyof BioEvents\n\n/** RPC error codes */\nexport const BioErrorCodes = {\n USER_REJECTED: 4001,\n UNAUTHORIZED: 4100,\n UNSUPPORTED_METHOD: 4200,\n DISCONNECTED: 4900,\n CHAIN_DISCONNECTED: 4901,\n INTERNAL_ERROR: -32603,\n INVALID_PARAMS: -32602,\n METHOD_NOT_FOUND: -32601,\n} as const\n\n/** Create a provider RPC error */\nexport function createProviderError(code: number, message: string, data?: unknown): ProviderRpcError {\n const error = new Error(message) as ProviderRpcError\n error.code = code\n error.data = data\n return error\n}\n","/**\n * Event emitter for Bio SDK\n */\n\nimport type { EventHandler } from './types'\n\nexport class EventEmitter {\n private handlers = new Map>()\n\n on(event: string, handler: EventHandler): void {\n let handlers = this.handlers.get(event)\n if (!handlers) {\n handlers = new Set()\n this.handlers.set(event, handlers)\n }\n handlers.add(handler)\n }\n\n off(event: string, handler: EventHandler): void {\n const handlers = this.handlers.get(event)\n if (handlers) {\n handlers.delete(handler)\n if (handlers.size === 0) {\n this.handlers.delete(event)\n }\n }\n }\n\n emit(event: string, ...args: unknown[]): void {\n const handlers = this.handlers.get(event)\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n handler(...args)\n } catch (error) {\n console.error(`[BioSDK] Error in event handler for \"${event}\":`, error)\n }\n })\n }\n }\n\n removeAllListeners(event?: string): void {\n if (event) {\n this.handlers.delete(event)\n } else {\n this.handlers.clear()\n }\n }\n}\n","/**\n * Bio Provider Implementation\n * Communicates with KeyApp host via postMessage\n */\n\nimport type { BioProvider, RequestArguments, EventHandler } from './types'\nimport { BioErrorCodes, createProviderError } from './types'\nimport { EventEmitter } from './events'\n\n/** Message sent to host */\ninterface RequestMessage {\n type: 'bio_request'\n id: string\n method: string\n params?: unknown[]\n}\n\n/** Response from host */\ninterface ResponseMessage {\n type: 'bio_response'\n id: string\n success: boolean\n result?: unknown\n error?: { code: number; message: string; data?: unknown }\n}\n\n/** Event from host */\ninterface EventMessage {\n type: 'bio_event'\n event: string\n args: unknown[]\n}\n\ntype HostMessage = ResponseMessage | EventMessage\n\nexport class BioProviderImpl implements BioProvider {\n private events = new EventEmitter()\n private pendingRequests = new Map void\n reject: (error: Error) => void\n }>()\n private requestIdCounter = 0\n private connected = false\n private readonly targetOrigin: string\n\n constructor(targetOrigin = '*') {\n this.targetOrigin = targetOrigin\n this.setupMessageListener()\n this.connect()\n }\n\n private setupMessageListener(): void {\n window.addEventListener('message', this.handleMessage.bind(this))\n }\n\n private handleMessage(event: MessageEvent): void {\n const data = event.data as HostMessage\n if (!data || typeof data !== 'object') return\n\n if (data.type === 'bio_response') {\n this.handleResponse(data)\n } else if (data.type === 'bio_event') {\n this.handleEvent(data)\n }\n }\n\n private handleResponse(message: ResponseMessage): void {\n const pending = this.pendingRequests.get(message.id)\n if (!pending) return\n\n this.pendingRequests.delete(message.id)\n\n if (message.success) {\n pending.resolve(message.result)\n } else {\n const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: 'Unknown error' }\n pending.reject(createProviderError(error.code, error.message, error.data))\n }\n }\n\n private handleEvent(message: EventMessage): void {\n this.events.emit(message.event, ...message.args)\n\n // Handle built-in events\n if (message.event === 'connect') {\n this.connected = true\n } else if (message.event === 'disconnect') {\n this.connected = false\n }\n }\n\n private connect(): void {\n // Send handshake to host\n this.postMessage({\n type: 'bio_request',\n id: this.generateId(),\n method: 'bio_connect',\n params: [],\n })\n }\n\n private generateId(): string {\n return `bio_${Date.now()}_${++this.requestIdCounter}`\n }\n\n private postMessage(message: RequestMessage): void {\n if (window.parent === window) {\n console.warn('[BioSDK] Not running in iframe, cannot communicate with host')\n return\n }\n window.parent.postMessage(message, this.targetOrigin)\n }\n\n async request(args: RequestArguments): Promise {\n const id = this.generateId()\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, {\n resolve: resolve as (value: unknown) => void,\n reject,\n })\n\n this.postMessage({\n type: 'bio_request',\n id,\n method: args.method,\n params: args.params,\n })\n\n // Timeout after 5 minutes (for user interactions)\n setTimeout(() => {\n if (this.pendingRequests.has(id)) {\n this.pendingRequests.delete(id)\n reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, 'Request timeout'))\n }\n }, 5 * 60 * 1000)\n })\n }\n\n on(event: string, handler: EventHandler): void {\n this.events.on(event, handler)\n }\n\n off(event: string, handler: EventHandler): void {\n this.events.off(event, handler)\n }\n\n isConnected(): boolean {\n return this.connected\n }\n}\n","/**\n * Bio SDK - Client SDK for Bio Ecosystem MiniApps\n *\n * Injects `window.bio` provider similar to `window.ethereum` in Web3 DApps.\n *\n * @example\n * ```typescript\n * import '@biochain/bio-sdk'\n *\n * // Now window.bio is available\n * const accounts = await window.bio.request({ method: 'bio_requestAccounts' })\n * ```\n */\n\nimport { BioProviderImpl } from './provider'\nimport type { BioProvider } from './types'\n\n// Re-export types\nexport * from './types'\nexport { EventEmitter } from './events'\nexport { BioProviderImpl } from './provider'\n\n// Extend Window interface\ndeclare global {\n interface Window {\n bio?: BioProvider\n }\n}\n\n/**\n * Initialize and inject the Bio provider into window.bio\n */\nexport function initBioProvider(targetOrigin = '*'): BioProvider {\n if (typeof window === 'undefined') {\n throw new Error('[BioSDK] Cannot initialize: window is not defined')\n }\n\n if (window.bio) {\n console.warn('[BioSDK] Provider already exists, returning existing instance')\n return window.bio\n }\n\n const provider = new BioProviderImpl(targetOrigin)\n window.bio = provider\n\n console.log('[BioSDK] Provider initialized')\n return provider\n}\n\n// Auto-initialize if running in browser\nif (typeof window !== 'undefined') {\n // Use a slight delay to ensure DOM is ready\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => initBioProvider())\n } else {\n initBioProvider()\n }\n}\n"],"names":[],"mappings":";;AAqIO,MAAM,gBAAgB;AAAA,EAC3B,eAAe;AAAA,EACf,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,kBAAkB;AACpB;AAGO,SAAS,oBAAoB,MAAc,SAAiB,MAAkC;AACnG,QAAM,QAAQ,IAAI,MAAM,OAAO;AAC/B,QAAM,OAAO;AACb,QAAM,OAAO;AACb,SAAO;AACT;AChJO,MAAM,aAAa;AAAA,EAChB,+BAAe,IAAA;AAAA,EAEvB,GAAG,OAAe,SAA6B;AAC7C,QAAI,WAAW,KAAK,SAAS,IAAI,KAAK;AACtC,QAAI,CAAC,UAAU;AACb,qCAAe,IAAA;AACf,WAAK,SAAS,IAAI,OAAO,QAAQ;AAAA,IACnC;AACA,aAAS,IAAI,OAAO;AAAA,EACtB;AAAA,EAEA,IAAI,OAAe,SAA6B;AAC9C,UAAM,WAAW,KAAK,SAAS,IAAI,KAAK;AACxC,QAAI,UAAU;AACZ,eAAS,OAAO,OAAO;AACvB,UAAI,SAAS,SAAS,GAAG;AACvB,aAAK,SAAS,OAAO,KAAK;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,KAAK,UAAkB,MAAuB;AAC5C,UAAM,WAAW,KAAK,SAAS,IAAI,KAAK;AACxC,QAAI,UAAU;AACZ,eAAS,QAAQ,CAAC,YAAY;AAC5B,YAAI;AACF,kBAAQ,GAAG,IAAI;AAAA,QACjB,SAAS,OAAO;AACd,kBAAQ,MAAM,wCAAwC,KAAK,MAAM,KAAK;AAAA,QACxE;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,mBAAmB,OAAsB;AACvC,QAAI,OAAO;AACT,WAAK,SAAS,OAAO,KAAK;AAAA,IAC5B,OAAO;AACL,WAAK,SAAS,MAAA;AAAA,IAChB;AAAA,EACF;AACF;ACbO,MAAM,gBAAuC;AAAA,EAC1C,SAAS,IAAI,aAAA;AAAA,EACb,sCAAsB,IAAA;AAAA,EAItB,mBAAmB;AAAA,EACnB,YAAY;AAAA,EACH;AAAA,EAEjB,YAAY,eAAe,KAAK;AAC9B,SAAK,eAAe;AACpB,SAAK,qBAAA;AACL,SAAK,QAAA;AAAA,EACP;AAAA,EAEQ,uBAA6B;AACnC,WAAO,iBAAiB,WAAW,KAAK,cAAc,KAAK,IAAI,CAAC;AAAA,EAClE;AAAA,EAEQ,cAAc,OAA2B;AAC/C,UAAM,OAAO,MAAM;AACnB,QAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AAEvC,QAAI,KAAK,SAAS,gBAAgB;AAChC,WAAK,eAAe,IAAI;AAAA,IAC1B,WAAW,KAAK,SAAS,aAAa;AACpC,WAAK,YAAY,IAAI;AAAA,IACvB;AAAA,EACF;AAAA,EAEQ,eAAe,SAAgC;AACrD,UAAM,UAAU,KAAK,gBAAgB,IAAI,QAAQ,EAAE;AACnD,QAAI,CAAC,QAAS;AAEd,SAAK,gBAAgB,OAAO,QAAQ,EAAE;AAEtC,QAAI,QAAQ,SAAS;AACnB,cAAQ,QAAQ,QAAQ,MAAM;AAAA,IAChC,OAAO;AACL,YAAM,QAAQ,QAAQ,SAAS,EAAE,MAAM,cAAc,gBAAgB,SAAS,gBAAA;AAC9E,cAAQ,OAAO,oBAAoB,MAAM,MAAM,MAAM,SAAS,MAAM,IAAI,CAAC;AAAA,IAC3E;AAAA,EACF;AAAA,EAEQ,YAAY,SAA6B;AAC/C,SAAK,OAAO,KAAK,QAAQ,OAAO,GAAG,QAAQ,IAAI;AAG/C,QAAI,QAAQ,UAAU,WAAW;AAC/B,WAAK,YAAY;AAAA,IACnB,WAAW,QAAQ,UAAU,cAAc;AACzC,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AAAA,EAEQ,UAAgB;AAEtB,SAAK,YAAY;AAAA,MACf,MAAM;AAAA,MACN,IAAI,KAAK,WAAA;AAAA,MACT,QAAQ;AAAA,MACR,QAAQ,CAAA;AAAA,IAAC,CACV;AAAA,EACH;AAAA,EAEQ,aAAqB;AAC3B,WAAO,OAAO,KAAK,IAAA,CAAK,IAAI,EAAE,KAAK,gBAAgB;AAAA,EACrD;AAAA,EAEQ,YAAY,SAA+B;AACjD,QAAI,OAAO,WAAW,QAAQ;AAC5B,cAAQ,KAAK,8DAA8D;AAC3E;AAAA,IACF;AACA,WAAO,OAAO,YAAY,SAAS,KAAK,YAAY;AAAA,EACtD;AAAA,EAEA,MAAM,QAAqB,MAAoC;AAC7D,UAAM,KAAK,KAAK,WAAA;AAEhB,WAAO,IAAI,QAAW,CAAC,SAAS,WAAW;AACzC,WAAK,gBAAgB,IAAI,IAAI;AAAA,QAC3B;AAAA,QACA;AAAA,MAAA,CACD;AAED,WAAK,YAAY;AAAA,QACf,MAAM;AAAA,QACN;AAAA,QACA,QAAQ,KAAK;AAAA,QACb,QAAQ,KAAK;AAAA,MAAA,CACd;AAGD,iBAAW,MAAM;AACf,YAAI,KAAK,gBAAgB,IAAI,EAAE,GAAG;AAChC,eAAK,gBAAgB,OAAO,EAAE;AAC9B,iBAAO,oBAAoB,cAAc,gBAAgB,iBAAiB,CAAC;AAAA,QAC7E;AAAA,MACF,GAAG,IAAI,KAAK,GAAI;AAAA,IAClB,CAAC;AAAA,EACH;AAAA,EAEA,GAAG,OAAe,SAA6B;AAC7C,SAAK,OAAO,GAAG,OAAO,OAAO;AAAA,EAC/B;AAAA,EAEA,IAAI,OAAe,SAA6B;AAC9C,SAAK,OAAO,IAAI,OAAO,OAAO;AAAA,EAChC;AAAA,EAEA,cAAuB;AACrB,WAAO,KAAK;AAAA,EACd;AACF;ACtHO,SAAS,gBAAgB,eAAe,KAAkB;AAC/D,MAAI,OAAO,WAAW,aAAa;AACjC,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACrE;AAEA,MAAI,OAAO,KAAK;AACd,YAAQ,KAAK,+DAA+D;AAC5E,WAAO,OAAO;AAAA,EAChB;AAEA,QAAM,WAAW,IAAI,gBAAgB,YAAY;AACjD,SAAO,MAAM;AAEb,UAAQ,IAAI,+BAA+B;AAC3C,SAAO;AACT;AAGA,IAAI,OAAO,WAAW,aAAa;AAEjC,MAAI,SAAS,eAAe,WAAW;AACrC,aAAS,iBAAiB,oBAAoB,MAAM,gBAAA,CAAiB;AAAA,EACvE,OAAO;AACL,oBAAA;AAAA,EACF;AACF;;;;;;"} \ No newline at end of file +{"version":3,"file":"index.cjs","sources":["../src/types.ts","../src/events.ts","../src/provider.ts","../src/ethereum-provider.ts","../src/tron-provider.ts","../src/chain-id.ts","../src/index.ts"],"sourcesContent":["/**\n * Bio SDK Types\n * EIP-1193 style provider interface for Bio ecosystem\n */\n\n/** Account information */\nexport interface BioAccount {\n address: string\n chain: string\n name?: string\n /** Public key (hex encoded) */\n publicKey: string\n}\n\n/** Transfer parameters */\nexport interface TransferParams {\n from: string\n to: string\n amount: string\n chain: string\n asset?: string\n}\n\n/** Unsigned transaction payload (chain-specific) */\nexport interface BioUnsignedTransaction {\n chainId: string\n data: unknown\n}\n\n/** Signed transaction payload (chain-specific) */\nexport interface BioSignedTransaction {\n chainId: string\n data: unknown\n signature: string\n}\n\n/** Provider request arguments */\nexport interface RequestArguments {\n method: string\n params?: unknown[]\n}\n\n/** Provider RPC error */\nexport interface ProviderRpcError extends Error {\n code: number\n data?: unknown\n}\n\n/** Event handler type */\nexport type EventHandler = (...args: T[]) => void\n\n/**\n * Bio Provider Interface (EIP-1193 style)\n */\nexport interface BioProvider {\n /** Make a request to the provider */\n request(args: RequestArguments): Promise\n\n /** Subscribe to an event */\n on(event: string, handler: EventHandler): void\n\n /** Unsubscribe from an event */\n off(event: string, handler: EventHandler): void\n\n /** Check if connected */\n isConnected(): boolean\n}\n\n/**\n * Bio method definitions\n */\nexport interface BioMethods {\n /** Request wallet accounts (shows connection UI) */\n bio_requestAccounts: () => Promise\n\n /** Get connected accounts (no UI) */\n bio_accounts: () => Promise\n\n /** Select an account (shows account picker UI) */\n bio_selectAccount: (opts?: { chain?: string }) => Promise\n\n /** Pick another wallet address (shows wallet picker UI) */\n bio_pickWallet: (opts?: { chain?: string; exclude?: string }) => Promise\n\n /** Sign a message, returns signature and public key (hex) */\n bio_signMessage: (params: { message: string; address: string }) => Promise<{ signature: string; publicKey: string }>\n\n /** Sign typed data, returns signature and public key (hex) */\n bio_signTypedData: (params: { data: object; address: string }) => Promise<{ signature: string; publicKey: string }>\n\n /** Create an unsigned transaction (no signature, no broadcast) */\n bio_createTransaction: (params: TransferParams) => Promise\n\n /** Sign an unsigned transaction (requires user confirmation) */\n bio_signTransaction: (params: { from: string; chain: string; unsignedTx: BioUnsignedTransaction }) => Promise\n\n /** Send a transaction */\n bio_sendTransaction: (params: TransferParams) => Promise<{ txHash: string }>\n\n /** Get current chain ID */\n bio_chainId: () => Promise\n\n /** Get balance */\n bio_getBalance: (params: { address: string; chain: string }) => Promise\n\n /** Close splash screen (indicates app is ready) */\n bio_closeSplashScreen: () => Promise\n}\n\n/**\n * Bio event definitions\n */\nexport interface BioEvents {\n /** Emitted when accounts change */\n accountsChanged: (accounts: BioAccount[]) => void\n\n /** Emitted when chain changes */\n chainChanged: (chainId: string) => void\n\n /** Emitted when connected */\n connect: (info: { chainId: string }) => void\n\n /** Emitted when disconnected */\n disconnect: (error: { code: number; message: string }) => void\n}\n\n/** Method names */\nexport type BioMethodName = keyof BioMethods\n\n/** Event names */\nexport type BioEventName = keyof BioEvents\n\n/** RPC error codes */\nexport const BioErrorCodes = {\n USER_REJECTED: 4001,\n UNAUTHORIZED: 4100,\n UNSUPPORTED_METHOD: 4200,\n DISCONNECTED: 4900,\n CHAIN_DISCONNECTED: 4901,\n INTERNAL_ERROR: -32603,\n INVALID_PARAMS: -32602,\n METHOD_NOT_FOUND: -32601,\n} as const\n\n/** Create a provider RPC error */\nexport function createProviderError(code: number, message: string, data?: unknown): ProviderRpcError {\n const error = new Error(message) as ProviderRpcError\n error.code = code\n error.data = data\n return error\n}\n","/**\n * Event emitter for Bio SDK\n */\n\nimport type { EventHandler } from './types'\n\nexport class EventEmitter {\n private handlers = new Map>()\n\n on(event: string, handler: EventHandler): void {\n let handlers = this.handlers.get(event)\n if (!handlers) {\n handlers = new Set()\n this.handlers.set(event, handlers)\n }\n handlers.add(handler)\n }\n\n off(event: string, handler: EventHandler): void {\n const handlers = this.handlers.get(event)\n if (handlers) {\n handlers.delete(handler)\n if (handlers.size === 0) {\n this.handlers.delete(event)\n }\n }\n }\n\n emit(event: string, ...args: unknown[]): void {\n const handlers = this.handlers.get(event)\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n handler(...args)\n } catch (error) {\n console.error(`[BioSDK] Error in event handler for \"${event}\":`, error)\n }\n })\n }\n }\n\n removeAllListeners(event?: string): void {\n if (event) {\n this.handlers.delete(event)\n } else {\n this.handlers.clear()\n }\n }\n}\n","/**\n * Bio Provider Implementation\n * Communicates with KeyApp host via postMessage\n */\n\nimport type { BioProvider, RequestArguments, EventHandler } from './types'\nimport { BioErrorCodes, createProviderError } from './types'\nimport { EventEmitter } from './events'\n\n/** Message sent to host */\ninterface RequestMessage {\n type: 'bio_request'\n id: string\n method: string\n params?: unknown[]\n}\n\n/** Response from host */\ninterface ResponseMessage {\n type: 'bio_response'\n id: string\n success: boolean\n result?: unknown\n error?: { code: number; message: string; data?: unknown }\n}\n\n/** Event from host */\ninterface EventMessage {\n type: 'bio_event'\n event: string\n args: unknown[]\n}\n\ntype HostMessage = ResponseMessage | EventMessage\n\nexport class BioProviderImpl implements BioProvider {\n private events = new EventEmitter()\n private pendingRequests = new Map void\n reject: (error: Error) => void\n }>()\n private requestIdCounter = 0\n private connected = false\n private readonly targetOrigin: string\n\n constructor(targetOrigin = '*') {\n this.targetOrigin = targetOrigin\n this.setupMessageListener()\n this.connect()\n }\n\n private setupMessageListener(): void {\n window.addEventListener('message', this.handleMessage.bind(this))\n }\n\n private handleMessage(event: MessageEvent): void {\n const data = event.data as HostMessage\n if (!data || typeof data !== 'object') return\n\n if (data.type === 'bio_response') {\n this.handleResponse(data)\n } else if (data.type === 'bio_event') {\n this.handleEvent(data)\n }\n }\n\n private handleResponse(message: ResponseMessage): void {\n const pending = this.pendingRequests.get(message.id)\n if (!pending) return\n\n this.pendingRequests.delete(message.id)\n\n if (message.success) {\n pending.resolve(message.result)\n } else {\n const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: 'Unknown error' }\n pending.reject(createProviderError(error.code, error.message, error.data))\n }\n }\n\n private handleEvent(message: EventMessage): void {\n this.events.emit(message.event, ...message.args)\n\n // Handle built-in events\n if (message.event === 'connect') {\n this.connected = true\n } else if (message.event === 'disconnect') {\n this.connected = false\n }\n }\n\n private connect(): void {\n // Send handshake to host\n this.postMessage({\n type: 'bio_request',\n id: this.generateId(),\n method: 'bio_connect',\n params: [],\n })\n }\n\n private generateId(): string {\n return `bio_${Date.now()}_${++this.requestIdCounter}`\n }\n\n private postMessage(message: RequestMessage): void {\n if (window.parent === window) {\n console.warn('[BioSDK] Not running in iframe, cannot communicate with host')\n return\n }\n window.parent.postMessage(message, this.targetOrigin)\n }\n\n async request(args: RequestArguments): Promise {\n const id = this.generateId()\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, {\n resolve: resolve as (value: unknown) => void,\n reject,\n })\n\n this.postMessage({\n type: 'bio_request',\n id,\n method: args.method,\n params: args.params,\n })\n\n // Timeout after 5 minutes (for user interactions)\n setTimeout(() => {\n if (this.pendingRequests.has(id)) {\n this.pendingRequests.delete(id)\n reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, 'Request timeout'))\n }\n }, 5 * 60 * 1000)\n })\n }\n\n on(event: string, handler: EventHandler): void {\n this.events.on(event, handler)\n }\n\n off(event: string, handler: EventHandler): void {\n this.events.off(event, handler)\n }\n\n isConnected(): boolean {\n return this.connected\n }\n}\n","/**\n * Ethereum Provider (EIP-1193 Compatible)\n *\n * Provides window.ethereum for EVM-compatible dApps.\n * Communicates with KeyApp host via postMessage.\n */\n\nimport { EventEmitter } from './events'\nimport { BioErrorCodes, createProviderError, type ProviderRpcError } from './types'\nimport { toHexChainId, parseHexChainId, getKeyAppChainId, EVM_CHAIN_IDS } from './chain-id'\n\n/** EIP-1193 Request Arguments */\nexport interface EthRequestArguments {\n method: string\n params?: unknown[] | Record\n}\n\n/** EIP-1193 Provider Connect Info */\nexport interface ProviderConnectInfo {\n chainId: string\n}\n\n/** EIP-1193 Provider Message */\nexport interface ProviderMessage {\n type: string\n data: unknown\n}\n\n/** Transaction request (eth_sendTransaction) */\nexport interface TransactionRequest {\n from: string\n to?: string\n value?: string\n data?: string\n gas?: string\n gasPrice?: string\n maxFeePerGas?: string\n maxPriorityFeePerGas?: string\n nonce?: string\n}\n\n/** Message sent to host */\ninterface RequestMessage {\n type: 'eth_request'\n id: string\n method: string\n params?: unknown[]\n}\n\n/** Response from host */\ninterface ResponseMessage {\n type: 'eth_response'\n id: string\n success: boolean\n result?: unknown\n error?: { code: number; message: string; data?: unknown }\n}\n\n/** Event from host */\ninterface EventMessage {\n type: 'eth_event'\n event: string\n args: unknown[]\n}\n\ntype HostMessage = ResponseMessage | EventMessage\n\n/**\n * EIP-1193 Ethereum Provider Implementation\n */\nexport class EthereumProvider {\n private events = new EventEmitter()\n private pendingRequests = new Map void\n reject: (error: Error) => void\n }>()\n private requestIdCounter = 0\n private connected = false\n private currentChainId: string | null = null\n private accounts: string[] = []\n private readonly targetOrigin: string\n\n // EIP-1193 required properties\n readonly isMetaMask = false\n readonly isKeyApp = true\n\n constructor(targetOrigin = '*') {\n this.targetOrigin = targetOrigin\n this.setupMessageListener()\n }\n\n private setupMessageListener(): void {\n window.addEventListener('message', this.handleMessage.bind(this))\n }\n\n private handleMessage(event: MessageEvent): void {\n const data = event.data as HostMessage\n if (!data || typeof data !== 'object') return\n\n if (data.type === 'eth_response') {\n this.handleResponse(data)\n } else if (data.type === 'eth_event') {\n this.handleEvent(data)\n }\n }\n\n private handleResponse(message: ResponseMessage): void {\n const pending = this.pendingRequests.get(message.id)\n if (!pending) return\n\n this.pendingRequests.delete(message.id)\n\n if (message.success) {\n pending.resolve(message.result)\n } else {\n const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: 'Unknown error' }\n pending.reject(createProviderError(error.code, error.message, error.data))\n }\n }\n\n private handleEvent(message: EventMessage): void {\n this.events.emit(message.event, ...message.args)\n\n // Handle built-in events\n if (message.event === 'connect') {\n this.connected = true\n const info = message.args[0] as ProviderConnectInfo\n this.currentChainId = info?.chainId ?? null\n } else if (message.event === 'disconnect') {\n this.connected = false\n this.accounts = []\n } else if (message.event === 'chainChanged') {\n this.currentChainId = message.args[0] as string\n } else if (message.event === 'accountsChanged') {\n this.accounts = message.args[0] as string[]\n }\n }\n\n private generateId(): string {\n return `eth_${Date.now()}_${++this.requestIdCounter}`\n }\n\n private postMessage(message: RequestMessage): void {\n if (window.parent === window) {\n console.warn('[EthereumProvider] Not running in iframe, cannot communicate with host')\n return\n }\n window.parent.postMessage(message, this.targetOrigin)\n }\n\n /**\n * EIP-1193 request method\n */\n async request(args: EthRequestArguments): Promise {\n const { method, params } = args\n const paramsArray = Array.isArray(params) ? params : params ? [params] : []\n\n const id = this.generateId()\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, {\n resolve: resolve as (value: unknown) => void,\n reject,\n })\n\n this.postMessage({\n type: 'eth_request',\n id,\n method,\n params: paramsArray,\n })\n\n // Timeout after 5 minutes (for user interactions)\n setTimeout(() => {\n if (this.pendingRequests.has(id)) {\n this.pendingRequests.delete(id)\n reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, 'Request timeout'))\n }\n }, 5 * 60 * 1000)\n })\n }\n\n /**\n * Subscribe to an event\n */\n on(event: string, handler: (...args: unknown[]) => void): this {\n this.events.on(event, handler)\n return this\n }\n\n /**\n * Unsubscribe from an event\n */\n off(event: string, handler: (...args: unknown[]) => void): this {\n this.events.off(event, handler)\n return this\n }\n\n /**\n * Alias for off (Node.js EventEmitter compatibility)\n */\n removeListener(event: string, handler: (...args: unknown[]) => void): this {\n return this.off(event, handler)\n }\n\n /**\n * Add listener that fires only once\n */\n once(event: string, handler: (...args: unknown[]) => void): this {\n const wrapper = (...args: unknown[]) => {\n this.off(event, wrapper)\n handler(...args)\n }\n this.on(event, wrapper)\n return this\n }\n\n /**\n * EIP-1193 isConnected method\n */\n isConnected(): boolean {\n return this.connected\n }\n\n /**\n * Get current chain ID (cached)\n */\n get chainId(): string | null {\n return this.currentChainId\n }\n\n /**\n * Get selected address (first account)\n */\n get selectedAddress(): string | null {\n return this.accounts[0] ?? null\n }\n\n // ============================================\n // Legacy methods (for backwards compatibility)\n // ============================================\n\n /**\n * @deprecated Use request({ method: 'eth_requestAccounts' })\n */\n async enable(): Promise {\n return this.request({ method: 'eth_requestAccounts' })\n }\n\n /**\n * @deprecated Use request()\n */\n send(method: string, params?: unknown[]): Promise {\n return this.request({ method, params })\n }\n\n /**\n * @deprecated Use request()\n */\n sendAsync(\n payload: { method: string; params?: unknown[]; id?: number },\n callback: (error: Error | null, result?: { result: unknown }) => void\n ): void {\n this.request({ method: payload.method, params: payload.params })\n .then((result) => callback(null, { result }))\n .catch((error) => callback(error))\n }\n}\n\n// Extend Window interface\ndeclare global {\n interface Window {\n ethereum?: EthereumProvider\n }\n}\n\n/**\n * Initialize and inject the Ethereum provider into window.ethereum\n */\nexport function initEthereumProvider(targetOrigin = '*'): EthereumProvider {\n if (typeof window === 'undefined') {\n throw new Error('[EthereumProvider] Cannot initialize: window is not defined')\n }\n\n if (window.ethereum) {\n console.warn('[EthereumProvider] Provider already exists, returning existing instance')\n return window.ethereum\n }\n\n const provider = new EthereumProvider(targetOrigin)\n window.ethereum = provider\n\n console.log('[EthereumProvider] Provider initialized')\n return provider\n}\n","/**\n * Tron Provider (TronLink Compatible)\n *\n * Provides window.tronWeb and window.tronLink for Tron dApps.\n * Communicates with KeyApp host via postMessage.\n */\n\nimport { EventEmitter } from './events'\nimport { BioErrorCodes, createProviderError } from './types'\n\n/** Tron address format */\nexport interface TronAddress {\n base58: string\n hex: string\n}\n\n/** TronLink request arguments (EIP-1193 style) */\nexport interface TronRequestArguments {\n method: string\n params?: unknown\n}\n\n/** Message sent to host */\ninterface RequestMessage {\n type: 'tron_request'\n id: string\n method: string\n params?: unknown[]\n}\n\n/** Response from host */\ninterface ResponseMessage {\n type: 'tron_response'\n id: string\n success: boolean\n result?: unknown\n error?: { code: number; message: string; data?: unknown }\n}\n\n/** Event from host */\ninterface EventMessage {\n type: 'tron_event'\n event: string\n args: unknown[]\n}\n\ntype HostMessage = ResponseMessage | EventMessage\n\n/**\n * TronLink-compatible Provider\n */\nexport class TronLinkProvider {\n private events = new EventEmitter()\n private pendingRequests = new Map void\n reject: (error: Error) => void\n }>()\n private requestIdCounter = 0\n private readonly targetOrigin: string\n\n constructor(targetOrigin = '*') {\n this.targetOrigin = targetOrigin\n this.setupMessageListener()\n }\n\n private setupMessageListener(): void {\n window.addEventListener('message', this.handleMessage.bind(this))\n }\n\n private handleMessage(event: MessageEvent): void {\n const data = event.data as HostMessage\n if (!data || typeof data !== 'object') return\n\n if (data.type === 'tron_response') {\n this.handleResponse(data)\n } else if (data.type === 'tron_event') {\n this.handleEvent(data)\n }\n }\n\n private handleResponse(message: ResponseMessage): void {\n const pending = this.pendingRequests.get(message.id)\n if (!pending) return\n\n this.pendingRequests.delete(message.id)\n\n if (message.success) {\n pending.resolve(message.result)\n } else {\n const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: 'Unknown error' }\n pending.reject(createProviderError(error.code, error.message, error.data))\n }\n }\n\n private handleEvent(message: EventMessage): void {\n this.events.emit(message.event, ...message.args)\n }\n\n private generateId(): string {\n return `tron_${Date.now()}_${++this.requestIdCounter}`\n }\n\n private postMessage(message: RequestMessage): void {\n if (window.parent === window) {\n console.warn('[TronLinkProvider] Not running in iframe, cannot communicate with host')\n return\n }\n window.parent.postMessage(message, this.targetOrigin)\n }\n\n /**\n * TronLink request method (EIP-1193 style)\n */\n async request(args: TronRequestArguments): Promise {\n const { method, params } = args\n const paramsArray = Array.isArray(params) ? params : params !== undefined ? [params] : []\n\n const id = this.generateId()\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, {\n resolve: resolve as (value: unknown) => void,\n reject,\n })\n\n this.postMessage({\n type: 'tron_request',\n id,\n method,\n params: paramsArray,\n })\n\n // Timeout after 5 minutes\n setTimeout(() => {\n if (this.pendingRequests.has(id)) {\n this.pendingRequests.delete(id)\n reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, 'Request timeout'))\n }\n }, 5 * 60 * 1000)\n })\n }\n\n on(event: string, handler: (...args: unknown[]) => void): this {\n this.events.on(event, handler)\n return this\n }\n\n off(event: string, handler: (...args: unknown[]) => void): this {\n this.events.off(event, handler)\n return this\n }\n}\n\n/**\n * TronWeb-compatible API\n * Provides the subset of TronWeb API that KeyApp supports\n */\nexport class TronWebProvider {\n private tronLink: TronLinkProvider\n private _ready = false\n private _defaultAddress: TronAddress = { base58: '', hex: '' }\n\n /** TRX operations */\n readonly trx: TronWebTrx\n\n constructor(tronLink: TronLinkProvider) {\n this.tronLink = tronLink\n this.trx = new TronWebTrx(tronLink)\n\n // Listen for account changes\n tronLink.on('accountsChanged', (accounts: unknown) => {\n if (Array.isArray(accounts) && accounts.length > 0) {\n const addr = accounts[0] as TronAddress\n this._defaultAddress = addr\n this._ready = true\n } else {\n this._defaultAddress = { base58: '', hex: '' }\n this._ready = false\n }\n })\n }\n\n /** Whether TronWeb is ready (connected) */\n get ready(): boolean {\n return this._ready\n }\n\n /** Current default address */\n get defaultAddress(): TronAddress {\n return this._defaultAddress\n }\n\n /**\n * Set default address (called by host after connection)\n */\n setAddress(address: TronAddress): void {\n this._defaultAddress = address\n this._ready = true\n }\n\n /**\n * Check if an address is valid\n */\n isAddress(address: string): boolean {\n // Basic validation: base58 starts with T, hex starts with 41\n if (address.startsWith('T')) {\n return address.length === 34\n }\n if (address.startsWith('41')) {\n return address.length === 42\n }\n return false\n }\n\n /**\n * Convert address to hex format\n */\n address = {\n toHex: (base58: string): string => {\n // This is a stub - actual conversion requires TronWeb library\n // KeyApp will handle the conversion on the host side\n return base58\n },\n fromHex: (hex: string): string => {\n return hex\n },\n }\n}\n\n/**\n * TronWeb.trx operations\n */\nclass TronWebTrx {\n private tronLink: TronLinkProvider\n\n constructor(tronLink: TronLinkProvider) {\n this.tronLink = tronLink\n }\n\n /**\n * Sign a transaction\n */\n async sign(transaction: unknown): Promise {\n return this.tronLink.request({\n method: 'tron_signTransaction',\n params: transaction,\n })\n }\n\n /**\n * Send raw transaction (broadcast)\n */\n async sendRawTransaction(signedTransaction: unknown): Promise {\n return this.tronLink.request({\n method: 'tron_sendRawTransaction',\n params: signedTransaction,\n })\n }\n\n /**\n * Get account balance\n */\n async getBalance(address: string): Promise {\n return this.tronLink.request({\n method: 'tron_getBalance',\n params: address,\n })\n }\n\n /**\n * Get account info\n */\n async getAccount(address: string): Promise {\n return this.tronLink.request({\n method: 'tron_getAccount',\n params: address,\n })\n }\n}\n\n// Extend Window interface\ndeclare global {\n interface Window {\n tronLink?: TronLinkProvider\n tronWeb?: TronWebProvider\n }\n}\n\n/**\n * Initialize and inject the Tron providers\n */\nexport function initTronProvider(targetOrigin = '*'): { tronLink: TronLinkProvider; tronWeb: TronWebProvider } {\n if (typeof window === 'undefined') {\n throw new Error('[TronProvider] Cannot initialize: window is not defined')\n }\n\n if (window.tronLink && window.tronWeb) {\n console.warn('[TronProvider] Providers already exist, returning existing instances')\n return { tronLink: window.tronLink, tronWeb: window.tronWeb }\n }\n\n const tronLink = new TronLinkProvider(targetOrigin)\n const tronWeb = new TronWebProvider(tronLink)\n\n window.tronLink = tronLink\n window.tronWeb = tronWeb\n\n console.log('[TronProvider] Providers initialized')\n return { tronLink, tronWeb }\n}\n","/**\n * Chain ID utilities\n * Maps between KeyApp internal chain IDs and standard chain IDs\n */\n\n/** EVM Chain ID mapping (decimal) */\nexport const EVM_CHAIN_IDS: Record = {\n ethereum: 1,\n binance: 56,\n // Future chains\n // polygon: 137,\n // arbitrum: 42161,\n // optimism: 10,\n} as const\n\n/** Reverse mapping: EVM chainId -> KeyApp chain ID */\nexport const EVM_CHAIN_ID_TO_KEYAPP: Record = Object.fromEntries(\n Object.entries(EVM_CHAIN_IDS).map(([key, value]) => [value, key])\n)\n\n/** API chain name to KeyApp chain ID mapping */\nexport const API_CHAIN_TO_KEYAPP: Record = {\n ETH: 'ethereum',\n BSC: 'binance',\n TRON: 'tron',\n // Lowercase variants\n eth: 'ethereum',\n bsc: 'binance',\n tron: 'tron',\n} as const\n\n/** KeyApp chain ID to display name */\nexport const CHAIN_DISPLAY_NAMES: Record = {\n ethereum: 'Ethereum',\n binance: 'BNB Smart Chain',\n tron: 'Tron',\n bfmeta: 'BFMeta',\n bfchain: 'BFChain',\n} as const\n\n/**\n * Convert decimal chain ID to hex string (EIP-155 format)\n * @example toHexChainId(56) => '0x38'\n */\nexport function toHexChainId(chainId: number): string {\n return `0x${chainId.toString(16)}`\n}\n\n/**\n * Parse hex chain ID to decimal\n * @example parseHexChainId('0x38') => 56\n */\nexport function parseHexChainId(hexChainId: string): number {\n if (!hexChainId.startsWith('0x')) {\n throw new Error(`Invalid hex chain ID: ${hexChainId}`)\n }\n return parseInt(hexChainId, 16)\n}\n\n/**\n * Get KeyApp chain ID from EVM hex chain ID\n * @example getKeyAppChainId('0x38') => 'binance'\n */\nexport function getKeyAppChainId(hexChainId: string): string | null {\n const decimal = parseHexChainId(hexChainId)\n return EVM_CHAIN_ID_TO_KEYAPP[decimal] ?? null\n}\n\n/**\n * Get EVM hex chain ID from KeyApp chain ID\n * @example getEvmChainId('binance') => '0x38'\n */\nexport function getEvmChainId(keyAppChainId: string): string | null {\n const decimal = EVM_CHAIN_IDS[keyAppChainId]\n return decimal ? toHexChainId(decimal) : null\n}\n\n/**\n * Check if a chain is EVM compatible\n */\nexport function isEvmChain(chainId: string): boolean {\n return chainId in EVM_CHAIN_IDS\n}\n\n/**\n * Normalize API chain name to KeyApp chain ID\n * @example normalizeChainId('BSC') => 'binance'\n */\nexport function normalizeChainId(chainName: string): string {\n return API_CHAIN_TO_KEYAPP[chainName] ?? chainName.toLowerCase()\n}\n","/**\n * Bio SDK - Client SDK for Bio Ecosystem MiniApps\n *\n * Injects providers for multi-chain dApp support:\n * - `window.bio` - BioChain + KeyApp wallet features\n * - `window.ethereum` - EVM-compatible chains (ETH, BSC)\n * - `window.tronWeb` / `window.tronLink` - Tron chain\n *\n * @example\n * ```typescript\n * import '@biochain/bio-sdk'\n *\n * // BioChain operations\n * const accounts = await window.bio.request({ method: 'bio_requestAccounts' })\n *\n * // EVM operations (ETH, BSC)\n * const ethAccounts = await window.ethereum.request({ method: 'eth_requestAccounts' })\n *\n * // Tron operations\n * const tronAccounts = await window.tronLink.request({ method: 'tron_requestAccounts' })\n * ```\n */\n\nimport { BioProviderImpl } from './provider'\nimport { EthereumProvider, initEthereumProvider } from './ethereum-provider'\nimport { TronLinkProvider, TronWebProvider, initTronProvider } from './tron-provider'\nimport type { BioProvider } from './types'\n\n// Re-export types\nexport * from './types'\nexport * from './chain-id'\nexport { EventEmitter } from './events'\nexport { BioProviderImpl } from './provider'\nexport { EthereumProvider, initEthereumProvider } from './ethereum-provider'\nexport { TronLinkProvider, TronWebProvider, initTronProvider } from './tron-provider'\n\n// Extend Window interface (bio is declared in types.ts already for ethereum/tron)\ndeclare global {\n interface Window {\n bio?: BioProvider\n }\n}\n\n/**\n * Initialize and inject the Bio provider into window.bio\n */\nexport function initBioProvider(targetOrigin = '*'): BioProvider {\n if (typeof window === 'undefined') {\n throw new Error('[BioSDK] Cannot initialize: window is not defined')\n }\n\n if (window.bio) {\n console.warn('[BioSDK] Provider already exists, returning existing instance')\n return window.bio\n }\n\n const provider = new BioProviderImpl(targetOrigin)\n window.bio = provider\n\n console.log('[BioSDK] Provider initialized')\n return provider\n}\n\n/**\n * Initialize all providers (bio, ethereum, tron)\n */\nexport function initAllProviders(targetOrigin = '*'): {\n bio: BioProvider\n ethereum: EthereumProvider\n tronLink: TronLinkProvider\n tronWeb: TronWebProvider\n} {\n const bio = initBioProvider(targetOrigin)\n const ethereum = initEthereumProvider(targetOrigin)\n const { tronLink, tronWeb } = initTronProvider(targetOrigin)\n\n return { bio, ethereum, tronLink, tronWeb }\n}\n\n// Auto-initialize if running in browser\nif (typeof window !== 'undefined') {\n const init = () => {\n initBioProvider()\n initEthereumProvider()\n initTronProvider()\n }\n\n // Use a slight delay to ensure DOM is ready\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', init)\n } else {\n init()\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/bio-sdk/dist/index.d.ts b/packages/bio-sdk/dist/index.d.ts index 5b18d899f..10143204a 100644 --- a/packages/bio-sdk/dist/index.d.ts +++ b/packages/bio-sdk/dist/index.d.ts @@ -1,3 +1,6 @@ +/** API chain name to KeyApp chain ID mapping */ +export declare const API_CHAIN_TO_KEYAPP: Record; + /** * Bio SDK Types * EIP-1193 style provider interface for Bio ecosystem @@ -151,9 +154,96 @@ export declare interface BioUnsignedTransaction { data: unknown; } +/** KeyApp chain ID to display name */ +export declare const CHAIN_DISPLAY_NAMES: Record; + /** Create a provider RPC error */ export declare function createProviderError(code: number, message: string, data?: unknown): ProviderRpcError; +/** + * EIP-1193 Ethereum Provider Implementation + */ +export declare class EthereumProvider { + private events; + private pendingRequests; + private requestIdCounter; + private connected; + private currentChainId; + private accounts; + private readonly targetOrigin; + readonly isMetaMask = false; + readonly isKeyApp = true; + constructor(targetOrigin?: string); + private setupMessageListener; + private handleMessage; + private handleResponse; + private handleEvent; + private generateId; + private postMessage; + /** + * EIP-1193 request method + */ + request(args: EthRequestArguments): Promise; + /** + * Subscribe to an event + */ + on(event: string, handler: (...args: unknown[]) => void): this; + /** + * Unsubscribe from an event + */ + off(event: string, handler: (...args: unknown[]) => void): this; + /** + * Alias for off (Node.js EventEmitter compatibility) + */ + removeListener(event: string, handler: (...args: unknown[]) => void): this; + /** + * Add listener that fires only once + */ + once(event: string, handler: (...args: unknown[]) => void): this; + /** + * EIP-1193 isConnected method + */ + isConnected(): boolean; + /** + * Get current chain ID (cached) + */ + get chainId(): string | null; + /** + * Get selected address (first account) + */ + get selectedAddress(): string | null; + /** + * @deprecated Use request({ method: 'eth_requestAccounts' }) + */ + enable(): Promise; + /** + * @deprecated Use request() + */ + send(method: string, params?: unknown[]): Promise; + /** + * @deprecated Use request() + */ + sendAsync(payload: { + method: string; + params?: unknown[]; + id?: number; + }, callback: (error: Error | null, result?: { + result: unknown; + }) => void): void; +} + +/** + * Ethereum Provider (EIP-1193 Compatible) + * + * Provides window.ethereum for EVM-compatible dApps. + * Communicates with KeyApp host via postMessage. + */ +/** EIP-1193 Request Arguments */ +declare interface EthRequestArguments { + method: string; + params?: unknown[] | Record; +} + export declare class EventEmitter { private handlers; on(event: string, handler: EventHandler): void; @@ -165,11 +255,69 @@ export declare class EventEmitter { /** Event handler type */ export declare type EventHandler = (...args: T[]) => void; +/** Reverse mapping: EVM chainId -> KeyApp chain ID */ +export declare const EVM_CHAIN_ID_TO_KEYAPP: Record; + +/** EVM Chain ID mapping (decimal) */ +export declare const EVM_CHAIN_IDS: Record; + +/** + * Get EVM hex chain ID from KeyApp chain ID + * @example getEvmChainId('binance') => '0x38' + */ +export declare function getEvmChainId(keyAppChainId: string): string | null; + +/** + * Get KeyApp chain ID from EVM hex chain ID + * @example getKeyAppChainId('0x38') => 'binance' + */ +export declare function getKeyAppChainId(hexChainId: string): string | null; + +/** + * Initialize all providers (bio, ethereum, tron) + */ +export declare function initAllProviders(targetOrigin?: string): { + bio: BioProvider; + ethereum: EthereumProvider; + tronLink: TronLinkProvider; + tronWeb: TronWebProvider; +}; + /** * Initialize and inject the Bio provider into window.bio */ export declare function initBioProvider(targetOrigin?: string): BioProvider; +/** + * Initialize and inject the Ethereum provider into window.ethereum + */ +export declare function initEthereumProvider(targetOrigin?: string): EthereumProvider; + +/** + * Initialize and inject the Tron providers + */ +export declare function initTronProvider(targetOrigin?: string): { + tronLink: TronLinkProvider; + tronWeb: TronWebProvider; +}; + +/** + * Check if a chain is EVM compatible + */ +export declare function isEvmChain(chainId: string): boolean; + +/** + * Normalize API chain name to KeyApp chain ID + * @example normalizeChainId('BSC') => 'binance' + */ +export declare function normalizeChainId(chainName: string): string; + +/** + * Parse hex chain ID to decimal + * @example parseHexChainId('0x38') => 56 + */ +export declare function parseHexChainId(hexChainId: string): number; + /** Provider RPC error */ export declare interface ProviderRpcError extends Error { code: number; @@ -182,6 +330,12 @@ export declare interface RequestArguments { params?: unknown[]; } +/** + * Convert decimal chain ID to hex string (EIP-155 format) + * @example toHexChainId(56) => '0x38' + */ +export declare function toHexChainId(chainId: number): string; + /** Transfer parameters */ export declare interface TransferParams { from: string; @@ -191,4 +345,101 @@ export declare interface TransferParams { asset?: string; } +/** + * Tron Provider (TronLink Compatible) + * + * Provides window.tronWeb and window.tronLink for Tron dApps. + * Communicates with KeyApp host via postMessage. + */ +/** Tron address format */ +declare interface TronAddress { + base58: string; + hex: string; +} + +/** + * TronLink-compatible Provider + */ +export declare class TronLinkProvider { + private events; + private pendingRequests; + private requestIdCounter; + private readonly targetOrigin; + constructor(targetOrigin?: string); + private setupMessageListener; + private handleMessage; + private handleResponse; + private handleEvent; + private generateId; + private postMessage; + /** + * TronLink request method (EIP-1193 style) + */ + request(args: TronRequestArguments): Promise; + on(event: string, handler: (...args: unknown[]) => void): this; + off(event: string, handler: (...args: unknown[]) => void): this; +} + +/** TronLink request arguments (EIP-1193 style) */ +declare interface TronRequestArguments { + method: string; + params?: unknown; +} + +/** + * TronWeb-compatible API + * Provides the subset of TronWeb API that KeyApp supports + */ +export declare class TronWebProvider { + private tronLink; + private _ready; + private _defaultAddress; + /** TRX operations */ + readonly trx: TronWebTrx; + constructor(tronLink: TronLinkProvider); + /** Whether TronWeb is ready (connected) */ + get ready(): boolean; + /** Current default address */ + get defaultAddress(): TronAddress; + /** + * Set default address (called by host after connection) + */ + setAddress(address: TronAddress): void; + /** + * Check if an address is valid + */ + isAddress(address: string): boolean; + /** + * Convert address to hex format + */ + address: { + toHex: (base58: string) => string; + fromHex: (hex: string) => string; + }; +} + +/** + * TronWeb.trx operations + */ +declare class TronWebTrx { + private tronLink; + constructor(tronLink: TronLinkProvider); + /** + * Sign a transaction + */ + sign(transaction: unknown): Promise; + /** + * Send raw transaction (broadcast) + */ + sendRawTransaction(signedTransaction: unknown): Promise; + /** + * Get account balance + */ + getBalance(address: string): Promise; + /** + * Get account info + */ + getAccount(address: string): Promise; +} + export { } diff --git a/packages/bio-sdk/dist/index.js b/packages/bio-sdk/dist/index.js index 3342a76c6..7b21fd7f7 100644 --- a/packages/bio-sdk/dist/index.js +++ b/packages/bio-sdk/dist/index.js @@ -144,6 +144,425 @@ class BioProviderImpl { return this.connected; } } +class EthereumProvider { + events = new EventEmitter(); + pendingRequests = /* @__PURE__ */ new Map(); + requestIdCounter = 0; + connected = false; + currentChainId = null; + accounts = []; + targetOrigin; + // EIP-1193 required properties + isMetaMask = false; + isKeyApp = true; + constructor(targetOrigin = "*") { + this.targetOrigin = targetOrigin; + this.setupMessageListener(); + } + setupMessageListener() { + window.addEventListener("message", this.handleMessage.bind(this)); + } + handleMessage(event) { + const data = event.data; + if (!data || typeof data !== "object") return; + if (data.type === "eth_response") { + this.handleResponse(data); + } else if (data.type === "eth_event") { + this.handleEvent(data); + } + } + handleResponse(message) { + const pending = this.pendingRequests.get(message.id); + if (!pending) return; + this.pendingRequests.delete(message.id); + if (message.success) { + pending.resolve(message.result); + } else { + const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: "Unknown error" }; + pending.reject(createProviderError(error.code, error.message, error.data)); + } + } + handleEvent(message) { + this.events.emit(message.event, ...message.args); + if (message.event === "connect") { + this.connected = true; + const info = message.args[0]; + this.currentChainId = info?.chainId ?? null; + } else if (message.event === "disconnect") { + this.connected = false; + this.accounts = []; + } else if (message.event === "chainChanged") { + this.currentChainId = message.args[0]; + } else if (message.event === "accountsChanged") { + this.accounts = message.args[0]; + } + } + generateId() { + return `eth_${Date.now()}_${++this.requestIdCounter}`; + } + postMessage(message) { + if (window.parent === window) { + console.warn("[EthereumProvider] Not running in iframe, cannot communicate with host"); + return; + } + window.parent.postMessage(message, this.targetOrigin); + } + /** + * EIP-1193 request method + */ + async request(args) { + const { method, params } = args; + const paramsArray = Array.isArray(params) ? params : params ? [params] : []; + const id = this.generateId(); + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { + resolve, + reject + }); + this.postMessage({ + type: "eth_request", + id, + method, + params: paramsArray + }); + setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id); + reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, "Request timeout")); + } + }, 5 * 60 * 1e3); + }); + } + /** + * Subscribe to an event + */ + on(event, handler) { + this.events.on(event, handler); + return this; + } + /** + * Unsubscribe from an event + */ + off(event, handler) { + this.events.off(event, handler); + return this; + } + /** + * Alias for off (Node.js EventEmitter compatibility) + */ + removeListener(event, handler) { + return this.off(event, handler); + } + /** + * Add listener that fires only once + */ + once(event, handler) { + const wrapper = (...args) => { + this.off(event, wrapper); + handler(...args); + }; + this.on(event, wrapper); + return this; + } + /** + * EIP-1193 isConnected method + */ + isConnected() { + return this.connected; + } + /** + * Get current chain ID (cached) + */ + get chainId() { + return this.currentChainId; + } + /** + * Get selected address (first account) + */ + get selectedAddress() { + return this.accounts[0] ?? null; + } + // ============================================ + // Legacy methods (for backwards compatibility) + // ============================================ + /** + * @deprecated Use request({ method: 'eth_requestAccounts' }) + */ + async enable() { + return this.request({ method: "eth_requestAccounts" }); + } + /** + * @deprecated Use request() + */ + send(method, params) { + return this.request({ method, params }); + } + /** + * @deprecated Use request() + */ + sendAsync(payload, callback) { + this.request({ method: payload.method, params: payload.params }).then((result) => callback(null, { result })).catch((error) => callback(error)); + } +} +function initEthereumProvider(targetOrigin = "*") { + if (typeof window === "undefined") { + throw new Error("[EthereumProvider] Cannot initialize: window is not defined"); + } + if (window.ethereum) { + console.warn("[EthereumProvider] Provider already exists, returning existing instance"); + return window.ethereum; + } + const provider = new EthereumProvider(targetOrigin); + window.ethereum = provider; + console.log("[EthereumProvider] Provider initialized"); + return provider; +} +class TronLinkProvider { + events = new EventEmitter(); + pendingRequests = /* @__PURE__ */ new Map(); + requestIdCounter = 0; + targetOrigin; + constructor(targetOrigin = "*") { + this.targetOrigin = targetOrigin; + this.setupMessageListener(); + } + setupMessageListener() { + window.addEventListener("message", this.handleMessage.bind(this)); + } + handleMessage(event) { + const data = event.data; + if (!data || typeof data !== "object") return; + if (data.type === "tron_response") { + this.handleResponse(data); + } else if (data.type === "tron_event") { + this.handleEvent(data); + } + } + handleResponse(message) { + const pending = this.pendingRequests.get(message.id); + if (!pending) return; + this.pendingRequests.delete(message.id); + if (message.success) { + pending.resolve(message.result); + } else { + const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: "Unknown error" }; + pending.reject(createProviderError(error.code, error.message, error.data)); + } + } + handleEvent(message) { + this.events.emit(message.event, ...message.args); + } + generateId() { + return `tron_${Date.now()}_${++this.requestIdCounter}`; + } + postMessage(message) { + if (window.parent === window) { + console.warn("[TronLinkProvider] Not running in iframe, cannot communicate with host"); + return; + } + window.parent.postMessage(message, this.targetOrigin); + } + /** + * TronLink request method (EIP-1193 style) + */ + async request(args) { + const { method, params } = args; + const paramsArray = Array.isArray(params) ? params : params !== void 0 ? [params] : []; + const id = this.generateId(); + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { + resolve, + reject + }); + this.postMessage({ + type: "tron_request", + id, + method, + params: paramsArray + }); + setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id); + reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, "Request timeout")); + } + }, 5 * 60 * 1e3); + }); + } + on(event, handler) { + this.events.on(event, handler); + return this; + } + off(event, handler) { + this.events.off(event, handler); + return this; + } +} +class TronWebProvider { + tronLink; + _ready = false; + _defaultAddress = { base58: "", hex: "" }; + /** TRX operations */ + trx; + constructor(tronLink) { + this.tronLink = tronLink; + this.trx = new TronWebTrx(tronLink); + tronLink.on("accountsChanged", (accounts) => { + if (Array.isArray(accounts) && accounts.length > 0) { + const addr = accounts[0]; + this._defaultAddress = addr; + this._ready = true; + } else { + this._defaultAddress = { base58: "", hex: "" }; + this._ready = false; + } + }); + } + /** Whether TronWeb is ready (connected) */ + get ready() { + return this._ready; + } + /** Current default address */ + get defaultAddress() { + return this._defaultAddress; + } + /** + * Set default address (called by host after connection) + */ + setAddress(address) { + this._defaultAddress = address; + this._ready = true; + } + /** + * Check if an address is valid + */ + isAddress(address) { + if (address.startsWith("T")) { + return address.length === 34; + } + if (address.startsWith("41")) { + return address.length === 42; + } + return false; + } + /** + * Convert address to hex format + */ + address = { + toHex: (base58) => { + return base58; + }, + fromHex: (hex) => { + return hex; + } + }; +} +class TronWebTrx { + tronLink; + constructor(tronLink) { + this.tronLink = tronLink; + } + /** + * Sign a transaction + */ + async sign(transaction) { + return this.tronLink.request({ + method: "tron_signTransaction", + params: transaction + }); + } + /** + * Send raw transaction (broadcast) + */ + async sendRawTransaction(signedTransaction) { + return this.tronLink.request({ + method: "tron_sendRawTransaction", + params: signedTransaction + }); + } + /** + * Get account balance + */ + async getBalance(address) { + return this.tronLink.request({ + method: "tron_getBalance", + params: address + }); + } + /** + * Get account info + */ + async getAccount(address) { + return this.tronLink.request({ + method: "tron_getAccount", + params: address + }); + } +} +function initTronProvider(targetOrigin = "*") { + if (typeof window === "undefined") { + throw new Error("[TronProvider] Cannot initialize: window is not defined"); + } + if (window.tronLink && window.tronWeb) { + console.warn("[TronProvider] Providers already exist, returning existing instances"); + return { tronLink: window.tronLink, tronWeb: window.tronWeb }; + } + const tronLink = new TronLinkProvider(targetOrigin); + const tronWeb = new TronWebProvider(tronLink); + window.tronLink = tronLink; + window.tronWeb = tronWeb; + console.log("[TronProvider] Providers initialized"); + return { tronLink, tronWeb }; +} +const EVM_CHAIN_IDS = { + ethereum: 1, + binance: 56 + // Future chains + // polygon: 137, + // arbitrum: 42161, + // optimism: 10, +}; +const EVM_CHAIN_ID_TO_KEYAPP = Object.fromEntries( + Object.entries(EVM_CHAIN_IDS).map(([key, value]) => [value, key]) +); +const API_CHAIN_TO_KEYAPP = { + ETH: "ethereum", + BSC: "binance", + TRON: "tron", + // Lowercase variants + eth: "ethereum", + bsc: "binance", + tron: "tron" +}; +const CHAIN_DISPLAY_NAMES = { + ethereum: "Ethereum", + binance: "BNB Smart Chain", + tron: "Tron", + bfmeta: "BFMeta", + bfchain: "BFChain" +}; +function toHexChainId(chainId) { + return `0x${chainId.toString(16)}`; +} +function parseHexChainId(hexChainId) { + if (!hexChainId.startsWith("0x")) { + throw new Error(`Invalid hex chain ID: ${hexChainId}`); + } + return parseInt(hexChainId, 16); +} +function getKeyAppChainId(hexChainId) { + const decimal = parseHexChainId(hexChainId); + return EVM_CHAIN_ID_TO_KEYAPP[decimal] ?? null; +} +function getEvmChainId(keyAppChainId) { + const decimal = EVM_CHAIN_IDS[keyAppChainId]; + return decimal ? toHexChainId(decimal) : null; +} +function isEvmChain(chainId) { + return chainId in EVM_CHAIN_IDS; +} +function normalizeChainId(chainName) { + return API_CHAIN_TO_KEYAPP[chainName] ?? chainName.toLowerCase(); +} function initBioProvider(targetOrigin = "*") { if (typeof window === "undefined") { throw new Error("[BioSDK] Cannot initialize: window is not defined"); @@ -157,18 +576,45 @@ function initBioProvider(targetOrigin = "*") { console.log("[BioSDK] Provider initialized"); return provider; } +function initAllProviders(targetOrigin = "*") { + const bio = initBioProvider(targetOrigin); + const ethereum = initEthereumProvider(targetOrigin); + const { tronLink, tronWeb } = initTronProvider(targetOrigin); + return { bio, ethereum, tronLink, tronWeb }; +} if (typeof window !== "undefined") { + const init = () => { + initBioProvider(); + initEthereumProvider(); + initTronProvider(); + }; if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", () => initBioProvider()); + document.addEventListener("DOMContentLoaded", init); } else { - initBioProvider(); + init(); } } export { + API_CHAIN_TO_KEYAPP, BioErrorCodes, BioProviderImpl, + CHAIN_DISPLAY_NAMES, + EVM_CHAIN_IDS, + EVM_CHAIN_ID_TO_KEYAPP, + EthereumProvider, EventEmitter, + TronLinkProvider, + TronWebProvider, createProviderError, - initBioProvider + getEvmChainId, + getKeyAppChainId, + initAllProviders, + initBioProvider, + initEthereumProvider, + initTronProvider, + isEvmChain, + normalizeChainId, + parseHexChainId, + toHexChainId }; //# sourceMappingURL=index.js.map diff --git a/packages/bio-sdk/dist/index.js.map b/packages/bio-sdk/dist/index.js.map index 0eb660931..f75a23424 100644 --- a/packages/bio-sdk/dist/index.js.map +++ b/packages/bio-sdk/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sources":["../src/types.ts","../src/events.ts","../src/provider.ts","../src/index.ts"],"sourcesContent":["/**\n * Bio SDK Types\n * EIP-1193 style provider interface for Bio ecosystem\n */\n\n/** Account information */\nexport interface BioAccount {\n address: string\n chain: string\n name?: string\n /** Public key (hex encoded) */\n publicKey: string\n}\n\n/** Transfer parameters */\nexport interface TransferParams {\n from: string\n to: string\n amount: string\n chain: string\n asset?: string\n}\n\n/** Unsigned transaction payload (chain-specific) */\nexport interface BioUnsignedTransaction {\n chainId: string\n data: unknown\n}\n\n/** Signed transaction payload (chain-specific) */\nexport interface BioSignedTransaction {\n chainId: string\n data: unknown\n signature: string\n}\n\n/** Provider request arguments */\nexport interface RequestArguments {\n method: string\n params?: unknown[]\n}\n\n/** Provider RPC error */\nexport interface ProviderRpcError extends Error {\n code: number\n data?: unknown\n}\n\n/** Event handler type */\nexport type EventHandler = (...args: T[]) => void\n\n/**\n * Bio Provider Interface (EIP-1193 style)\n */\nexport interface BioProvider {\n /** Make a request to the provider */\n request(args: RequestArguments): Promise\n\n /** Subscribe to an event */\n on(event: string, handler: EventHandler): void\n\n /** Unsubscribe from an event */\n off(event: string, handler: EventHandler): void\n\n /** Check if connected */\n isConnected(): boolean\n}\n\n/**\n * Bio method definitions\n */\nexport interface BioMethods {\n /** Request wallet accounts (shows connection UI) */\n bio_requestAccounts: () => Promise\n\n /** Get connected accounts (no UI) */\n bio_accounts: () => Promise\n\n /** Select an account (shows account picker UI) */\n bio_selectAccount: (opts?: { chain?: string }) => Promise\n\n /** Pick another wallet address (shows wallet picker UI) */\n bio_pickWallet: (opts?: { chain?: string; exclude?: string }) => Promise\n\n /** Sign a message, returns signature and public key (hex) */\n bio_signMessage: (params: { message: string; address: string }) => Promise<{ signature: string; publicKey: string }>\n\n /** Sign typed data, returns signature and public key (hex) */\n bio_signTypedData: (params: { data: object; address: string }) => Promise<{ signature: string; publicKey: string }>\n\n /** Create an unsigned transaction (no signature, no broadcast) */\n bio_createTransaction: (params: TransferParams) => Promise\n\n /** Sign an unsigned transaction (requires user confirmation) */\n bio_signTransaction: (params: { from: string; chain: string; unsignedTx: BioUnsignedTransaction }) => Promise\n\n /** Send a transaction */\n bio_sendTransaction: (params: TransferParams) => Promise<{ txHash: string }>\n\n /** Get current chain ID */\n bio_chainId: () => Promise\n\n /** Get balance */\n bio_getBalance: (params: { address: string; chain: string }) => Promise\n\n /** Close splash screen (indicates app is ready) */\n bio_closeSplashScreen: () => Promise\n}\n\n/**\n * Bio event definitions\n */\nexport interface BioEvents {\n /** Emitted when accounts change */\n accountsChanged: (accounts: BioAccount[]) => void\n\n /** Emitted when chain changes */\n chainChanged: (chainId: string) => void\n\n /** Emitted when connected */\n connect: (info: { chainId: string }) => void\n\n /** Emitted when disconnected */\n disconnect: (error: { code: number; message: string }) => void\n}\n\n/** Method names */\nexport type BioMethodName = keyof BioMethods\n\n/** Event names */\nexport type BioEventName = keyof BioEvents\n\n/** RPC error codes */\nexport const BioErrorCodes = {\n USER_REJECTED: 4001,\n UNAUTHORIZED: 4100,\n UNSUPPORTED_METHOD: 4200,\n DISCONNECTED: 4900,\n CHAIN_DISCONNECTED: 4901,\n INTERNAL_ERROR: -32603,\n INVALID_PARAMS: -32602,\n METHOD_NOT_FOUND: -32601,\n} as const\n\n/** Create a provider RPC error */\nexport function createProviderError(code: number, message: string, data?: unknown): ProviderRpcError {\n const error = new Error(message) as ProviderRpcError\n error.code = code\n error.data = data\n return error\n}\n","/**\n * Event emitter for Bio SDK\n */\n\nimport type { EventHandler } from './types'\n\nexport class EventEmitter {\n private handlers = new Map>()\n\n on(event: string, handler: EventHandler): void {\n let handlers = this.handlers.get(event)\n if (!handlers) {\n handlers = new Set()\n this.handlers.set(event, handlers)\n }\n handlers.add(handler)\n }\n\n off(event: string, handler: EventHandler): void {\n const handlers = this.handlers.get(event)\n if (handlers) {\n handlers.delete(handler)\n if (handlers.size === 0) {\n this.handlers.delete(event)\n }\n }\n }\n\n emit(event: string, ...args: unknown[]): void {\n const handlers = this.handlers.get(event)\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n handler(...args)\n } catch (error) {\n console.error(`[BioSDK] Error in event handler for \"${event}\":`, error)\n }\n })\n }\n }\n\n removeAllListeners(event?: string): void {\n if (event) {\n this.handlers.delete(event)\n } else {\n this.handlers.clear()\n }\n }\n}\n","/**\n * Bio Provider Implementation\n * Communicates with KeyApp host via postMessage\n */\n\nimport type { BioProvider, RequestArguments, EventHandler } from './types'\nimport { BioErrorCodes, createProviderError } from './types'\nimport { EventEmitter } from './events'\n\n/** Message sent to host */\ninterface RequestMessage {\n type: 'bio_request'\n id: string\n method: string\n params?: unknown[]\n}\n\n/** Response from host */\ninterface ResponseMessage {\n type: 'bio_response'\n id: string\n success: boolean\n result?: unknown\n error?: { code: number; message: string; data?: unknown }\n}\n\n/** Event from host */\ninterface EventMessage {\n type: 'bio_event'\n event: string\n args: unknown[]\n}\n\ntype HostMessage = ResponseMessage | EventMessage\n\nexport class BioProviderImpl implements BioProvider {\n private events = new EventEmitter()\n private pendingRequests = new Map void\n reject: (error: Error) => void\n }>()\n private requestIdCounter = 0\n private connected = false\n private readonly targetOrigin: string\n\n constructor(targetOrigin = '*') {\n this.targetOrigin = targetOrigin\n this.setupMessageListener()\n this.connect()\n }\n\n private setupMessageListener(): void {\n window.addEventListener('message', this.handleMessage.bind(this))\n }\n\n private handleMessage(event: MessageEvent): void {\n const data = event.data as HostMessage\n if (!data || typeof data !== 'object') return\n\n if (data.type === 'bio_response') {\n this.handleResponse(data)\n } else if (data.type === 'bio_event') {\n this.handleEvent(data)\n }\n }\n\n private handleResponse(message: ResponseMessage): void {\n const pending = this.pendingRequests.get(message.id)\n if (!pending) return\n\n this.pendingRequests.delete(message.id)\n\n if (message.success) {\n pending.resolve(message.result)\n } else {\n const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: 'Unknown error' }\n pending.reject(createProviderError(error.code, error.message, error.data))\n }\n }\n\n private handleEvent(message: EventMessage): void {\n this.events.emit(message.event, ...message.args)\n\n // Handle built-in events\n if (message.event === 'connect') {\n this.connected = true\n } else if (message.event === 'disconnect') {\n this.connected = false\n }\n }\n\n private connect(): void {\n // Send handshake to host\n this.postMessage({\n type: 'bio_request',\n id: this.generateId(),\n method: 'bio_connect',\n params: [],\n })\n }\n\n private generateId(): string {\n return `bio_${Date.now()}_${++this.requestIdCounter}`\n }\n\n private postMessage(message: RequestMessage): void {\n if (window.parent === window) {\n console.warn('[BioSDK] Not running in iframe, cannot communicate with host')\n return\n }\n window.parent.postMessage(message, this.targetOrigin)\n }\n\n async request(args: RequestArguments): Promise {\n const id = this.generateId()\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, {\n resolve: resolve as (value: unknown) => void,\n reject,\n })\n\n this.postMessage({\n type: 'bio_request',\n id,\n method: args.method,\n params: args.params,\n })\n\n // Timeout after 5 minutes (for user interactions)\n setTimeout(() => {\n if (this.pendingRequests.has(id)) {\n this.pendingRequests.delete(id)\n reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, 'Request timeout'))\n }\n }, 5 * 60 * 1000)\n })\n }\n\n on(event: string, handler: EventHandler): void {\n this.events.on(event, handler)\n }\n\n off(event: string, handler: EventHandler): void {\n this.events.off(event, handler)\n }\n\n isConnected(): boolean {\n return this.connected\n }\n}\n","/**\n * Bio SDK - Client SDK for Bio Ecosystem MiniApps\n *\n * Injects `window.bio` provider similar to `window.ethereum` in Web3 DApps.\n *\n * @example\n * ```typescript\n * import '@biochain/bio-sdk'\n *\n * // Now window.bio is available\n * const accounts = await window.bio.request({ method: 'bio_requestAccounts' })\n * ```\n */\n\nimport { BioProviderImpl } from './provider'\nimport type { BioProvider } from './types'\n\n// Re-export types\nexport * from './types'\nexport { EventEmitter } from './events'\nexport { BioProviderImpl } from './provider'\n\n// Extend Window interface\ndeclare global {\n interface Window {\n bio?: BioProvider\n }\n}\n\n/**\n * Initialize and inject the Bio provider into window.bio\n */\nexport function initBioProvider(targetOrigin = '*'): BioProvider {\n if (typeof window === 'undefined') {\n throw new Error('[BioSDK] Cannot initialize: window is not defined')\n }\n\n if (window.bio) {\n console.warn('[BioSDK] Provider already exists, returning existing instance')\n return window.bio\n }\n\n const provider = new BioProviderImpl(targetOrigin)\n window.bio = provider\n\n console.log('[BioSDK] Provider initialized')\n return provider\n}\n\n// Auto-initialize if running in browser\nif (typeof window !== 'undefined') {\n // Use a slight delay to ensure DOM is ready\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => initBioProvider())\n } else {\n initBioProvider()\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sources":["../src/types.ts","../src/events.ts","../src/provider.ts","../src/ethereum-provider.ts","../src/tron-provider.ts","../src/chain-id.ts","../src/index.ts"],"sourcesContent":["/**\n * Bio SDK Types\n * EIP-1193 style provider interface for Bio ecosystem\n */\n\n/** Account information */\nexport interface BioAccount {\n address: string\n chain: string\n name?: string\n /** Public key (hex encoded) */\n publicKey: string\n}\n\n/** Transfer parameters */\nexport interface TransferParams {\n from: string\n to: string\n amount: string\n chain: string\n asset?: string\n}\n\n/** Unsigned transaction payload (chain-specific) */\nexport interface BioUnsignedTransaction {\n chainId: string\n data: unknown\n}\n\n/** Signed transaction payload (chain-specific) */\nexport interface BioSignedTransaction {\n chainId: string\n data: unknown\n signature: string\n}\n\n/** Provider request arguments */\nexport interface RequestArguments {\n method: string\n params?: unknown[]\n}\n\n/** Provider RPC error */\nexport interface ProviderRpcError extends Error {\n code: number\n data?: unknown\n}\n\n/** Event handler type */\nexport type EventHandler = (...args: T[]) => void\n\n/**\n * Bio Provider Interface (EIP-1193 style)\n */\nexport interface BioProvider {\n /** Make a request to the provider */\n request(args: RequestArguments): Promise\n\n /** Subscribe to an event */\n on(event: string, handler: EventHandler): void\n\n /** Unsubscribe from an event */\n off(event: string, handler: EventHandler): void\n\n /** Check if connected */\n isConnected(): boolean\n}\n\n/**\n * Bio method definitions\n */\nexport interface BioMethods {\n /** Request wallet accounts (shows connection UI) */\n bio_requestAccounts: () => Promise\n\n /** Get connected accounts (no UI) */\n bio_accounts: () => Promise\n\n /** Select an account (shows account picker UI) */\n bio_selectAccount: (opts?: { chain?: string }) => Promise\n\n /** Pick another wallet address (shows wallet picker UI) */\n bio_pickWallet: (opts?: { chain?: string; exclude?: string }) => Promise\n\n /** Sign a message, returns signature and public key (hex) */\n bio_signMessage: (params: { message: string; address: string }) => Promise<{ signature: string; publicKey: string }>\n\n /** Sign typed data, returns signature and public key (hex) */\n bio_signTypedData: (params: { data: object; address: string }) => Promise<{ signature: string; publicKey: string }>\n\n /** Create an unsigned transaction (no signature, no broadcast) */\n bio_createTransaction: (params: TransferParams) => Promise\n\n /** Sign an unsigned transaction (requires user confirmation) */\n bio_signTransaction: (params: { from: string; chain: string; unsignedTx: BioUnsignedTransaction }) => Promise\n\n /** Send a transaction */\n bio_sendTransaction: (params: TransferParams) => Promise<{ txHash: string }>\n\n /** Get current chain ID */\n bio_chainId: () => Promise\n\n /** Get balance */\n bio_getBalance: (params: { address: string; chain: string }) => Promise\n\n /** Close splash screen (indicates app is ready) */\n bio_closeSplashScreen: () => Promise\n}\n\n/**\n * Bio event definitions\n */\nexport interface BioEvents {\n /** Emitted when accounts change */\n accountsChanged: (accounts: BioAccount[]) => void\n\n /** Emitted when chain changes */\n chainChanged: (chainId: string) => void\n\n /** Emitted when connected */\n connect: (info: { chainId: string }) => void\n\n /** Emitted when disconnected */\n disconnect: (error: { code: number; message: string }) => void\n}\n\n/** Method names */\nexport type BioMethodName = keyof BioMethods\n\n/** Event names */\nexport type BioEventName = keyof BioEvents\n\n/** RPC error codes */\nexport const BioErrorCodes = {\n USER_REJECTED: 4001,\n UNAUTHORIZED: 4100,\n UNSUPPORTED_METHOD: 4200,\n DISCONNECTED: 4900,\n CHAIN_DISCONNECTED: 4901,\n INTERNAL_ERROR: -32603,\n INVALID_PARAMS: -32602,\n METHOD_NOT_FOUND: -32601,\n} as const\n\n/** Create a provider RPC error */\nexport function createProviderError(code: number, message: string, data?: unknown): ProviderRpcError {\n const error = new Error(message) as ProviderRpcError\n error.code = code\n error.data = data\n return error\n}\n","/**\n * Event emitter for Bio SDK\n */\n\nimport type { EventHandler } from './types'\n\nexport class EventEmitter {\n private handlers = new Map>()\n\n on(event: string, handler: EventHandler): void {\n let handlers = this.handlers.get(event)\n if (!handlers) {\n handlers = new Set()\n this.handlers.set(event, handlers)\n }\n handlers.add(handler)\n }\n\n off(event: string, handler: EventHandler): void {\n const handlers = this.handlers.get(event)\n if (handlers) {\n handlers.delete(handler)\n if (handlers.size === 0) {\n this.handlers.delete(event)\n }\n }\n }\n\n emit(event: string, ...args: unknown[]): void {\n const handlers = this.handlers.get(event)\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n handler(...args)\n } catch (error) {\n console.error(`[BioSDK] Error in event handler for \"${event}\":`, error)\n }\n })\n }\n }\n\n removeAllListeners(event?: string): void {\n if (event) {\n this.handlers.delete(event)\n } else {\n this.handlers.clear()\n }\n }\n}\n","/**\n * Bio Provider Implementation\n * Communicates with KeyApp host via postMessage\n */\n\nimport type { BioProvider, RequestArguments, EventHandler } from './types'\nimport { BioErrorCodes, createProviderError } from './types'\nimport { EventEmitter } from './events'\n\n/** Message sent to host */\ninterface RequestMessage {\n type: 'bio_request'\n id: string\n method: string\n params?: unknown[]\n}\n\n/** Response from host */\ninterface ResponseMessage {\n type: 'bio_response'\n id: string\n success: boolean\n result?: unknown\n error?: { code: number; message: string; data?: unknown }\n}\n\n/** Event from host */\ninterface EventMessage {\n type: 'bio_event'\n event: string\n args: unknown[]\n}\n\ntype HostMessage = ResponseMessage | EventMessage\n\nexport class BioProviderImpl implements BioProvider {\n private events = new EventEmitter()\n private pendingRequests = new Map void\n reject: (error: Error) => void\n }>()\n private requestIdCounter = 0\n private connected = false\n private readonly targetOrigin: string\n\n constructor(targetOrigin = '*') {\n this.targetOrigin = targetOrigin\n this.setupMessageListener()\n this.connect()\n }\n\n private setupMessageListener(): void {\n window.addEventListener('message', this.handleMessage.bind(this))\n }\n\n private handleMessage(event: MessageEvent): void {\n const data = event.data as HostMessage\n if (!data || typeof data !== 'object') return\n\n if (data.type === 'bio_response') {\n this.handleResponse(data)\n } else if (data.type === 'bio_event') {\n this.handleEvent(data)\n }\n }\n\n private handleResponse(message: ResponseMessage): void {\n const pending = this.pendingRequests.get(message.id)\n if (!pending) return\n\n this.pendingRequests.delete(message.id)\n\n if (message.success) {\n pending.resolve(message.result)\n } else {\n const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: 'Unknown error' }\n pending.reject(createProviderError(error.code, error.message, error.data))\n }\n }\n\n private handleEvent(message: EventMessage): void {\n this.events.emit(message.event, ...message.args)\n\n // Handle built-in events\n if (message.event === 'connect') {\n this.connected = true\n } else if (message.event === 'disconnect') {\n this.connected = false\n }\n }\n\n private connect(): void {\n // Send handshake to host\n this.postMessage({\n type: 'bio_request',\n id: this.generateId(),\n method: 'bio_connect',\n params: [],\n })\n }\n\n private generateId(): string {\n return `bio_${Date.now()}_${++this.requestIdCounter}`\n }\n\n private postMessage(message: RequestMessage): void {\n if (window.parent === window) {\n console.warn('[BioSDK] Not running in iframe, cannot communicate with host')\n return\n }\n window.parent.postMessage(message, this.targetOrigin)\n }\n\n async request(args: RequestArguments): Promise {\n const id = this.generateId()\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, {\n resolve: resolve as (value: unknown) => void,\n reject,\n })\n\n this.postMessage({\n type: 'bio_request',\n id,\n method: args.method,\n params: args.params,\n })\n\n // Timeout after 5 minutes (for user interactions)\n setTimeout(() => {\n if (this.pendingRequests.has(id)) {\n this.pendingRequests.delete(id)\n reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, 'Request timeout'))\n }\n }, 5 * 60 * 1000)\n })\n }\n\n on(event: string, handler: EventHandler): void {\n this.events.on(event, handler)\n }\n\n off(event: string, handler: EventHandler): void {\n this.events.off(event, handler)\n }\n\n isConnected(): boolean {\n return this.connected\n }\n}\n","/**\n * Ethereum Provider (EIP-1193 Compatible)\n *\n * Provides window.ethereum for EVM-compatible dApps.\n * Communicates with KeyApp host via postMessage.\n */\n\nimport { EventEmitter } from './events'\nimport { BioErrorCodes, createProviderError, type ProviderRpcError } from './types'\nimport { toHexChainId, parseHexChainId, getKeyAppChainId, EVM_CHAIN_IDS } from './chain-id'\n\n/** EIP-1193 Request Arguments */\nexport interface EthRequestArguments {\n method: string\n params?: unknown[] | Record\n}\n\n/** EIP-1193 Provider Connect Info */\nexport interface ProviderConnectInfo {\n chainId: string\n}\n\n/** EIP-1193 Provider Message */\nexport interface ProviderMessage {\n type: string\n data: unknown\n}\n\n/** Transaction request (eth_sendTransaction) */\nexport interface TransactionRequest {\n from: string\n to?: string\n value?: string\n data?: string\n gas?: string\n gasPrice?: string\n maxFeePerGas?: string\n maxPriorityFeePerGas?: string\n nonce?: string\n}\n\n/** Message sent to host */\ninterface RequestMessage {\n type: 'eth_request'\n id: string\n method: string\n params?: unknown[]\n}\n\n/** Response from host */\ninterface ResponseMessage {\n type: 'eth_response'\n id: string\n success: boolean\n result?: unknown\n error?: { code: number; message: string; data?: unknown }\n}\n\n/** Event from host */\ninterface EventMessage {\n type: 'eth_event'\n event: string\n args: unknown[]\n}\n\ntype HostMessage = ResponseMessage | EventMessage\n\n/**\n * EIP-1193 Ethereum Provider Implementation\n */\nexport class EthereumProvider {\n private events = new EventEmitter()\n private pendingRequests = new Map void\n reject: (error: Error) => void\n }>()\n private requestIdCounter = 0\n private connected = false\n private currentChainId: string | null = null\n private accounts: string[] = []\n private readonly targetOrigin: string\n\n // EIP-1193 required properties\n readonly isMetaMask = false\n readonly isKeyApp = true\n\n constructor(targetOrigin = '*') {\n this.targetOrigin = targetOrigin\n this.setupMessageListener()\n }\n\n private setupMessageListener(): void {\n window.addEventListener('message', this.handleMessage.bind(this))\n }\n\n private handleMessage(event: MessageEvent): void {\n const data = event.data as HostMessage\n if (!data || typeof data !== 'object') return\n\n if (data.type === 'eth_response') {\n this.handleResponse(data)\n } else if (data.type === 'eth_event') {\n this.handleEvent(data)\n }\n }\n\n private handleResponse(message: ResponseMessage): void {\n const pending = this.pendingRequests.get(message.id)\n if (!pending) return\n\n this.pendingRequests.delete(message.id)\n\n if (message.success) {\n pending.resolve(message.result)\n } else {\n const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: 'Unknown error' }\n pending.reject(createProviderError(error.code, error.message, error.data))\n }\n }\n\n private handleEvent(message: EventMessage): void {\n this.events.emit(message.event, ...message.args)\n\n // Handle built-in events\n if (message.event === 'connect') {\n this.connected = true\n const info = message.args[0] as ProviderConnectInfo\n this.currentChainId = info?.chainId ?? null\n } else if (message.event === 'disconnect') {\n this.connected = false\n this.accounts = []\n } else if (message.event === 'chainChanged') {\n this.currentChainId = message.args[0] as string\n } else if (message.event === 'accountsChanged') {\n this.accounts = message.args[0] as string[]\n }\n }\n\n private generateId(): string {\n return `eth_${Date.now()}_${++this.requestIdCounter}`\n }\n\n private postMessage(message: RequestMessage): void {\n if (window.parent === window) {\n console.warn('[EthereumProvider] Not running in iframe, cannot communicate with host')\n return\n }\n window.parent.postMessage(message, this.targetOrigin)\n }\n\n /**\n * EIP-1193 request method\n */\n async request(args: EthRequestArguments): Promise {\n const { method, params } = args\n const paramsArray = Array.isArray(params) ? params : params ? [params] : []\n\n const id = this.generateId()\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, {\n resolve: resolve as (value: unknown) => void,\n reject,\n })\n\n this.postMessage({\n type: 'eth_request',\n id,\n method,\n params: paramsArray,\n })\n\n // Timeout after 5 minutes (for user interactions)\n setTimeout(() => {\n if (this.pendingRequests.has(id)) {\n this.pendingRequests.delete(id)\n reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, 'Request timeout'))\n }\n }, 5 * 60 * 1000)\n })\n }\n\n /**\n * Subscribe to an event\n */\n on(event: string, handler: (...args: unknown[]) => void): this {\n this.events.on(event, handler)\n return this\n }\n\n /**\n * Unsubscribe from an event\n */\n off(event: string, handler: (...args: unknown[]) => void): this {\n this.events.off(event, handler)\n return this\n }\n\n /**\n * Alias for off (Node.js EventEmitter compatibility)\n */\n removeListener(event: string, handler: (...args: unknown[]) => void): this {\n return this.off(event, handler)\n }\n\n /**\n * Add listener that fires only once\n */\n once(event: string, handler: (...args: unknown[]) => void): this {\n const wrapper = (...args: unknown[]) => {\n this.off(event, wrapper)\n handler(...args)\n }\n this.on(event, wrapper)\n return this\n }\n\n /**\n * EIP-1193 isConnected method\n */\n isConnected(): boolean {\n return this.connected\n }\n\n /**\n * Get current chain ID (cached)\n */\n get chainId(): string | null {\n return this.currentChainId\n }\n\n /**\n * Get selected address (first account)\n */\n get selectedAddress(): string | null {\n return this.accounts[0] ?? null\n }\n\n // ============================================\n // Legacy methods (for backwards compatibility)\n // ============================================\n\n /**\n * @deprecated Use request({ method: 'eth_requestAccounts' })\n */\n async enable(): Promise {\n return this.request({ method: 'eth_requestAccounts' })\n }\n\n /**\n * @deprecated Use request()\n */\n send(method: string, params?: unknown[]): Promise {\n return this.request({ method, params })\n }\n\n /**\n * @deprecated Use request()\n */\n sendAsync(\n payload: { method: string; params?: unknown[]; id?: number },\n callback: (error: Error | null, result?: { result: unknown }) => void\n ): void {\n this.request({ method: payload.method, params: payload.params })\n .then((result) => callback(null, { result }))\n .catch((error) => callback(error))\n }\n}\n\n// Extend Window interface\ndeclare global {\n interface Window {\n ethereum?: EthereumProvider\n }\n}\n\n/**\n * Initialize and inject the Ethereum provider into window.ethereum\n */\nexport function initEthereumProvider(targetOrigin = '*'): EthereumProvider {\n if (typeof window === 'undefined') {\n throw new Error('[EthereumProvider] Cannot initialize: window is not defined')\n }\n\n if (window.ethereum) {\n console.warn('[EthereumProvider] Provider already exists, returning existing instance')\n return window.ethereum\n }\n\n const provider = new EthereumProvider(targetOrigin)\n window.ethereum = provider\n\n console.log('[EthereumProvider] Provider initialized')\n return provider\n}\n","/**\n * Tron Provider (TronLink Compatible)\n *\n * Provides window.tronWeb and window.tronLink for Tron dApps.\n * Communicates with KeyApp host via postMessage.\n */\n\nimport { EventEmitter } from './events'\nimport { BioErrorCodes, createProviderError } from './types'\n\n/** Tron address format */\nexport interface TronAddress {\n base58: string\n hex: string\n}\n\n/** TronLink request arguments (EIP-1193 style) */\nexport interface TronRequestArguments {\n method: string\n params?: unknown\n}\n\n/** Message sent to host */\ninterface RequestMessage {\n type: 'tron_request'\n id: string\n method: string\n params?: unknown[]\n}\n\n/** Response from host */\ninterface ResponseMessage {\n type: 'tron_response'\n id: string\n success: boolean\n result?: unknown\n error?: { code: number; message: string; data?: unknown }\n}\n\n/** Event from host */\ninterface EventMessage {\n type: 'tron_event'\n event: string\n args: unknown[]\n}\n\ntype HostMessage = ResponseMessage | EventMessage\n\n/**\n * TronLink-compatible Provider\n */\nexport class TronLinkProvider {\n private events = new EventEmitter()\n private pendingRequests = new Map void\n reject: (error: Error) => void\n }>()\n private requestIdCounter = 0\n private readonly targetOrigin: string\n\n constructor(targetOrigin = '*') {\n this.targetOrigin = targetOrigin\n this.setupMessageListener()\n }\n\n private setupMessageListener(): void {\n window.addEventListener('message', this.handleMessage.bind(this))\n }\n\n private handleMessage(event: MessageEvent): void {\n const data = event.data as HostMessage\n if (!data || typeof data !== 'object') return\n\n if (data.type === 'tron_response') {\n this.handleResponse(data)\n } else if (data.type === 'tron_event') {\n this.handleEvent(data)\n }\n }\n\n private handleResponse(message: ResponseMessage): void {\n const pending = this.pendingRequests.get(message.id)\n if (!pending) return\n\n this.pendingRequests.delete(message.id)\n\n if (message.success) {\n pending.resolve(message.result)\n } else {\n const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: 'Unknown error' }\n pending.reject(createProviderError(error.code, error.message, error.data))\n }\n }\n\n private handleEvent(message: EventMessage): void {\n this.events.emit(message.event, ...message.args)\n }\n\n private generateId(): string {\n return `tron_${Date.now()}_${++this.requestIdCounter}`\n }\n\n private postMessage(message: RequestMessage): void {\n if (window.parent === window) {\n console.warn('[TronLinkProvider] Not running in iframe, cannot communicate with host')\n return\n }\n window.parent.postMessage(message, this.targetOrigin)\n }\n\n /**\n * TronLink request method (EIP-1193 style)\n */\n async request(args: TronRequestArguments): Promise {\n const { method, params } = args\n const paramsArray = Array.isArray(params) ? params : params !== undefined ? [params] : []\n\n const id = this.generateId()\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, {\n resolve: resolve as (value: unknown) => void,\n reject,\n })\n\n this.postMessage({\n type: 'tron_request',\n id,\n method,\n params: paramsArray,\n })\n\n // Timeout after 5 minutes\n setTimeout(() => {\n if (this.pendingRequests.has(id)) {\n this.pendingRequests.delete(id)\n reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, 'Request timeout'))\n }\n }, 5 * 60 * 1000)\n })\n }\n\n on(event: string, handler: (...args: unknown[]) => void): this {\n this.events.on(event, handler)\n return this\n }\n\n off(event: string, handler: (...args: unknown[]) => void): this {\n this.events.off(event, handler)\n return this\n }\n}\n\n/**\n * TronWeb-compatible API\n * Provides the subset of TronWeb API that KeyApp supports\n */\nexport class TronWebProvider {\n private tronLink: TronLinkProvider\n private _ready = false\n private _defaultAddress: TronAddress = { base58: '', hex: '' }\n\n /** TRX operations */\n readonly trx: TronWebTrx\n\n constructor(tronLink: TronLinkProvider) {\n this.tronLink = tronLink\n this.trx = new TronWebTrx(tronLink)\n\n // Listen for account changes\n tronLink.on('accountsChanged', (accounts: unknown) => {\n if (Array.isArray(accounts) && accounts.length > 0) {\n const addr = accounts[0] as TronAddress\n this._defaultAddress = addr\n this._ready = true\n } else {\n this._defaultAddress = { base58: '', hex: '' }\n this._ready = false\n }\n })\n }\n\n /** Whether TronWeb is ready (connected) */\n get ready(): boolean {\n return this._ready\n }\n\n /** Current default address */\n get defaultAddress(): TronAddress {\n return this._defaultAddress\n }\n\n /**\n * Set default address (called by host after connection)\n */\n setAddress(address: TronAddress): void {\n this._defaultAddress = address\n this._ready = true\n }\n\n /**\n * Check if an address is valid\n */\n isAddress(address: string): boolean {\n // Basic validation: base58 starts with T, hex starts with 41\n if (address.startsWith('T')) {\n return address.length === 34\n }\n if (address.startsWith('41')) {\n return address.length === 42\n }\n return false\n }\n\n /**\n * Convert address to hex format\n */\n address = {\n toHex: (base58: string): string => {\n // This is a stub - actual conversion requires TronWeb library\n // KeyApp will handle the conversion on the host side\n return base58\n },\n fromHex: (hex: string): string => {\n return hex\n },\n }\n}\n\n/**\n * TronWeb.trx operations\n */\nclass TronWebTrx {\n private tronLink: TronLinkProvider\n\n constructor(tronLink: TronLinkProvider) {\n this.tronLink = tronLink\n }\n\n /**\n * Sign a transaction\n */\n async sign(transaction: unknown): Promise {\n return this.tronLink.request({\n method: 'tron_signTransaction',\n params: transaction,\n })\n }\n\n /**\n * Send raw transaction (broadcast)\n */\n async sendRawTransaction(signedTransaction: unknown): Promise {\n return this.tronLink.request({\n method: 'tron_sendRawTransaction',\n params: signedTransaction,\n })\n }\n\n /**\n * Get account balance\n */\n async getBalance(address: string): Promise {\n return this.tronLink.request({\n method: 'tron_getBalance',\n params: address,\n })\n }\n\n /**\n * Get account info\n */\n async getAccount(address: string): Promise {\n return this.tronLink.request({\n method: 'tron_getAccount',\n params: address,\n })\n }\n}\n\n// Extend Window interface\ndeclare global {\n interface Window {\n tronLink?: TronLinkProvider\n tronWeb?: TronWebProvider\n }\n}\n\n/**\n * Initialize and inject the Tron providers\n */\nexport function initTronProvider(targetOrigin = '*'): { tronLink: TronLinkProvider; tronWeb: TronWebProvider } {\n if (typeof window === 'undefined') {\n throw new Error('[TronProvider] Cannot initialize: window is not defined')\n }\n\n if (window.tronLink && window.tronWeb) {\n console.warn('[TronProvider] Providers already exist, returning existing instances')\n return { tronLink: window.tronLink, tronWeb: window.tronWeb }\n }\n\n const tronLink = new TronLinkProvider(targetOrigin)\n const tronWeb = new TronWebProvider(tronLink)\n\n window.tronLink = tronLink\n window.tronWeb = tronWeb\n\n console.log('[TronProvider] Providers initialized')\n return { tronLink, tronWeb }\n}\n","/**\n * Chain ID utilities\n * Maps between KeyApp internal chain IDs and standard chain IDs\n */\n\n/** EVM Chain ID mapping (decimal) */\nexport const EVM_CHAIN_IDS: Record = {\n ethereum: 1,\n binance: 56,\n // Future chains\n // polygon: 137,\n // arbitrum: 42161,\n // optimism: 10,\n} as const\n\n/** Reverse mapping: EVM chainId -> KeyApp chain ID */\nexport const EVM_CHAIN_ID_TO_KEYAPP: Record = Object.fromEntries(\n Object.entries(EVM_CHAIN_IDS).map(([key, value]) => [value, key])\n)\n\n/** API chain name to KeyApp chain ID mapping */\nexport const API_CHAIN_TO_KEYAPP: Record = {\n ETH: 'ethereum',\n BSC: 'binance',\n TRON: 'tron',\n // Lowercase variants\n eth: 'ethereum',\n bsc: 'binance',\n tron: 'tron',\n} as const\n\n/** KeyApp chain ID to display name */\nexport const CHAIN_DISPLAY_NAMES: Record = {\n ethereum: 'Ethereum',\n binance: 'BNB Smart Chain',\n tron: 'Tron',\n bfmeta: 'BFMeta',\n bfchain: 'BFChain',\n} as const\n\n/**\n * Convert decimal chain ID to hex string (EIP-155 format)\n * @example toHexChainId(56) => '0x38'\n */\nexport function toHexChainId(chainId: number): string {\n return `0x${chainId.toString(16)}`\n}\n\n/**\n * Parse hex chain ID to decimal\n * @example parseHexChainId('0x38') => 56\n */\nexport function parseHexChainId(hexChainId: string): number {\n if (!hexChainId.startsWith('0x')) {\n throw new Error(`Invalid hex chain ID: ${hexChainId}`)\n }\n return parseInt(hexChainId, 16)\n}\n\n/**\n * Get KeyApp chain ID from EVM hex chain ID\n * @example getKeyAppChainId('0x38') => 'binance'\n */\nexport function getKeyAppChainId(hexChainId: string): string | null {\n const decimal = parseHexChainId(hexChainId)\n return EVM_CHAIN_ID_TO_KEYAPP[decimal] ?? null\n}\n\n/**\n * Get EVM hex chain ID from KeyApp chain ID\n * @example getEvmChainId('binance') => '0x38'\n */\nexport function getEvmChainId(keyAppChainId: string): string | null {\n const decimal = EVM_CHAIN_IDS[keyAppChainId]\n return decimal ? toHexChainId(decimal) : null\n}\n\n/**\n * Check if a chain is EVM compatible\n */\nexport function isEvmChain(chainId: string): boolean {\n return chainId in EVM_CHAIN_IDS\n}\n\n/**\n * Normalize API chain name to KeyApp chain ID\n * @example normalizeChainId('BSC') => 'binance'\n */\nexport function normalizeChainId(chainName: string): string {\n return API_CHAIN_TO_KEYAPP[chainName] ?? chainName.toLowerCase()\n}\n","/**\n * Bio SDK - Client SDK for Bio Ecosystem MiniApps\n *\n * Injects providers for multi-chain dApp support:\n * - `window.bio` - BioChain + KeyApp wallet features\n * - `window.ethereum` - EVM-compatible chains (ETH, BSC)\n * - `window.tronWeb` / `window.tronLink` - Tron chain\n *\n * @example\n * ```typescript\n * import '@biochain/bio-sdk'\n *\n * // BioChain operations\n * const accounts = await window.bio.request({ method: 'bio_requestAccounts' })\n *\n * // EVM operations (ETH, BSC)\n * const ethAccounts = await window.ethereum.request({ method: 'eth_requestAccounts' })\n *\n * // Tron operations\n * const tronAccounts = await window.tronLink.request({ method: 'tron_requestAccounts' })\n * ```\n */\n\nimport { BioProviderImpl } from './provider'\nimport { EthereumProvider, initEthereumProvider } from './ethereum-provider'\nimport { TronLinkProvider, TronWebProvider, initTronProvider } from './tron-provider'\nimport type { BioProvider } from './types'\n\n// Re-export types\nexport * from './types'\nexport * from './chain-id'\nexport { EventEmitter } from './events'\nexport { BioProviderImpl } from './provider'\nexport { EthereumProvider, initEthereumProvider } from './ethereum-provider'\nexport { TronLinkProvider, TronWebProvider, initTronProvider } from './tron-provider'\n\n// Extend Window interface (bio is declared in types.ts already for ethereum/tron)\ndeclare global {\n interface Window {\n bio?: BioProvider\n }\n}\n\n/**\n * Initialize and inject the Bio provider into window.bio\n */\nexport function initBioProvider(targetOrigin = '*'): BioProvider {\n if (typeof window === 'undefined') {\n throw new Error('[BioSDK] Cannot initialize: window is not defined')\n }\n\n if (window.bio) {\n console.warn('[BioSDK] Provider already exists, returning existing instance')\n return window.bio\n }\n\n const provider = new BioProviderImpl(targetOrigin)\n window.bio = provider\n\n console.log('[BioSDK] Provider initialized')\n return provider\n}\n\n/**\n * Initialize all providers (bio, ethereum, tron)\n */\nexport function initAllProviders(targetOrigin = '*'): {\n bio: BioProvider\n ethereum: EthereumProvider\n tronLink: TronLinkProvider\n tronWeb: TronWebProvider\n} {\n const bio = initBioProvider(targetOrigin)\n const ethereum = initEthereumProvider(targetOrigin)\n const { tronLink, tronWeb } = initTronProvider(targetOrigin)\n\n return { bio, ethereum, tronLink, tronWeb }\n}\n\n// Auto-initialize if running in browser\nif (typeof window !== 'undefined') {\n const init = () => {\n initBioProvider()\n initEthereumProvider()\n initTronProvider()\n }\n\n // Use a slight delay to ensure DOM is ready\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', init)\n } else {\n init()\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/bio-sdk/dist/index.umd.js b/packages/bio-sdk/dist/index.umd.js index 29a26ff9d..d375b02af 100644 --- a/packages/bio-sdk/dist/index.umd.js +++ b/packages/bio-sdk/dist/index.umd.js @@ -148,6 +148,425 @@ return this.connected; } } + class EthereumProvider { + events = new EventEmitter(); + pendingRequests = /* @__PURE__ */ new Map(); + requestIdCounter = 0; + connected = false; + currentChainId = null; + accounts = []; + targetOrigin; + // EIP-1193 required properties + isMetaMask = false; + isKeyApp = true; + constructor(targetOrigin = "*") { + this.targetOrigin = targetOrigin; + this.setupMessageListener(); + } + setupMessageListener() { + window.addEventListener("message", this.handleMessage.bind(this)); + } + handleMessage(event) { + const data = event.data; + if (!data || typeof data !== "object") return; + if (data.type === "eth_response") { + this.handleResponse(data); + } else if (data.type === "eth_event") { + this.handleEvent(data); + } + } + handleResponse(message) { + const pending = this.pendingRequests.get(message.id); + if (!pending) return; + this.pendingRequests.delete(message.id); + if (message.success) { + pending.resolve(message.result); + } else { + const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: "Unknown error" }; + pending.reject(createProviderError(error.code, error.message, error.data)); + } + } + handleEvent(message) { + this.events.emit(message.event, ...message.args); + if (message.event === "connect") { + this.connected = true; + const info = message.args[0]; + this.currentChainId = info?.chainId ?? null; + } else if (message.event === "disconnect") { + this.connected = false; + this.accounts = []; + } else if (message.event === "chainChanged") { + this.currentChainId = message.args[0]; + } else if (message.event === "accountsChanged") { + this.accounts = message.args[0]; + } + } + generateId() { + return `eth_${Date.now()}_${++this.requestIdCounter}`; + } + postMessage(message) { + if (window.parent === window) { + console.warn("[EthereumProvider] Not running in iframe, cannot communicate with host"); + return; + } + window.parent.postMessage(message, this.targetOrigin); + } + /** + * EIP-1193 request method + */ + async request(args) { + const { method, params } = args; + const paramsArray = Array.isArray(params) ? params : params ? [params] : []; + const id = this.generateId(); + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { + resolve, + reject + }); + this.postMessage({ + type: "eth_request", + id, + method, + params: paramsArray + }); + setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id); + reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, "Request timeout")); + } + }, 5 * 60 * 1e3); + }); + } + /** + * Subscribe to an event + */ + on(event, handler) { + this.events.on(event, handler); + return this; + } + /** + * Unsubscribe from an event + */ + off(event, handler) { + this.events.off(event, handler); + return this; + } + /** + * Alias for off (Node.js EventEmitter compatibility) + */ + removeListener(event, handler) { + return this.off(event, handler); + } + /** + * Add listener that fires only once + */ + once(event, handler) { + const wrapper = (...args) => { + this.off(event, wrapper); + handler(...args); + }; + this.on(event, wrapper); + return this; + } + /** + * EIP-1193 isConnected method + */ + isConnected() { + return this.connected; + } + /** + * Get current chain ID (cached) + */ + get chainId() { + return this.currentChainId; + } + /** + * Get selected address (first account) + */ + get selectedAddress() { + return this.accounts[0] ?? null; + } + // ============================================ + // Legacy methods (for backwards compatibility) + // ============================================ + /** + * @deprecated Use request({ method: 'eth_requestAccounts' }) + */ + async enable() { + return this.request({ method: "eth_requestAccounts" }); + } + /** + * @deprecated Use request() + */ + send(method, params) { + return this.request({ method, params }); + } + /** + * @deprecated Use request() + */ + sendAsync(payload, callback) { + this.request({ method: payload.method, params: payload.params }).then((result) => callback(null, { result })).catch((error) => callback(error)); + } + } + function initEthereumProvider(targetOrigin = "*") { + if (typeof window === "undefined") { + throw new Error("[EthereumProvider] Cannot initialize: window is not defined"); + } + if (window.ethereum) { + console.warn("[EthereumProvider] Provider already exists, returning existing instance"); + return window.ethereum; + } + const provider = new EthereumProvider(targetOrigin); + window.ethereum = provider; + console.log("[EthereumProvider] Provider initialized"); + return provider; + } + class TronLinkProvider { + events = new EventEmitter(); + pendingRequests = /* @__PURE__ */ new Map(); + requestIdCounter = 0; + targetOrigin; + constructor(targetOrigin = "*") { + this.targetOrigin = targetOrigin; + this.setupMessageListener(); + } + setupMessageListener() { + window.addEventListener("message", this.handleMessage.bind(this)); + } + handleMessage(event) { + const data = event.data; + if (!data || typeof data !== "object") return; + if (data.type === "tron_response") { + this.handleResponse(data); + } else if (data.type === "tron_event") { + this.handleEvent(data); + } + } + handleResponse(message) { + const pending = this.pendingRequests.get(message.id); + if (!pending) return; + this.pendingRequests.delete(message.id); + if (message.success) { + pending.resolve(message.result); + } else { + const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: "Unknown error" }; + pending.reject(createProviderError(error.code, error.message, error.data)); + } + } + handleEvent(message) { + this.events.emit(message.event, ...message.args); + } + generateId() { + return `tron_${Date.now()}_${++this.requestIdCounter}`; + } + postMessage(message) { + if (window.parent === window) { + console.warn("[TronLinkProvider] Not running in iframe, cannot communicate with host"); + return; + } + window.parent.postMessage(message, this.targetOrigin); + } + /** + * TronLink request method (EIP-1193 style) + */ + async request(args) { + const { method, params } = args; + const paramsArray = Array.isArray(params) ? params : params !== void 0 ? [params] : []; + const id = this.generateId(); + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { + resolve, + reject + }); + this.postMessage({ + type: "tron_request", + id, + method, + params: paramsArray + }); + setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id); + reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, "Request timeout")); + } + }, 5 * 60 * 1e3); + }); + } + on(event, handler) { + this.events.on(event, handler); + return this; + } + off(event, handler) { + this.events.off(event, handler); + return this; + } + } + class TronWebProvider { + tronLink; + _ready = false; + _defaultAddress = { base58: "", hex: "" }; + /** TRX operations */ + trx; + constructor(tronLink) { + this.tronLink = tronLink; + this.trx = new TronWebTrx(tronLink); + tronLink.on("accountsChanged", (accounts) => { + if (Array.isArray(accounts) && accounts.length > 0) { + const addr = accounts[0]; + this._defaultAddress = addr; + this._ready = true; + } else { + this._defaultAddress = { base58: "", hex: "" }; + this._ready = false; + } + }); + } + /** Whether TronWeb is ready (connected) */ + get ready() { + return this._ready; + } + /** Current default address */ + get defaultAddress() { + return this._defaultAddress; + } + /** + * Set default address (called by host after connection) + */ + setAddress(address) { + this._defaultAddress = address; + this._ready = true; + } + /** + * Check if an address is valid + */ + isAddress(address) { + if (address.startsWith("T")) { + return address.length === 34; + } + if (address.startsWith("41")) { + return address.length === 42; + } + return false; + } + /** + * Convert address to hex format + */ + address = { + toHex: (base58) => { + return base58; + }, + fromHex: (hex) => { + return hex; + } + }; + } + class TronWebTrx { + tronLink; + constructor(tronLink) { + this.tronLink = tronLink; + } + /** + * Sign a transaction + */ + async sign(transaction) { + return this.tronLink.request({ + method: "tron_signTransaction", + params: transaction + }); + } + /** + * Send raw transaction (broadcast) + */ + async sendRawTransaction(signedTransaction) { + return this.tronLink.request({ + method: "tron_sendRawTransaction", + params: signedTransaction + }); + } + /** + * Get account balance + */ + async getBalance(address) { + return this.tronLink.request({ + method: "tron_getBalance", + params: address + }); + } + /** + * Get account info + */ + async getAccount(address) { + return this.tronLink.request({ + method: "tron_getAccount", + params: address + }); + } + } + function initTronProvider(targetOrigin = "*") { + if (typeof window === "undefined") { + throw new Error("[TronProvider] Cannot initialize: window is not defined"); + } + if (window.tronLink && window.tronWeb) { + console.warn("[TronProvider] Providers already exist, returning existing instances"); + return { tronLink: window.tronLink, tronWeb: window.tronWeb }; + } + const tronLink = new TronLinkProvider(targetOrigin); + const tronWeb = new TronWebProvider(tronLink); + window.tronLink = tronLink; + window.tronWeb = tronWeb; + console.log("[TronProvider] Providers initialized"); + return { tronLink, tronWeb }; + } + const EVM_CHAIN_IDS = { + ethereum: 1, + binance: 56 + // Future chains + // polygon: 137, + // arbitrum: 42161, + // optimism: 10, + }; + const EVM_CHAIN_ID_TO_KEYAPP = Object.fromEntries( + Object.entries(EVM_CHAIN_IDS).map(([key, value]) => [value, key]) + ); + const API_CHAIN_TO_KEYAPP = { + ETH: "ethereum", + BSC: "binance", + TRON: "tron", + // Lowercase variants + eth: "ethereum", + bsc: "binance", + tron: "tron" + }; + const CHAIN_DISPLAY_NAMES = { + ethereum: "Ethereum", + binance: "BNB Smart Chain", + tron: "Tron", + bfmeta: "BFMeta", + bfchain: "BFChain" + }; + function toHexChainId(chainId) { + return `0x${chainId.toString(16)}`; + } + function parseHexChainId(hexChainId) { + if (!hexChainId.startsWith("0x")) { + throw new Error(`Invalid hex chain ID: ${hexChainId}`); + } + return parseInt(hexChainId, 16); + } + function getKeyAppChainId(hexChainId) { + const decimal = parseHexChainId(hexChainId); + return EVM_CHAIN_ID_TO_KEYAPP[decimal] ?? null; + } + function getEvmChainId(keyAppChainId) { + const decimal = EVM_CHAIN_IDS[keyAppChainId]; + return decimal ? toHexChainId(decimal) : null; + } + function isEvmChain(chainId) { + return chainId in EVM_CHAIN_IDS; + } + function normalizeChainId(chainName) { + return API_CHAIN_TO_KEYAPP[chainName] ?? chainName.toLowerCase(); + } function initBioProvider(targetOrigin = "*") { if (typeof window === "undefined") { throw new Error("[BioSDK] Cannot initialize: window is not defined"); @@ -161,18 +580,45 @@ console.log("[BioSDK] Provider initialized"); return provider; } + function initAllProviders(targetOrigin = "*") { + const bio = initBioProvider(targetOrigin); + const ethereum = initEthereumProvider(targetOrigin); + const { tronLink, tronWeb } = initTronProvider(targetOrigin); + return { bio, ethereum, tronLink, tronWeb }; + } if (typeof window !== "undefined") { + const init = () => { + initBioProvider(); + initEthereumProvider(); + initTronProvider(); + }; if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", () => initBioProvider()); + document.addEventListener("DOMContentLoaded", init); } else { - initBioProvider(); + init(); } } + exports2.API_CHAIN_TO_KEYAPP = API_CHAIN_TO_KEYAPP; exports2.BioErrorCodes = BioErrorCodes; exports2.BioProviderImpl = BioProviderImpl; + exports2.CHAIN_DISPLAY_NAMES = CHAIN_DISPLAY_NAMES; + exports2.EVM_CHAIN_IDS = EVM_CHAIN_IDS; + exports2.EVM_CHAIN_ID_TO_KEYAPP = EVM_CHAIN_ID_TO_KEYAPP; + exports2.EthereumProvider = EthereumProvider; exports2.EventEmitter = EventEmitter; + exports2.TronLinkProvider = TronLinkProvider; + exports2.TronWebProvider = TronWebProvider; exports2.createProviderError = createProviderError; + exports2.getEvmChainId = getEvmChainId; + exports2.getKeyAppChainId = getKeyAppChainId; + exports2.initAllProviders = initAllProviders; exports2.initBioProvider = initBioProvider; + exports2.initEthereumProvider = initEthereumProvider; + exports2.initTronProvider = initTronProvider; + exports2.isEvmChain = isEvmChain; + exports2.normalizeChainId = normalizeChainId; + exports2.parseHexChainId = parseHexChainId; + exports2.toHexChainId = toHexChainId; Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" }); })); //# sourceMappingURL=index.umd.js.map diff --git a/packages/bio-sdk/dist/index.umd.js.map b/packages/bio-sdk/dist/index.umd.js.map index 19761307f..f3ad67750 100644 --- a/packages/bio-sdk/dist/index.umd.js.map +++ b/packages/bio-sdk/dist/index.umd.js.map @@ -1 +1 @@ -{"version":3,"file":"index.umd.js","sources":["../src/types.ts","../src/events.ts","../src/provider.ts","../src/index.ts"],"sourcesContent":["/**\n * Bio SDK Types\n * EIP-1193 style provider interface for Bio ecosystem\n */\n\n/** Account information */\nexport interface BioAccount {\n address: string\n chain: string\n name?: string\n /** Public key (hex encoded) */\n publicKey: string\n}\n\n/** Transfer parameters */\nexport interface TransferParams {\n from: string\n to: string\n amount: string\n chain: string\n asset?: string\n}\n\n/** Unsigned transaction payload (chain-specific) */\nexport interface BioUnsignedTransaction {\n chainId: string\n data: unknown\n}\n\n/** Signed transaction payload (chain-specific) */\nexport interface BioSignedTransaction {\n chainId: string\n data: unknown\n signature: string\n}\n\n/** Provider request arguments */\nexport interface RequestArguments {\n method: string\n params?: unknown[]\n}\n\n/** Provider RPC error */\nexport interface ProviderRpcError extends Error {\n code: number\n data?: unknown\n}\n\n/** Event handler type */\nexport type EventHandler = (...args: T[]) => void\n\n/**\n * Bio Provider Interface (EIP-1193 style)\n */\nexport interface BioProvider {\n /** Make a request to the provider */\n request(args: RequestArguments): Promise\n\n /** Subscribe to an event */\n on(event: string, handler: EventHandler): void\n\n /** Unsubscribe from an event */\n off(event: string, handler: EventHandler): void\n\n /** Check if connected */\n isConnected(): boolean\n}\n\n/**\n * Bio method definitions\n */\nexport interface BioMethods {\n /** Request wallet accounts (shows connection UI) */\n bio_requestAccounts: () => Promise\n\n /** Get connected accounts (no UI) */\n bio_accounts: () => Promise\n\n /** Select an account (shows account picker UI) */\n bio_selectAccount: (opts?: { chain?: string }) => Promise\n\n /** Pick another wallet address (shows wallet picker UI) */\n bio_pickWallet: (opts?: { chain?: string; exclude?: string }) => Promise\n\n /** Sign a message, returns signature and public key (hex) */\n bio_signMessage: (params: { message: string; address: string }) => Promise<{ signature: string; publicKey: string }>\n\n /** Sign typed data, returns signature and public key (hex) */\n bio_signTypedData: (params: { data: object; address: string }) => Promise<{ signature: string; publicKey: string }>\n\n /** Create an unsigned transaction (no signature, no broadcast) */\n bio_createTransaction: (params: TransferParams) => Promise\n\n /** Sign an unsigned transaction (requires user confirmation) */\n bio_signTransaction: (params: { from: string; chain: string; unsignedTx: BioUnsignedTransaction }) => Promise\n\n /** Send a transaction */\n bio_sendTransaction: (params: TransferParams) => Promise<{ txHash: string }>\n\n /** Get current chain ID */\n bio_chainId: () => Promise\n\n /** Get balance */\n bio_getBalance: (params: { address: string; chain: string }) => Promise\n\n /** Close splash screen (indicates app is ready) */\n bio_closeSplashScreen: () => Promise\n}\n\n/**\n * Bio event definitions\n */\nexport interface BioEvents {\n /** Emitted when accounts change */\n accountsChanged: (accounts: BioAccount[]) => void\n\n /** Emitted when chain changes */\n chainChanged: (chainId: string) => void\n\n /** Emitted when connected */\n connect: (info: { chainId: string }) => void\n\n /** Emitted when disconnected */\n disconnect: (error: { code: number; message: string }) => void\n}\n\n/** Method names */\nexport type BioMethodName = keyof BioMethods\n\n/** Event names */\nexport type BioEventName = keyof BioEvents\n\n/** RPC error codes */\nexport const BioErrorCodes = {\n USER_REJECTED: 4001,\n UNAUTHORIZED: 4100,\n UNSUPPORTED_METHOD: 4200,\n DISCONNECTED: 4900,\n CHAIN_DISCONNECTED: 4901,\n INTERNAL_ERROR: -32603,\n INVALID_PARAMS: -32602,\n METHOD_NOT_FOUND: -32601,\n} as const\n\n/** Create a provider RPC error */\nexport function createProviderError(code: number, message: string, data?: unknown): ProviderRpcError {\n const error = new Error(message) as ProviderRpcError\n error.code = code\n error.data = data\n return error\n}\n","/**\n * Event emitter for Bio SDK\n */\n\nimport type { EventHandler } from './types'\n\nexport class EventEmitter {\n private handlers = new Map>()\n\n on(event: string, handler: EventHandler): void {\n let handlers = this.handlers.get(event)\n if (!handlers) {\n handlers = new Set()\n this.handlers.set(event, handlers)\n }\n handlers.add(handler)\n }\n\n off(event: string, handler: EventHandler): void {\n const handlers = this.handlers.get(event)\n if (handlers) {\n handlers.delete(handler)\n if (handlers.size === 0) {\n this.handlers.delete(event)\n }\n }\n }\n\n emit(event: string, ...args: unknown[]): void {\n const handlers = this.handlers.get(event)\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n handler(...args)\n } catch (error) {\n console.error(`[BioSDK] Error in event handler for \"${event}\":`, error)\n }\n })\n }\n }\n\n removeAllListeners(event?: string): void {\n if (event) {\n this.handlers.delete(event)\n } else {\n this.handlers.clear()\n }\n }\n}\n","/**\n * Bio Provider Implementation\n * Communicates with KeyApp host via postMessage\n */\n\nimport type { BioProvider, RequestArguments, EventHandler } from './types'\nimport { BioErrorCodes, createProviderError } from './types'\nimport { EventEmitter } from './events'\n\n/** Message sent to host */\ninterface RequestMessage {\n type: 'bio_request'\n id: string\n method: string\n params?: unknown[]\n}\n\n/** Response from host */\ninterface ResponseMessage {\n type: 'bio_response'\n id: string\n success: boolean\n result?: unknown\n error?: { code: number; message: string; data?: unknown }\n}\n\n/** Event from host */\ninterface EventMessage {\n type: 'bio_event'\n event: string\n args: unknown[]\n}\n\ntype HostMessage = ResponseMessage | EventMessage\n\nexport class BioProviderImpl implements BioProvider {\n private events = new EventEmitter()\n private pendingRequests = new Map void\n reject: (error: Error) => void\n }>()\n private requestIdCounter = 0\n private connected = false\n private readonly targetOrigin: string\n\n constructor(targetOrigin = '*') {\n this.targetOrigin = targetOrigin\n this.setupMessageListener()\n this.connect()\n }\n\n private setupMessageListener(): void {\n window.addEventListener('message', this.handleMessage.bind(this))\n }\n\n private handleMessage(event: MessageEvent): void {\n const data = event.data as HostMessage\n if (!data || typeof data !== 'object') return\n\n if (data.type === 'bio_response') {\n this.handleResponse(data)\n } else if (data.type === 'bio_event') {\n this.handleEvent(data)\n }\n }\n\n private handleResponse(message: ResponseMessage): void {\n const pending = this.pendingRequests.get(message.id)\n if (!pending) return\n\n this.pendingRequests.delete(message.id)\n\n if (message.success) {\n pending.resolve(message.result)\n } else {\n const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: 'Unknown error' }\n pending.reject(createProviderError(error.code, error.message, error.data))\n }\n }\n\n private handleEvent(message: EventMessage): void {\n this.events.emit(message.event, ...message.args)\n\n // Handle built-in events\n if (message.event === 'connect') {\n this.connected = true\n } else if (message.event === 'disconnect') {\n this.connected = false\n }\n }\n\n private connect(): void {\n // Send handshake to host\n this.postMessage({\n type: 'bio_request',\n id: this.generateId(),\n method: 'bio_connect',\n params: [],\n })\n }\n\n private generateId(): string {\n return `bio_${Date.now()}_${++this.requestIdCounter}`\n }\n\n private postMessage(message: RequestMessage): void {\n if (window.parent === window) {\n console.warn('[BioSDK] Not running in iframe, cannot communicate with host')\n return\n }\n window.parent.postMessage(message, this.targetOrigin)\n }\n\n async request(args: RequestArguments): Promise {\n const id = this.generateId()\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, {\n resolve: resolve as (value: unknown) => void,\n reject,\n })\n\n this.postMessage({\n type: 'bio_request',\n id,\n method: args.method,\n params: args.params,\n })\n\n // Timeout after 5 minutes (for user interactions)\n setTimeout(() => {\n if (this.pendingRequests.has(id)) {\n this.pendingRequests.delete(id)\n reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, 'Request timeout'))\n }\n }, 5 * 60 * 1000)\n })\n }\n\n on(event: string, handler: EventHandler): void {\n this.events.on(event, handler)\n }\n\n off(event: string, handler: EventHandler): void {\n this.events.off(event, handler)\n }\n\n isConnected(): boolean {\n return this.connected\n }\n}\n","/**\n * Bio SDK - Client SDK for Bio Ecosystem MiniApps\n *\n * Injects `window.bio` provider similar to `window.ethereum` in Web3 DApps.\n *\n * @example\n * ```typescript\n * import '@biochain/bio-sdk'\n *\n * // Now window.bio is available\n * const accounts = await window.bio.request({ method: 'bio_requestAccounts' })\n * ```\n */\n\nimport { BioProviderImpl } from './provider'\nimport type { BioProvider } from './types'\n\n// Re-export types\nexport * from './types'\nexport { EventEmitter } from './events'\nexport { BioProviderImpl } from './provider'\n\n// Extend Window interface\ndeclare global {\n interface Window {\n bio?: BioProvider\n }\n}\n\n/**\n * Initialize and inject the Bio provider into window.bio\n */\nexport function initBioProvider(targetOrigin = '*'): BioProvider {\n if (typeof window === 'undefined') {\n throw new Error('[BioSDK] Cannot initialize: window is not defined')\n }\n\n if (window.bio) {\n console.warn('[BioSDK] Provider already exists, returning existing instance')\n return window.bio\n }\n\n const provider = new BioProviderImpl(targetOrigin)\n window.bio = provider\n\n console.log('[BioSDK] Provider initialized')\n return provider\n}\n\n// Auto-initialize if running in browser\nif (typeof window !== 'undefined') {\n // Use a slight delay to ensure DOM is ready\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => initBioProvider())\n } else {\n initBioProvider()\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.umd.js","sources":["../src/types.ts","../src/events.ts","../src/provider.ts","../src/ethereum-provider.ts","../src/tron-provider.ts","../src/chain-id.ts","../src/index.ts"],"sourcesContent":["/**\n * Bio SDK Types\n * EIP-1193 style provider interface for Bio ecosystem\n */\n\n/** Account information */\nexport interface BioAccount {\n address: string\n chain: string\n name?: string\n /** Public key (hex encoded) */\n publicKey: string\n}\n\n/** Transfer parameters */\nexport interface TransferParams {\n from: string\n to: string\n amount: string\n chain: string\n asset?: string\n}\n\n/** Unsigned transaction payload (chain-specific) */\nexport interface BioUnsignedTransaction {\n chainId: string\n data: unknown\n}\n\n/** Signed transaction payload (chain-specific) */\nexport interface BioSignedTransaction {\n chainId: string\n data: unknown\n signature: string\n}\n\n/** Provider request arguments */\nexport interface RequestArguments {\n method: string\n params?: unknown[]\n}\n\n/** Provider RPC error */\nexport interface ProviderRpcError extends Error {\n code: number\n data?: unknown\n}\n\n/** Event handler type */\nexport type EventHandler = (...args: T[]) => void\n\n/**\n * Bio Provider Interface (EIP-1193 style)\n */\nexport interface BioProvider {\n /** Make a request to the provider */\n request(args: RequestArguments): Promise\n\n /** Subscribe to an event */\n on(event: string, handler: EventHandler): void\n\n /** Unsubscribe from an event */\n off(event: string, handler: EventHandler): void\n\n /** Check if connected */\n isConnected(): boolean\n}\n\n/**\n * Bio method definitions\n */\nexport interface BioMethods {\n /** Request wallet accounts (shows connection UI) */\n bio_requestAccounts: () => Promise\n\n /** Get connected accounts (no UI) */\n bio_accounts: () => Promise\n\n /** Select an account (shows account picker UI) */\n bio_selectAccount: (opts?: { chain?: string }) => Promise\n\n /** Pick another wallet address (shows wallet picker UI) */\n bio_pickWallet: (opts?: { chain?: string; exclude?: string }) => Promise\n\n /** Sign a message, returns signature and public key (hex) */\n bio_signMessage: (params: { message: string; address: string }) => Promise<{ signature: string; publicKey: string }>\n\n /** Sign typed data, returns signature and public key (hex) */\n bio_signTypedData: (params: { data: object; address: string }) => Promise<{ signature: string; publicKey: string }>\n\n /** Create an unsigned transaction (no signature, no broadcast) */\n bio_createTransaction: (params: TransferParams) => Promise\n\n /** Sign an unsigned transaction (requires user confirmation) */\n bio_signTransaction: (params: { from: string; chain: string; unsignedTx: BioUnsignedTransaction }) => Promise\n\n /** Send a transaction */\n bio_sendTransaction: (params: TransferParams) => Promise<{ txHash: string }>\n\n /** Get current chain ID */\n bio_chainId: () => Promise\n\n /** Get balance */\n bio_getBalance: (params: { address: string; chain: string }) => Promise\n\n /** Close splash screen (indicates app is ready) */\n bio_closeSplashScreen: () => Promise\n}\n\n/**\n * Bio event definitions\n */\nexport interface BioEvents {\n /** Emitted when accounts change */\n accountsChanged: (accounts: BioAccount[]) => void\n\n /** Emitted when chain changes */\n chainChanged: (chainId: string) => void\n\n /** Emitted when connected */\n connect: (info: { chainId: string }) => void\n\n /** Emitted when disconnected */\n disconnect: (error: { code: number; message: string }) => void\n}\n\n/** Method names */\nexport type BioMethodName = keyof BioMethods\n\n/** Event names */\nexport type BioEventName = keyof BioEvents\n\n/** RPC error codes */\nexport const BioErrorCodes = {\n USER_REJECTED: 4001,\n UNAUTHORIZED: 4100,\n UNSUPPORTED_METHOD: 4200,\n DISCONNECTED: 4900,\n CHAIN_DISCONNECTED: 4901,\n INTERNAL_ERROR: -32603,\n INVALID_PARAMS: -32602,\n METHOD_NOT_FOUND: -32601,\n} as const\n\n/** Create a provider RPC error */\nexport function createProviderError(code: number, message: string, data?: unknown): ProviderRpcError {\n const error = new Error(message) as ProviderRpcError\n error.code = code\n error.data = data\n return error\n}\n","/**\n * Event emitter for Bio SDK\n */\n\nimport type { EventHandler } from './types'\n\nexport class EventEmitter {\n private handlers = new Map>()\n\n on(event: string, handler: EventHandler): void {\n let handlers = this.handlers.get(event)\n if (!handlers) {\n handlers = new Set()\n this.handlers.set(event, handlers)\n }\n handlers.add(handler)\n }\n\n off(event: string, handler: EventHandler): void {\n const handlers = this.handlers.get(event)\n if (handlers) {\n handlers.delete(handler)\n if (handlers.size === 0) {\n this.handlers.delete(event)\n }\n }\n }\n\n emit(event: string, ...args: unknown[]): void {\n const handlers = this.handlers.get(event)\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n handler(...args)\n } catch (error) {\n console.error(`[BioSDK] Error in event handler for \"${event}\":`, error)\n }\n })\n }\n }\n\n removeAllListeners(event?: string): void {\n if (event) {\n this.handlers.delete(event)\n } else {\n this.handlers.clear()\n }\n }\n}\n","/**\n * Bio Provider Implementation\n * Communicates with KeyApp host via postMessage\n */\n\nimport type { BioProvider, RequestArguments, EventHandler } from './types'\nimport { BioErrorCodes, createProviderError } from './types'\nimport { EventEmitter } from './events'\n\n/** Message sent to host */\ninterface RequestMessage {\n type: 'bio_request'\n id: string\n method: string\n params?: unknown[]\n}\n\n/** Response from host */\ninterface ResponseMessage {\n type: 'bio_response'\n id: string\n success: boolean\n result?: unknown\n error?: { code: number; message: string; data?: unknown }\n}\n\n/** Event from host */\ninterface EventMessage {\n type: 'bio_event'\n event: string\n args: unknown[]\n}\n\ntype HostMessage = ResponseMessage | EventMessage\n\nexport class BioProviderImpl implements BioProvider {\n private events = new EventEmitter()\n private pendingRequests = new Map void\n reject: (error: Error) => void\n }>()\n private requestIdCounter = 0\n private connected = false\n private readonly targetOrigin: string\n\n constructor(targetOrigin = '*') {\n this.targetOrigin = targetOrigin\n this.setupMessageListener()\n this.connect()\n }\n\n private setupMessageListener(): void {\n window.addEventListener('message', this.handleMessage.bind(this))\n }\n\n private handleMessage(event: MessageEvent): void {\n const data = event.data as HostMessage\n if (!data || typeof data !== 'object') return\n\n if (data.type === 'bio_response') {\n this.handleResponse(data)\n } else if (data.type === 'bio_event') {\n this.handleEvent(data)\n }\n }\n\n private handleResponse(message: ResponseMessage): void {\n const pending = this.pendingRequests.get(message.id)\n if (!pending) return\n\n this.pendingRequests.delete(message.id)\n\n if (message.success) {\n pending.resolve(message.result)\n } else {\n const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: 'Unknown error' }\n pending.reject(createProviderError(error.code, error.message, error.data))\n }\n }\n\n private handleEvent(message: EventMessage): void {\n this.events.emit(message.event, ...message.args)\n\n // Handle built-in events\n if (message.event === 'connect') {\n this.connected = true\n } else if (message.event === 'disconnect') {\n this.connected = false\n }\n }\n\n private connect(): void {\n // Send handshake to host\n this.postMessage({\n type: 'bio_request',\n id: this.generateId(),\n method: 'bio_connect',\n params: [],\n })\n }\n\n private generateId(): string {\n return `bio_${Date.now()}_${++this.requestIdCounter}`\n }\n\n private postMessage(message: RequestMessage): void {\n if (window.parent === window) {\n console.warn('[BioSDK] Not running in iframe, cannot communicate with host')\n return\n }\n window.parent.postMessage(message, this.targetOrigin)\n }\n\n async request(args: RequestArguments): Promise {\n const id = this.generateId()\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, {\n resolve: resolve as (value: unknown) => void,\n reject,\n })\n\n this.postMessage({\n type: 'bio_request',\n id,\n method: args.method,\n params: args.params,\n })\n\n // Timeout after 5 minutes (for user interactions)\n setTimeout(() => {\n if (this.pendingRequests.has(id)) {\n this.pendingRequests.delete(id)\n reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, 'Request timeout'))\n }\n }, 5 * 60 * 1000)\n })\n }\n\n on(event: string, handler: EventHandler): void {\n this.events.on(event, handler)\n }\n\n off(event: string, handler: EventHandler): void {\n this.events.off(event, handler)\n }\n\n isConnected(): boolean {\n return this.connected\n }\n}\n","/**\n * Ethereum Provider (EIP-1193 Compatible)\n *\n * Provides window.ethereum for EVM-compatible dApps.\n * Communicates with KeyApp host via postMessage.\n */\n\nimport { EventEmitter } from './events'\nimport { BioErrorCodes, createProviderError, type ProviderRpcError } from './types'\nimport { toHexChainId, parseHexChainId, getKeyAppChainId, EVM_CHAIN_IDS } from './chain-id'\n\n/** EIP-1193 Request Arguments */\nexport interface EthRequestArguments {\n method: string\n params?: unknown[] | Record\n}\n\n/** EIP-1193 Provider Connect Info */\nexport interface ProviderConnectInfo {\n chainId: string\n}\n\n/** EIP-1193 Provider Message */\nexport interface ProviderMessage {\n type: string\n data: unknown\n}\n\n/** Transaction request (eth_sendTransaction) */\nexport interface TransactionRequest {\n from: string\n to?: string\n value?: string\n data?: string\n gas?: string\n gasPrice?: string\n maxFeePerGas?: string\n maxPriorityFeePerGas?: string\n nonce?: string\n}\n\n/** Message sent to host */\ninterface RequestMessage {\n type: 'eth_request'\n id: string\n method: string\n params?: unknown[]\n}\n\n/** Response from host */\ninterface ResponseMessage {\n type: 'eth_response'\n id: string\n success: boolean\n result?: unknown\n error?: { code: number; message: string; data?: unknown }\n}\n\n/** Event from host */\ninterface EventMessage {\n type: 'eth_event'\n event: string\n args: unknown[]\n}\n\ntype HostMessage = ResponseMessage | EventMessage\n\n/**\n * EIP-1193 Ethereum Provider Implementation\n */\nexport class EthereumProvider {\n private events = new EventEmitter()\n private pendingRequests = new Map void\n reject: (error: Error) => void\n }>()\n private requestIdCounter = 0\n private connected = false\n private currentChainId: string | null = null\n private accounts: string[] = []\n private readonly targetOrigin: string\n\n // EIP-1193 required properties\n readonly isMetaMask = false\n readonly isKeyApp = true\n\n constructor(targetOrigin = '*') {\n this.targetOrigin = targetOrigin\n this.setupMessageListener()\n }\n\n private setupMessageListener(): void {\n window.addEventListener('message', this.handleMessage.bind(this))\n }\n\n private handleMessage(event: MessageEvent): void {\n const data = event.data as HostMessage\n if (!data || typeof data !== 'object') return\n\n if (data.type === 'eth_response') {\n this.handleResponse(data)\n } else if (data.type === 'eth_event') {\n this.handleEvent(data)\n }\n }\n\n private handleResponse(message: ResponseMessage): void {\n const pending = this.pendingRequests.get(message.id)\n if (!pending) return\n\n this.pendingRequests.delete(message.id)\n\n if (message.success) {\n pending.resolve(message.result)\n } else {\n const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: 'Unknown error' }\n pending.reject(createProviderError(error.code, error.message, error.data))\n }\n }\n\n private handleEvent(message: EventMessage): void {\n this.events.emit(message.event, ...message.args)\n\n // Handle built-in events\n if (message.event === 'connect') {\n this.connected = true\n const info = message.args[0] as ProviderConnectInfo\n this.currentChainId = info?.chainId ?? null\n } else if (message.event === 'disconnect') {\n this.connected = false\n this.accounts = []\n } else if (message.event === 'chainChanged') {\n this.currentChainId = message.args[0] as string\n } else if (message.event === 'accountsChanged') {\n this.accounts = message.args[0] as string[]\n }\n }\n\n private generateId(): string {\n return `eth_${Date.now()}_${++this.requestIdCounter}`\n }\n\n private postMessage(message: RequestMessage): void {\n if (window.parent === window) {\n console.warn('[EthereumProvider] Not running in iframe, cannot communicate with host')\n return\n }\n window.parent.postMessage(message, this.targetOrigin)\n }\n\n /**\n * EIP-1193 request method\n */\n async request(args: EthRequestArguments): Promise {\n const { method, params } = args\n const paramsArray = Array.isArray(params) ? params : params ? [params] : []\n\n const id = this.generateId()\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, {\n resolve: resolve as (value: unknown) => void,\n reject,\n })\n\n this.postMessage({\n type: 'eth_request',\n id,\n method,\n params: paramsArray,\n })\n\n // Timeout after 5 minutes (for user interactions)\n setTimeout(() => {\n if (this.pendingRequests.has(id)) {\n this.pendingRequests.delete(id)\n reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, 'Request timeout'))\n }\n }, 5 * 60 * 1000)\n })\n }\n\n /**\n * Subscribe to an event\n */\n on(event: string, handler: (...args: unknown[]) => void): this {\n this.events.on(event, handler)\n return this\n }\n\n /**\n * Unsubscribe from an event\n */\n off(event: string, handler: (...args: unknown[]) => void): this {\n this.events.off(event, handler)\n return this\n }\n\n /**\n * Alias for off (Node.js EventEmitter compatibility)\n */\n removeListener(event: string, handler: (...args: unknown[]) => void): this {\n return this.off(event, handler)\n }\n\n /**\n * Add listener that fires only once\n */\n once(event: string, handler: (...args: unknown[]) => void): this {\n const wrapper = (...args: unknown[]) => {\n this.off(event, wrapper)\n handler(...args)\n }\n this.on(event, wrapper)\n return this\n }\n\n /**\n * EIP-1193 isConnected method\n */\n isConnected(): boolean {\n return this.connected\n }\n\n /**\n * Get current chain ID (cached)\n */\n get chainId(): string | null {\n return this.currentChainId\n }\n\n /**\n * Get selected address (first account)\n */\n get selectedAddress(): string | null {\n return this.accounts[0] ?? null\n }\n\n // ============================================\n // Legacy methods (for backwards compatibility)\n // ============================================\n\n /**\n * @deprecated Use request({ method: 'eth_requestAccounts' })\n */\n async enable(): Promise {\n return this.request({ method: 'eth_requestAccounts' })\n }\n\n /**\n * @deprecated Use request()\n */\n send(method: string, params?: unknown[]): Promise {\n return this.request({ method, params })\n }\n\n /**\n * @deprecated Use request()\n */\n sendAsync(\n payload: { method: string; params?: unknown[]; id?: number },\n callback: (error: Error | null, result?: { result: unknown }) => void\n ): void {\n this.request({ method: payload.method, params: payload.params })\n .then((result) => callback(null, { result }))\n .catch((error) => callback(error))\n }\n}\n\n// Extend Window interface\ndeclare global {\n interface Window {\n ethereum?: EthereumProvider\n }\n}\n\n/**\n * Initialize and inject the Ethereum provider into window.ethereum\n */\nexport function initEthereumProvider(targetOrigin = '*'): EthereumProvider {\n if (typeof window === 'undefined') {\n throw new Error('[EthereumProvider] Cannot initialize: window is not defined')\n }\n\n if (window.ethereum) {\n console.warn('[EthereumProvider] Provider already exists, returning existing instance')\n return window.ethereum\n }\n\n const provider = new EthereumProvider(targetOrigin)\n window.ethereum = provider\n\n console.log('[EthereumProvider] Provider initialized')\n return provider\n}\n","/**\n * Tron Provider (TronLink Compatible)\n *\n * Provides window.tronWeb and window.tronLink for Tron dApps.\n * Communicates with KeyApp host via postMessage.\n */\n\nimport { EventEmitter } from './events'\nimport { BioErrorCodes, createProviderError } from './types'\n\n/** Tron address format */\nexport interface TronAddress {\n base58: string\n hex: string\n}\n\n/** TronLink request arguments (EIP-1193 style) */\nexport interface TronRequestArguments {\n method: string\n params?: unknown\n}\n\n/** Message sent to host */\ninterface RequestMessage {\n type: 'tron_request'\n id: string\n method: string\n params?: unknown[]\n}\n\n/** Response from host */\ninterface ResponseMessage {\n type: 'tron_response'\n id: string\n success: boolean\n result?: unknown\n error?: { code: number; message: string; data?: unknown }\n}\n\n/** Event from host */\ninterface EventMessage {\n type: 'tron_event'\n event: string\n args: unknown[]\n}\n\ntype HostMessage = ResponseMessage | EventMessage\n\n/**\n * TronLink-compatible Provider\n */\nexport class TronLinkProvider {\n private events = new EventEmitter()\n private pendingRequests = new Map void\n reject: (error: Error) => void\n }>()\n private requestIdCounter = 0\n private readonly targetOrigin: string\n\n constructor(targetOrigin = '*') {\n this.targetOrigin = targetOrigin\n this.setupMessageListener()\n }\n\n private setupMessageListener(): void {\n window.addEventListener('message', this.handleMessage.bind(this))\n }\n\n private handleMessage(event: MessageEvent): void {\n const data = event.data as HostMessage\n if (!data || typeof data !== 'object') return\n\n if (data.type === 'tron_response') {\n this.handleResponse(data)\n } else if (data.type === 'tron_event') {\n this.handleEvent(data)\n }\n }\n\n private handleResponse(message: ResponseMessage): void {\n const pending = this.pendingRequests.get(message.id)\n if (!pending) return\n\n this.pendingRequests.delete(message.id)\n\n if (message.success) {\n pending.resolve(message.result)\n } else {\n const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: 'Unknown error' }\n pending.reject(createProviderError(error.code, error.message, error.data))\n }\n }\n\n private handleEvent(message: EventMessage): void {\n this.events.emit(message.event, ...message.args)\n }\n\n private generateId(): string {\n return `tron_${Date.now()}_${++this.requestIdCounter}`\n }\n\n private postMessage(message: RequestMessage): void {\n if (window.parent === window) {\n console.warn('[TronLinkProvider] Not running in iframe, cannot communicate with host')\n return\n }\n window.parent.postMessage(message, this.targetOrigin)\n }\n\n /**\n * TronLink request method (EIP-1193 style)\n */\n async request(args: TronRequestArguments): Promise {\n const { method, params } = args\n const paramsArray = Array.isArray(params) ? params : params !== undefined ? [params] : []\n\n const id = this.generateId()\n\n return new Promise((resolve, reject) => {\n this.pendingRequests.set(id, {\n resolve: resolve as (value: unknown) => void,\n reject,\n })\n\n this.postMessage({\n type: 'tron_request',\n id,\n method,\n params: paramsArray,\n })\n\n // Timeout after 5 minutes\n setTimeout(() => {\n if (this.pendingRequests.has(id)) {\n this.pendingRequests.delete(id)\n reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, 'Request timeout'))\n }\n }, 5 * 60 * 1000)\n })\n }\n\n on(event: string, handler: (...args: unknown[]) => void): this {\n this.events.on(event, handler)\n return this\n }\n\n off(event: string, handler: (...args: unknown[]) => void): this {\n this.events.off(event, handler)\n return this\n }\n}\n\n/**\n * TronWeb-compatible API\n * Provides the subset of TronWeb API that KeyApp supports\n */\nexport class TronWebProvider {\n private tronLink: TronLinkProvider\n private _ready = false\n private _defaultAddress: TronAddress = { base58: '', hex: '' }\n\n /** TRX operations */\n readonly trx: TronWebTrx\n\n constructor(tronLink: TronLinkProvider) {\n this.tronLink = tronLink\n this.trx = new TronWebTrx(tronLink)\n\n // Listen for account changes\n tronLink.on('accountsChanged', (accounts: unknown) => {\n if (Array.isArray(accounts) && accounts.length > 0) {\n const addr = accounts[0] as TronAddress\n this._defaultAddress = addr\n this._ready = true\n } else {\n this._defaultAddress = { base58: '', hex: '' }\n this._ready = false\n }\n })\n }\n\n /** Whether TronWeb is ready (connected) */\n get ready(): boolean {\n return this._ready\n }\n\n /** Current default address */\n get defaultAddress(): TronAddress {\n return this._defaultAddress\n }\n\n /**\n * Set default address (called by host after connection)\n */\n setAddress(address: TronAddress): void {\n this._defaultAddress = address\n this._ready = true\n }\n\n /**\n * Check if an address is valid\n */\n isAddress(address: string): boolean {\n // Basic validation: base58 starts with T, hex starts with 41\n if (address.startsWith('T')) {\n return address.length === 34\n }\n if (address.startsWith('41')) {\n return address.length === 42\n }\n return false\n }\n\n /**\n * Convert address to hex format\n */\n address = {\n toHex: (base58: string): string => {\n // This is a stub - actual conversion requires TronWeb library\n // KeyApp will handle the conversion on the host side\n return base58\n },\n fromHex: (hex: string): string => {\n return hex\n },\n }\n}\n\n/**\n * TronWeb.trx operations\n */\nclass TronWebTrx {\n private tronLink: TronLinkProvider\n\n constructor(tronLink: TronLinkProvider) {\n this.tronLink = tronLink\n }\n\n /**\n * Sign a transaction\n */\n async sign(transaction: unknown): Promise {\n return this.tronLink.request({\n method: 'tron_signTransaction',\n params: transaction,\n })\n }\n\n /**\n * Send raw transaction (broadcast)\n */\n async sendRawTransaction(signedTransaction: unknown): Promise {\n return this.tronLink.request({\n method: 'tron_sendRawTransaction',\n params: signedTransaction,\n })\n }\n\n /**\n * Get account balance\n */\n async getBalance(address: string): Promise {\n return this.tronLink.request({\n method: 'tron_getBalance',\n params: address,\n })\n }\n\n /**\n * Get account info\n */\n async getAccount(address: string): Promise {\n return this.tronLink.request({\n method: 'tron_getAccount',\n params: address,\n })\n }\n}\n\n// Extend Window interface\ndeclare global {\n interface Window {\n tronLink?: TronLinkProvider\n tronWeb?: TronWebProvider\n }\n}\n\n/**\n * Initialize and inject the Tron providers\n */\nexport function initTronProvider(targetOrigin = '*'): { tronLink: TronLinkProvider; tronWeb: TronWebProvider } {\n if (typeof window === 'undefined') {\n throw new Error('[TronProvider] Cannot initialize: window is not defined')\n }\n\n if (window.tronLink && window.tronWeb) {\n console.warn('[TronProvider] Providers already exist, returning existing instances')\n return { tronLink: window.tronLink, tronWeb: window.tronWeb }\n }\n\n const tronLink = new TronLinkProvider(targetOrigin)\n const tronWeb = new TronWebProvider(tronLink)\n\n window.tronLink = tronLink\n window.tronWeb = tronWeb\n\n console.log('[TronProvider] Providers initialized')\n return { tronLink, tronWeb }\n}\n","/**\n * Chain ID utilities\n * Maps between KeyApp internal chain IDs and standard chain IDs\n */\n\n/** EVM Chain ID mapping (decimal) */\nexport const EVM_CHAIN_IDS: Record = {\n ethereum: 1,\n binance: 56,\n // Future chains\n // polygon: 137,\n // arbitrum: 42161,\n // optimism: 10,\n} as const\n\n/** Reverse mapping: EVM chainId -> KeyApp chain ID */\nexport const EVM_CHAIN_ID_TO_KEYAPP: Record = Object.fromEntries(\n Object.entries(EVM_CHAIN_IDS).map(([key, value]) => [value, key])\n)\n\n/** API chain name to KeyApp chain ID mapping */\nexport const API_CHAIN_TO_KEYAPP: Record = {\n ETH: 'ethereum',\n BSC: 'binance',\n TRON: 'tron',\n // Lowercase variants\n eth: 'ethereum',\n bsc: 'binance',\n tron: 'tron',\n} as const\n\n/** KeyApp chain ID to display name */\nexport const CHAIN_DISPLAY_NAMES: Record = {\n ethereum: 'Ethereum',\n binance: 'BNB Smart Chain',\n tron: 'Tron',\n bfmeta: 'BFMeta',\n bfchain: 'BFChain',\n} as const\n\n/**\n * Convert decimal chain ID to hex string (EIP-155 format)\n * @example toHexChainId(56) => '0x38'\n */\nexport function toHexChainId(chainId: number): string {\n return `0x${chainId.toString(16)}`\n}\n\n/**\n * Parse hex chain ID to decimal\n * @example parseHexChainId('0x38') => 56\n */\nexport function parseHexChainId(hexChainId: string): number {\n if (!hexChainId.startsWith('0x')) {\n throw new Error(`Invalid hex chain ID: ${hexChainId}`)\n }\n return parseInt(hexChainId, 16)\n}\n\n/**\n * Get KeyApp chain ID from EVM hex chain ID\n * @example getKeyAppChainId('0x38') => 'binance'\n */\nexport function getKeyAppChainId(hexChainId: string): string | null {\n const decimal = parseHexChainId(hexChainId)\n return EVM_CHAIN_ID_TO_KEYAPP[decimal] ?? null\n}\n\n/**\n * Get EVM hex chain ID from KeyApp chain ID\n * @example getEvmChainId('binance') => '0x38'\n */\nexport function getEvmChainId(keyAppChainId: string): string | null {\n const decimal = EVM_CHAIN_IDS[keyAppChainId]\n return decimal ? toHexChainId(decimal) : null\n}\n\n/**\n * Check if a chain is EVM compatible\n */\nexport function isEvmChain(chainId: string): boolean {\n return chainId in EVM_CHAIN_IDS\n}\n\n/**\n * Normalize API chain name to KeyApp chain ID\n * @example normalizeChainId('BSC') => 'binance'\n */\nexport function normalizeChainId(chainName: string): string {\n return API_CHAIN_TO_KEYAPP[chainName] ?? chainName.toLowerCase()\n}\n","/**\n * Bio SDK - Client SDK for Bio Ecosystem MiniApps\n *\n * Injects providers for multi-chain dApp support:\n * - `window.bio` - BioChain + KeyApp wallet features\n * - `window.ethereum` - EVM-compatible chains (ETH, BSC)\n * - `window.tronWeb` / `window.tronLink` - Tron chain\n *\n * @example\n * ```typescript\n * import '@biochain/bio-sdk'\n *\n * // BioChain operations\n * const accounts = await window.bio.request({ method: 'bio_requestAccounts' })\n *\n * // EVM operations (ETH, BSC)\n * const ethAccounts = await window.ethereum.request({ method: 'eth_requestAccounts' })\n *\n * // Tron operations\n * const tronAccounts = await window.tronLink.request({ method: 'tron_requestAccounts' })\n * ```\n */\n\nimport { BioProviderImpl } from './provider'\nimport { EthereumProvider, initEthereumProvider } from './ethereum-provider'\nimport { TronLinkProvider, TronWebProvider, initTronProvider } from './tron-provider'\nimport type { BioProvider } from './types'\n\n// Re-export types\nexport * from './types'\nexport * from './chain-id'\nexport { EventEmitter } from './events'\nexport { BioProviderImpl } from './provider'\nexport { EthereumProvider, initEthereumProvider } from './ethereum-provider'\nexport { TronLinkProvider, TronWebProvider, initTronProvider } from './tron-provider'\n\n// Extend Window interface (bio is declared in types.ts already for ethereum/tron)\ndeclare global {\n interface Window {\n bio?: BioProvider\n }\n}\n\n/**\n * Initialize and inject the Bio provider into window.bio\n */\nexport function initBioProvider(targetOrigin = '*'): BioProvider {\n if (typeof window === 'undefined') {\n throw new Error('[BioSDK] Cannot initialize: window is not defined')\n }\n\n if (window.bio) {\n console.warn('[BioSDK] Provider already exists, returning existing instance')\n return window.bio\n }\n\n const provider = new BioProviderImpl(targetOrigin)\n window.bio = provider\n\n console.log('[BioSDK] Provider initialized')\n return provider\n}\n\n/**\n * Initialize all providers (bio, ethereum, tron)\n */\nexport function initAllProviders(targetOrigin = '*'): {\n bio: BioProvider\n ethereum: EthereumProvider\n tronLink: TronLinkProvider\n tronWeb: TronWebProvider\n} {\n const bio = initBioProvider(targetOrigin)\n const ethereum = initEthereumProvider(targetOrigin)\n const { tronLink, tronWeb } = initTronProvider(targetOrigin)\n\n return { bio, ethereum, tronLink, tronWeb }\n}\n\n// Auto-initialize if running in browser\nif (typeof window !== 'undefined') {\n const init = () => {\n initBioProvider()\n initEthereumProvider()\n initTronProvider()\n }\n\n // Use a slight delay to ensure DOM is ready\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', init)\n } else {\n init()\n }\n}\n"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/bio-sdk/src/chain-id.test.ts b/packages/bio-sdk/src/chain-id.test.ts new file mode 100644 index 000000000..940966984 --- /dev/null +++ b/packages/bio-sdk/src/chain-id.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect } from 'vitest' +import { + toHexChainId, + parseHexChainId, + getKeyAppChainId, + getEvmChainId, + isEvmChain, + normalizeChainId, + EVM_CHAIN_IDS, +} from './chain-id' + +describe('chain-id utilities', () => { + describe('toHexChainId', () => { + it('should convert decimal to hex', () => { + expect(toHexChainId(1)).toBe('0x1') + expect(toHexChainId(56)).toBe('0x38') + expect(toHexChainId(137)).toBe('0x89') + }) + }) + + describe('parseHexChainId', () => { + it('should parse hex to decimal', () => { + expect(parseHexChainId('0x1')).toBe(1) + expect(parseHexChainId('0x38')).toBe(56) + expect(parseHexChainId('0x89')).toBe(137) + }) + + it('should throw for invalid hex', () => { + expect(() => parseHexChainId('38')).toThrow('Invalid hex chain ID') + expect(() => parseHexChainId('invalid')).toThrow('Invalid hex chain ID') + }) + }) + + describe('getKeyAppChainId', () => { + it('should map EVM hex chainId to KeyApp ID', () => { + expect(getKeyAppChainId('0x1')).toBe('ethereum') + expect(getKeyAppChainId('0x38')).toBe('binance') + }) + + it('should return null for unknown chainId', () => { + expect(getKeyAppChainId('0x999')).toBeNull() + }) + }) + + describe('getEvmChainId', () => { + it('should map KeyApp ID to EVM hex chainId', () => { + expect(getEvmChainId('ethereum')).toBe('0x1') + expect(getEvmChainId('binance')).toBe('0x38') + }) + + it('should return null for non-EVM chains', () => { + expect(getEvmChainId('bfmeta')).toBeNull() + expect(getEvmChainId('tron')).toBeNull() + }) + }) + + describe('isEvmChain', () => { + it('should return true for EVM chains', () => { + expect(isEvmChain('ethereum')).toBe(true) + expect(isEvmChain('binance')).toBe(true) + }) + + it('should return false for non-EVM chains', () => { + expect(isEvmChain('tron')).toBe(false) + expect(isEvmChain('bfmeta')).toBe(false) + }) + }) + + describe('normalizeChainId', () => { + it('should normalize API chain names to KeyApp IDs', () => { + expect(normalizeChainId('BSC')).toBe('binance') + expect(normalizeChainId('ETH')).toBe('ethereum') + expect(normalizeChainId('TRON')).toBe('tron') + }) + + it('should handle lowercase variants', () => { + expect(normalizeChainId('bsc')).toBe('binance') + expect(normalizeChainId('eth')).toBe('ethereum') + }) + + it('should return lowercase for unknown chains', () => { + expect(normalizeChainId('UNKNOWN')).toBe('unknown') + }) + }) + + describe('EVM_CHAIN_IDS', () => { + it('should contain expected chains', () => { + expect(EVM_CHAIN_IDS.ethereum).toBe(1) + expect(EVM_CHAIN_IDS.binance).toBe(56) + }) + }) +}) diff --git a/packages/bio-sdk/src/chain-id.ts b/packages/bio-sdk/src/chain-id.ts new file mode 100644 index 000000000..d9daa485b --- /dev/null +++ b/packages/bio-sdk/src/chain-id.ts @@ -0,0 +1,91 @@ +/** + * Chain ID utilities + * Maps between KeyApp internal chain IDs and standard chain IDs + */ + +/** EVM Chain ID mapping (decimal) */ +export const EVM_CHAIN_IDS: Record = { + ethereum: 1, + binance: 56, + // Future chains + // polygon: 137, + // arbitrum: 42161, + // optimism: 10, +} as const + +/** Reverse mapping: EVM chainId -> KeyApp chain ID */ +export const EVM_CHAIN_ID_TO_KEYAPP: Record = Object.fromEntries( + Object.entries(EVM_CHAIN_IDS).map(([key, value]) => [value, key]) +) + +/** API chain name to KeyApp chain ID mapping */ +export const API_CHAIN_TO_KEYAPP: Record = { + ETH: 'ethereum', + BSC: 'binance', + TRON: 'tron', + // Lowercase variants + eth: 'ethereum', + bsc: 'binance', + tron: 'tron', +} as const + +/** KeyApp chain ID to display name */ +export const CHAIN_DISPLAY_NAMES: Record = { + ethereum: 'Ethereum', + binance: 'BNB Smart Chain', + tron: 'Tron', + bfmeta: 'BFMeta', + bfchain: 'BFChain', +} as const + +/** + * Convert decimal chain ID to hex string (EIP-155 format) + * @example toHexChainId(56) => '0x38' + */ +export function toHexChainId(chainId: number): string { + return `0x${chainId.toString(16)}` +} + +/** + * Parse hex chain ID to decimal + * @example parseHexChainId('0x38') => 56 + */ +export function parseHexChainId(hexChainId: string): number { + if (!hexChainId.startsWith('0x')) { + throw new Error(`Invalid hex chain ID: ${hexChainId}`) + } + return parseInt(hexChainId, 16) +} + +/** + * Get KeyApp chain ID from EVM hex chain ID + * @example getKeyAppChainId('0x38') => 'binance' + */ +export function getKeyAppChainId(hexChainId: string): string | null { + const decimal = parseHexChainId(hexChainId) + return EVM_CHAIN_ID_TO_KEYAPP[decimal] ?? null +} + +/** + * Get EVM hex chain ID from KeyApp chain ID + * @example getEvmChainId('binance') => '0x38' + */ +export function getEvmChainId(keyAppChainId: string): string | null { + const decimal = EVM_CHAIN_IDS[keyAppChainId] + return decimal ? toHexChainId(decimal) : null +} + +/** + * Check if a chain is EVM compatible + */ +export function isEvmChain(chainId: string): boolean { + return chainId in EVM_CHAIN_IDS +} + +/** + * Normalize API chain name to KeyApp chain ID + * @example normalizeChainId('BSC') => 'binance' + */ +export function normalizeChainId(chainName: string): string { + return API_CHAIN_TO_KEYAPP[chainName] ?? chainName.toLowerCase() +} diff --git a/packages/bio-sdk/src/ethereum-provider.test.ts b/packages/bio-sdk/src/ethereum-provider.test.ts new file mode 100644 index 000000000..386eb6946 --- /dev/null +++ b/packages/bio-sdk/src/ethereum-provider.test.ts @@ -0,0 +1,169 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { EthereumProvider, initEthereumProvider } from './ethereum-provider' + +describe('EthereumProvider', () => { + let provider: EthereumProvider + let originalWindow: typeof window + + beforeEach(() => { + provider = new EthereumProvider('*') + // Save original window.ethereum + originalWindow = { ...window } + }) + + afterEach(() => { + // Restore window + if (window.ethereum) { + delete (window as { ethereum?: unknown }).ethereum + } + }) + + describe('constructor', () => { + it('should have isKeyApp property', () => { + expect(provider.isKeyApp).toBe(true) + }) + + it('should have isMetaMask as false', () => { + expect(provider.isMetaMask).toBe(false) + }) + + it('should start disconnected', () => { + expect(provider.isConnected()).toBe(false) + }) + + it('should have null chainId initially', () => { + expect(provider.chainId).toBeNull() + }) + + it('should have null selectedAddress initially', () => { + expect(provider.selectedAddress).toBeNull() + }) + }) + + describe('request', () => { + it('should return a promise', () => { + const result = provider.request({ method: 'eth_chainId' }) + expect(result).toBeInstanceOf(Promise) + }) + + it('should timeout after 5 minutes', async () => { + vi.useFakeTimers() + + const promise = provider.request({ method: 'eth_chainId' }) + + // Fast forward 5 minutes + vi.advanceTimersByTime(5 * 60 * 1000 + 100) + + await expect(promise).rejects.toThrow('Request timeout') + + vi.useRealTimers() + }) + }) + + describe('event emitter', () => { + it('should support on/off', () => { + const handler = vi.fn() + + provider.on('connect', handler) + provider.off('connect', handler) + + // Should not throw + expect(true).toBe(true) + }) + + it('should support once', () => { + const handler = vi.fn() + provider.once('connect', handler) + + // Should not throw + expect(true).toBe(true) + }) + + it('should support removeListener alias', () => { + const handler = vi.fn() + provider.on('connect', handler) + provider.removeListener('connect', handler) + + // Should not throw + expect(true).toBe(true) + }) + }) + + describe('legacy methods', () => { + it('should have enable method', () => { + expect(typeof provider.enable).toBe('function') + }) + + it('should have send method', () => { + expect(typeof provider.send).toBe('function') + }) + + it('should have sendAsync method', () => { + expect(typeof provider.sendAsync).toBe('function') + }) + }) + + describe('initEthereumProvider', () => { + it('should inject provider into window.ethereum', () => { + const injected = initEthereumProvider('*') + expect(window.ethereum).toBe(injected) + }) + + it('should return existing provider if already initialized', () => { + const first = initEthereumProvider('*') + const second = initEthereumProvider('*') + expect(first).toBe(second) + }) + }) + + describe('message handling', () => { + it('should handle connect event', () => { + const handler = vi.fn() + provider.on('connect', handler) + + // Simulate message from host + const event = new MessageEvent('message', { + data: { + type: 'eth_event', + event: 'connect', + args: [{ chainId: '0x38' }], + }, + }) + window.dispatchEvent(event) + + expect(handler).toHaveBeenCalledWith({ chainId: '0x38' }) + }) + + it('should handle chainChanged event', () => { + const handler = vi.fn() + provider.on('chainChanged', handler) + + const event = new MessageEvent('message', { + data: { + type: 'eth_event', + event: 'chainChanged', + args: ['0x1'], + }, + }) + window.dispatchEvent(event) + + expect(handler).toHaveBeenCalledWith('0x1') + }) + + it('should handle accountsChanged event', () => { + const handler = vi.fn() + provider.on('accountsChanged', handler) + + const event = new MessageEvent('message', { + data: { + type: 'eth_event', + event: 'accountsChanged', + args: [['0x1234...']], + }, + }) + window.dispatchEvent(event) + + expect(handler).toHaveBeenCalledWith(['0x1234...']) + }) + }) +}) diff --git a/packages/bio-sdk/src/ethereum-provider.ts b/packages/bio-sdk/src/ethereum-provider.ts new file mode 100644 index 000000000..eec8d4e44 --- /dev/null +++ b/packages/bio-sdk/src/ethereum-provider.ts @@ -0,0 +1,295 @@ +/** + * Ethereum Provider (EIP-1193 Compatible) + * + * Provides window.ethereum for EVM-compatible dApps. + * Communicates with KeyApp host via postMessage. + */ + +import { EventEmitter } from './events' +import { BioErrorCodes, createProviderError, type ProviderRpcError } from './types' +import { toHexChainId, parseHexChainId, getKeyAppChainId, EVM_CHAIN_IDS } from './chain-id' + +/** EIP-1193 Request Arguments */ +export interface EthRequestArguments { + method: string + params?: unknown[] | Record +} + +/** EIP-1193 Provider Connect Info */ +export interface ProviderConnectInfo { + chainId: string +} + +/** EIP-1193 Provider Message */ +export interface ProviderMessage { + type: string + data: unknown +} + +/** Transaction request (eth_sendTransaction) */ +export interface TransactionRequest { + from: string + to?: string + value?: string + data?: string + gas?: string + gasPrice?: string + maxFeePerGas?: string + maxPriorityFeePerGas?: string + nonce?: string +} + +/** Message sent to host */ +interface RequestMessage { + type: 'eth_request' + id: string + method: string + params?: unknown[] +} + +/** Response from host */ +interface ResponseMessage { + type: 'eth_response' + id: string + success: boolean + result?: unknown + error?: { code: number; message: string; data?: unknown } +} + +/** Event from host */ +interface EventMessage { + type: 'eth_event' + event: string + args: unknown[] +} + +type HostMessage = ResponseMessage | EventMessage + +/** + * EIP-1193 Ethereum Provider Implementation + */ +export class EthereumProvider { + private events = new EventEmitter() + private pendingRequests = new Map void + reject: (error: Error) => void + }>() + private requestIdCounter = 0 + private connected = false + private currentChainId: string | null = null + private accounts: string[] = [] + private readonly targetOrigin: string + + // EIP-1193 required properties + readonly isMetaMask = false + readonly isKeyApp = true + + constructor(targetOrigin = '*') { + this.targetOrigin = targetOrigin + this.setupMessageListener() + } + + private setupMessageListener(): void { + window.addEventListener('message', this.handleMessage.bind(this)) + } + + private handleMessage(event: MessageEvent): void { + const data = event.data as HostMessage + if (!data || typeof data !== 'object') return + + if (data.type === 'eth_response') { + this.handleResponse(data) + } else if (data.type === 'eth_event') { + this.handleEvent(data) + } + } + + private handleResponse(message: ResponseMessage): void { + const pending = this.pendingRequests.get(message.id) + if (!pending) return + + this.pendingRequests.delete(message.id) + + if (message.success) { + pending.resolve(message.result) + } else { + const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: 'Unknown error' } + pending.reject(createProviderError(error.code, error.message, error.data)) + } + } + + private handleEvent(message: EventMessage): void { + this.events.emit(message.event, ...message.args) + + // Handle built-in events + if (message.event === 'connect') { + this.connected = true + const info = message.args[0] as ProviderConnectInfo + this.currentChainId = info?.chainId ?? null + } else if (message.event === 'disconnect') { + this.connected = false + this.accounts = [] + } else if (message.event === 'chainChanged') { + this.currentChainId = message.args[0] as string + } else if (message.event === 'accountsChanged') { + this.accounts = message.args[0] as string[] + } + } + + private generateId(): string { + return `eth_${Date.now()}_${++this.requestIdCounter}` + } + + private postMessage(message: RequestMessage): void { + if (window.parent === window) { + console.warn('[EthereumProvider] Not running in iframe, cannot communicate with host') + return + } + window.parent.postMessage(message, this.targetOrigin) + } + + /** + * EIP-1193 request method + */ + async request(args: EthRequestArguments): Promise { + const { method, params } = args + const paramsArray = Array.isArray(params) ? params : params ? [params] : [] + + const id = this.generateId() + + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { + resolve: resolve as (value: unknown) => void, + reject, + }) + + this.postMessage({ + type: 'eth_request', + id, + method, + params: paramsArray, + }) + + // Timeout after 5 minutes (for user interactions) + setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id) + reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, 'Request timeout')) + } + }, 5 * 60 * 1000) + }) + } + + /** + * Subscribe to an event + */ + on(event: string, handler: (...args: unknown[]) => void): this { + this.events.on(event, handler) + return this + } + + /** + * Unsubscribe from an event + */ + off(event: string, handler: (...args: unknown[]) => void): this { + this.events.off(event, handler) + return this + } + + /** + * Alias for off (Node.js EventEmitter compatibility) + */ + removeListener(event: string, handler: (...args: unknown[]) => void): this { + return this.off(event, handler) + } + + /** + * Add listener that fires only once + */ + once(event: string, handler: (...args: unknown[]) => void): this { + const wrapper = (...args: unknown[]) => { + this.off(event, wrapper) + handler(...args) + } + this.on(event, wrapper) + return this + } + + /** + * EIP-1193 isConnected method + */ + isConnected(): boolean { + return this.connected + } + + /** + * Get current chain ID (cached) + */ + get chainId(): string | null { + return this.currentChainId + } + + /** + * Get selected address (first account) + */ + get selectedAddress(): string | null { + return this.accounts[0] ?? null + } + + // ============================================ + // Legacy methods (for backwards compatibility) + // ============================================ + + /** + * @deprecated Use request({ method: 'eth_requestAccounts' }) + */ + async enable(): Promise { + return this.request({ method: 'eth_requestAccounts' }) + } + + /** + * @deprecated Use request() + */ + send(method: string, params?: unknown[]): Promise { + return this.request({ method, params }) + } + + /** + * @deprecated Use request() + */ + sendAsync( + payload: { method: string; params?: unknown[]; id?: number }, + callback: (error: Error | null, result?: { result: unknown }) => void + ): void { + this.request({ method: payload.method, params: payload.params }) + .then((result) => callback(null, { result })) + .catch((error) => callback(error)) + } +} + +// Extend Window interface +declare global { + interface Window { + ethereum?: EthereumProvider + } +} + +/** + * Initialize and inject the Ethereum provider into window.ethereum + */ +export function initEthereumProvider(targetOrigin = '*'): EthereumProvider { + if (typeof window === 'undefined') { + throw new Error('[EthereumProvider] Cannot initialize: window is not defined') + } + + if (window.ethereum) { + console.warn('[EthereumProvider] Provider already exists, returning existing instance') + return window.ethereum + } + + const provider = new EthereumProvider(targetOrigin) + window.ethereum = provider + + console.log('[EthereumProvider] Provider initialized') + return provider +} diff --git a/packages/bio-sdk/src/index.ts b/packages/bio-sdk/src/index.ts index a9dfbf8f1..4d6e022e4 100644 --- a/packages/bio-sdk/src/index.ts +++ b/packages/bio-sdk/src/index.ts @@ -1,26 +1,40 @@ /** * Bio SDK - Client SDK for Bio Ecosystem MiniApps * - * Injects `window.bio` provider similar to `window.ethereum` in Web3 DApps. + * Injects providers for multi-chain dApp support: + * - `window.bio` - BioChain + KeyApp wallet features + * - `window.ethereum` - EVM-compatible chains (ETH, BSC) + * - `window.tronWeb` / `window.tronLink` - Tron chain * * @example * ```typescript * import '@biochain/bio-sdk' * - * // Now window.bio is available + * // BioChain operations * const accounts = await window.bio.request({ method: 'bio_requestAccounts' }) + * + * // EVM operations (ETH, BSC) + * const ethAccounts = await window.ethereum.request({ method: 'eth_requestAccounts' }) + * + * // Tron operations + * const tronAccounts = await window.tronLink.request({ method: 'tron_requestAccounts' }) * ``` */ import { BioProviderImpl } from './provider' +import { EthereumProvider, initEthereumProvider } from './ethereum-provider' +import { TronLinkProvider, TronWebProvider, initTronProvider } from './tron-provider' import type { BioProvider } from './types' // Re-export types export * from './types' +export * from './chain-id' export { EventEmitter } from './events' export { BioProviderImpl } from './provider' +export { EthereumProvider, initEthereumProvider } from './ethereum-provider' +export { TronLinkProvider, TronWebProvider, initTronProvider } from './tron-provider' -// Extend Window interface +// Extend Window interface (bio is declared in types.ts already for ethereum/tron) declare global { interface Window { bio?: BioProvider @@ -47,12 +61,34 @@ export function initBioProvider(targetOrigin = '*'): BioProvider { return provider } +/** + * Initialize all providers (bio, ethereum, tron) + */ +export function initAllProviders(targetOrigin = '*'): { + bio: BioProvider + ethereum: EthereumProvider + tronLink: TronLinkProvider + tronWeb: TronWebProvider +} { + const bio = initBioProvider(targetOrigin) + const ethereum = initEthereumProvider(targetOrigin) + const { tronLink, tronWeb } = initTronProvider(targetOrigin) + + return { bio, ethereum, tronLink, tronWeb } +} + // Auto-initialize if running in browser if (typeof window !== 'undefined') { + const init = () => { + initBioProvider() + initEthereumProvider() + initTronProvider() + } + // Use a slight delay to ensure DOM is ready if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => initBioProvider()) + document.addEventListener('DOMContentLoaded', init) } else { - initBioProvider() + init() } } diff --git a/packages/bio-sdk/src/tron-provider.test.ts b/packages/bio-sdk/src/tron-provider.test.ts new file mode 100644 index 000000000..65f05dae7 --- /dev/null +++ b/packages/bio-sdk/src/tron-provider.test.ts @@ -0,0 +1,164 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { TronLinkProvider, TronWebProvider, initTronProvider } from './tron-provider' + +describe('TronLinkProvider', () => { + let provider: TronLinkProvider + + beforeEach(() => { + provider = new TronLinkProvider('*') + }) + + describe('request', () => { + it('should return a promise', () => { + const result = provider.request({ method: 'tron_requestAccounts' }) + expect(result).toBeInstanceOf(Promise) + }) + + it('should timeout after 5 minutes', async () => { + vi.useFakeTimers() + + const promise = provider.request({ method: 'tron_requestAccounts' }) + + // Fast forward 5 minutes + vi.advanceTimersByTime(5 * 60 * 1000 + 100) + + await expect(promise).rejects.toThrow('Request timeout') + + vi.useRealTimers() + }) + }) + + describe('event emitter', () => { + it('should support on/off', () => { + const handler = vi.fn() + + provider.on('accountsChanged', handler) + provider.off('accountsChanged', handler) + + expect(true).toBe(true) + }) + }) + + describe('message handling', () => { + it('should handle tron_response', () => { + const requestPromise = provider.request({ method: 'tron_accounts' }) + + // Get the request ID from pending requests (internal) + // We'll simulate a response + const event = new MessageEvent('message', { + data: { + type: 'tron_response', + id: 'tron_1_1', // This won't match, but tests the handler path + success: true, + result: { base58: 'T...', hex: '41...' }, + }, + }) + window.dispatchEvent(event) + + // Request will still timeout since ID doesn't match + // This just tests that the handler doesn't throw + expect(true).toBe(true) + }) + + it('should handle tron_event', () => { + const handler = vi.fn() + provider.on('accountsChanged', handler) + + const event = new MessageEvent('message', { + data: { + type: 'tron_event', + event: 'accountsChanged', + args: [{ base58: 'T...', hex: '41...' }], + }, + }) + window.dispatchEvent(event) + + expect(handler).toHaveBeenCalled() + }) + }) +}) + +describe('TronWebProvider', () => { + let tronLink: TronLinkProvider + let tronWeb: TronWebProvider + + beforeEach(() => { + tronLink = new TronLinkProvider('*') + tronWeb = new TronWebProvider(tronLink) + }) + + describe('initial state', () => { + it('should not be ready initially', () => { + expect(tronWeb.ready).toBe(false) + }) + + it('should have empty default address', () => { + expect(tronWeb.defaultAddress).toEqual({ base58: '', hex: '' }) + }) + }) + + describe('setAddress', () => { + it('should set address and mark as ready', () => { + tronWeb.setAddress({ base58: 'TAddr...', hex: '41...' }) + + expect(tronWeb.ready).toBe(true) + expect(tronWeb.defaultAddress.base58).toBe('TAddr...') + }) + }) + + describe('isAddress', () => { + it('should validate base58 address', () => { + expect(tronWeb.isAddress('T' + 'x'.repeat(33))).toBe(true) + expect(tronWeb.isAddress('invalid')).toBe(false) + }) + + it('should validate hex address', () => { + expect(tronWeb.isAddress('41' + 'x'.repeat(40))).toBe(true) + expect(tronWeb.isAddress('00' + 'x'.repeat(40))).toBe(false) + }) + }) + + describe('trx methods', () => { + it('should have sign method', () => { + expect(typeof tronWeb.trx.sign).toBe('function') + }) + + it('should have sendRawTransaction method', () => { + expect(typeof tronWeb.trx.sendRawTransaction).toBe('function') + }) + + it('should have getBalance method', () => { + expect(typeof tronWeb.trx.getBalance).toBe('function') + }) + + it('should have getAccount method', () => { + expect(typeof tronWeb.trx.getAccount).toBe('function') + }) + }) +}) + +describe('initTronProvider', () => { + afterEach(() => { + if (window.tronLink) { + delete (window as { tronLink?: unknown }).tronLink + } + if (window.tronWeb) { + delete (window as { tronWeb?: unknown }).tronWeb + } + }) + + it('should inject providers into window', () => { + const { tronLink, tronWeb } = initTronProvider('*') + + expect(window.tronLink).toBe(tronLink) + expect(window.tronWeb).toBe(tronWeb) + }) + + it('should return existing providers if already initialized', () => { + const first = initTronProvider('*') + const second = initTronProvider('*') + + expect(first.tronLink).toBe(second.tronLink) + expect(first.tronWeb).toBe(second.tronWeb) + }) +}) diff --git a/packages/bio-sdk/src/tron-provider.ts b/packages/bio-sdk/src/tron-provider.ts new file mode 100644 index 000000000..5539815d6 --- /dev/null +++ b/packages/bio-sdk/src/tron-provider.ts @@ -0,0 +1,310 @@ +/** + * Tron Provider (TronLink Compatible) + * + * Provides window.tronWeb and window.tronLink for Tron dApps. + * Communicates with KeyApp host via postMessage. + */ + +import { EventEmitter } from './events' +import { BioErrorCodes, createProviderError } from './types' + +/** Tron address format */ +export interface TronAddress { + base58: string + hex: string +} + +/** TronLink request arguments (EIP-1193 style) */ +export interface TronRequestArguments { + method: string + params?: unknown +} + +/** Message sent to host */ +interface RequestMessage { + type: 'tron_request' + id: string + method: string + params?: unknown[] +} + +/** Response from host */ +interface ResponseMessage { + type: 'tron_response' + id: string + success: boolean + result?: unknown + error?: { code: number; message: string; data?: unknown } +} + +/** Event from host */ +interface EventMessage { + type: 'tron_event' + event: string + args: unknown[] +} + +type HostMessage = ResponseMessage | EventMessage + +/** + * TronLink-compatible Provider + */ +export class TronLinkProvider { + private events = new EventEmitter() + private pendingRequests = new Map void + reject: (error: Error) => void + }>() + private requestIdCounter = 0 + private readonly targetOrigin: string + + constructor(targetOrigin = '*') { + this.targetOrigin = targetOrigin + this.setupMessageListener() + } + + private setupMessageListener(): void { + window.addEventListener('message', this.handleMessage.bind(this)) + } + + private handleMessage(event: MessageEvent): void { + const data = event.data as HostMessage + if (!data || typeof data !== 'object') return + + if (data.type === 'tron_response') { + this.handleResponse(data) + } else if (data.type === 'tron_event') { + this.handleEvent(data) + } + } + + private handleResponse(message: ResponseMessage): void { + const pending = this.pendingRequests.get(message.id) + if (!pending) return + + this.pendingRequests.delete(message.id) + + if (message.success) { + pending.resolve(message.result) + } else { + const error = message.error ?? { code: BioErrorCodes.INTERNAL_ERROR, message: 'Unknown error' } + pending.reject(createProviderError(error.code, error.message, error.data)) + } + } + + private handleEvent(message: EventMessage): void { + this.events.emit(message.event, ...message.args) + } + + private generateId(): string { + return `tron_${Date.now()}_${++this.requestIdCounter}` + } + + private postMessage(message: RequestMessage): void { + if (window.parent === window) { + console.warn('[TronLinkProvider] Not running in iframe, cannot communicate with host') + return + } + window.parent.postMessage(message, this.targetOrigin) + } + + /** + * TronLink request method (EIP-1193 style) + */ + async request(args: TronRequestArguments): Promise { + const { method, params } = args + const paramsArray = Array.isArray(params) ? params : params !== undefined ? [params] : [] + + const id = this.generateId() + + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { + resolve: resolve as (value: unknown) => void, + reject, + }) + + this.postMessage({ + type: 'tron_request', + id, + method, + params: paramsArray, + }) + + // Timeout after 5 minutes + setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id) + reject(createProviderError(BioErrorCodes.INTERNAL_ERROR, 'Request timeout')) + } + }, 5 * 60 * 1000) + }) + } + + on(event: string, handler: (...args: unknown[]) => void): this { + this.events.on(event, handler) + return this + } + + off(event: string, handler: (...args: unknown[]) => void): this { + this.events.off(event, handler) + return this + } +} + +/** + * TronWeb-compatible API + * Provides the subset of TronWeb API that KeyApp supports + */ +export class TronWebProvider { + private tronLink: TronLinkProvider + private _ready = false + private _defaultAddress: TronAddress = { base58: '', hex: '' } + + /** TRX operations */ + readonly trx: TronWebTrx + + constructor(tronLink: TronLinkProvider) { + this.tronLink = tronLink + this.trx = new TronWebTrx(tronLink) + + // Listen for account changes + tronLink.on('accountsChanged', (accounts: unknown) => { + if (Array.isArray(accounts) && accounts.length > 0) { + const addr = accounts[0] as TronAddress + this._defaultAddress = addr + this._ready = true + } else { + this._defaultAddress = { base58: '', hex: '' } + this._ready = false + } + }) + } + + /** Whether TronWeb is ready (connected) */ + get ready(): boolean { + return this._ready + } + + /** Current default address */ + get defaultAddress(): TronAddress { + return this._defaultAddress + } + + /** + * Set default address (called by host after connection) + */ + setAddress(address: TronAddress): void { + this._defaultAddress = address + this._ready = true + } + + /** + * Check if an address is valid + */ + isAddress(address: string): boolean { + // Basic validation: base58 starts with T, hex starts with 41 + if (address.startsWith('T')) { + return address.length === 34 + } + if (address.startsWith('41')) { + return address.length === 42 + } + return false + } + + /** + * Convert address to hex format + */ + address = { + toHex: (base58: string): string => { + // This is a stub - actual conversion requires TronWeb library + // KeyApp will handle the conversion on the host side + return base58 + }, + fromHex: (hex: string): string => { + return hex + }, + } +} + +/** + * TronWeb.trx operations + */ +class TronWebTrx { + private tronLink: TronLinkProvider + + constructor(tronLink: TronLinkProvider) { + this.tronLink = tronLink + } + + /** + * Sign a transaction + */ + async sign(transaction: unknown): Promise { + return this.tronLink.request({ + method: 'tron_signTransaction', + params: transaction, + }) + } + + /** + * Send raw transaction (broadcast) + */ + async sendRawTransaction(signedTransaction: unknown): Promise { + return this.tronLink.request({ + method: 'tron_sendRawTransaction', + params: signedTransaction, + }) + } + + /** + * Get account balance + */ + async getBalance(address: string): Promise { + return this.tronLink.request({ + method: 'tron_getBalance', + params: address, + }) + } + + /** + * Get account info + */ + async getAccount(address: string): Promise { + return this.tronLink.request({ + method: 'tron_getAccount', + params: address, + }) + } +} + +// Extend Window interface +declare global { + interface Window { + tronLink?: TronLinkProvider + tronWeb?: TronWebProvider + } +} + +/** + * Initialize and inject the Tron providers + */ +export function initTronProvider(targetOrigin = '*'): { tronLink: TronLinkProvider; tronWeb: TronWebProvider } { + if (typeof window === 'undefined') { + throw new Error('[TronProvider] Cannot initialize: window is not defined') + } + + if (window.tronLink && window.tronWeb) { + console.warn('[TronProvider] Providers already exist, returning existing instances') + return { tronLink: window.tronLink, tronWeb: window.tronWeb } + } + + const tronLink = new TronLinkProvider(targetOrigin) + const tronWeb = new TronWebProvider(tronLink) + + window.tronLink = tronLink + window.tronWeb = tronWeb + + console.log('[TronProvider] Providers initialized') + return { tronLink, tronWeb } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e598ea18..88cabe860 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@bfmeta/sign-util': specifier: ^1.3.10 version: 1.3.10 + '@biochain/bio-sdk': + specifier: workspace:* + version: link:packages/bio-sdk '@biochain/plugin-navigation-sync': specifier: workspace:* version: link:packages/plugin-navigation-sync diff --git a/src/components/ecosystem/index.ts b/src/components/ecosystem/index.ts index 5454f6074..1c60e95ba 100644 --- a/src/components/ecosystem/index.ts +++ b/src/components/ecosystem/index.ts @@ -81,3 +81,8 @@ export { type EcosystemDesktopCallbacks, type EcosystemDesktopHandle, } from './ecosystem-desktop' + +export { + MiniappSheetHeader, + type MiniappSheetHeaderProps, +} from './miniapp-sheet-header' diff --git a/src/components/ecosystem/miniapp-sheet-header.tsx b/src/components/ecosystem/miniapp-sheet-header.tsx new file mode 100644 index 000000000..65cff19a1 --- /dev/null +++ b/src/components/ecosystem/miniapp-sheet-header.tsx @@ -0,0 +1,30 @@ +/** + * MiniappSheetHeader - 统一的小程序 Sheet 页头组件 + * + * 用于 WalletPickerJob、ChainSwitchConfirmJob、SigningConfirmJob 等场景 + */ + +import { MiniappIcon } from './miniapp-icon' + +export type MiniappSheetHeaderProps = { + /** Sheet 标题 */ + title: string + /** 描述文本 (可选) */ + description?: string + /** 来源小程序名称 */ + appName?: string + /** 来源小程序图标 URL */ + appIcon?: string +} + +export function MiniappSheetHeader({ title, description, appName, appIcon }: MiniappSheetHeaderProps) { + return ( +
+
+ +
+

{title}

+ {description &&

{description}

} +
+ ) +} diff --git a/src/services/ecosystem/bridge.ts b/src/services/ecosystem/bridge.ts index 000a99325..ae7c4dade 100644 --- a/src/services/ecosystem/bridge.ts +++ b/src/services/ecosystem/bridge.ts @@ -9,6 +9,61 @@ import type { MethodHandler, HandlerContext, } from './types' +import { miniappRuntimeStore } from '../miniapp-runtime' + +/** EVM request message from miniapp */ +interface EthRequestMessage { + type: 'eth_request' + id: string + method: string + params?: unknown[] +} + +/** EVM response message to miniapp */ +interface EthResponseMessage { + type: 'eth_response' + id: string + success: boolean + result?: unknown + error?: { code: number; message: string; data?: unknown } +} + +/** EVM event message to miniapp */ +interface EthEventMessage { + type: 'eth_event' + event: string + args: unknown[] +} + +/** TRON request message from miniapp */ +interface TronRequestMessage { + type: 'tron_request' + id: string + method: string + params?: unknown[] +} + +/** TRON response message to miniapp */ +interface TronResponseMessage { + type: 'tron_response' + id: string + success: boolean + result?: unknown + error?: { code: number; message: string; data?: unknown } +} + +/** TRON event message to miniapp */ +interface TronEventMessage { + type: 'tron_event' + event: string + args: unknown[] +} + +/** All supported request types */ +type RequestMessage = BioRequestMessage | EthRequestMessage | TronRequestMessage + +/** Protocol type */ +type Protocol = 'bio' | 'eth' | 'tron' import { BioErrorCodes, createErrorResponse, @@ -87,12 +142,27 @@ export class PostMessageBridge { this.manifestPermissions = [] } - /** Send event to miniapp */ + /** Send event to miniapp (Bio protocol) */ emit(event: string, ...args: unknown[]): void { + this.emitTo('bio', event, ...args) + } + + /** Send event to miniapp (EVM protocol) */ + emitEth(event: string, ...args: unknown[]): void { + this.emitTo('eth', event, ...args) + } + + /** Send event to miniapp (TRON protocol) */ + emitTron(event: string, ...args: unknown[]): void { + this.emitTo('tron', event, ...args) + } + + /** Send event to specific protocol */ + private emitTo(protocol: Protocol, event: string, ...args: unknown[]): void { if (!this.iframe?.contentWindow) return - const message: BioEventMessage = { - type: 'bio_event', + const message: BioEventMessage | EthEventMessage | TronEventMessage = { + type: `${protocol}_event` as 'bio_event' | 'eth_event' | 'tron_event', event, args, } @@ -111,32 +181,40 @@ export class PostMessageBridge { return } - const data = event.data as BioRequestMessage - if (!data || data.type !== 'bio_request') { + const data = event.data as RequestMessage + if (!data || typeof data !== 'object' || !('type' in data)) { return } - this.processRequest(data) + // Route to appropriate handler based on protocol + if (data.type === 'bio_request') { + this.processRequest(data, 'bio') + } else if (data.type === 'eth_request') { + this.processRequest(data as EthRequestMessage, 'eth') + } else if (data.type === 'tron_request') { + this.processRequest(data as TronRequestMessage, 'tron') + } } - private async processRequest(request: BioRequestMessage): Promise { + private async processRequest(request: RequestMessage, protocol: Protocol): Promise { const { id, method, params } = request - console.log('[BioProvider] Request:', method, params) + console.log(`[BioProvider] ${protocol.toUpperCase()} Request:`, method, params) // Check if handler exists const handler = this.handlers.get(method) if (!handler) { - this.sendResponse(createErrorResponse( + this.sendResponse(protocol, { + type: `${protocol}_response` as 'bio_response', id, - BioErrorCodes.METHOD_NOT_FOUND, - `Method not found: ${method}` - )) + success: false, + error: { code: BioErrorCodes.METHOD_NOT_FOUND, message: `Method not found: ${method}` }, + }) return } - // Check permissions - const skipPermissionCheck = ['bio_connect', 'bio_closeSplashScreen'].includes(method) + // Permission check (skip for EVM/TRON - they use their own connection flow) + const skipPermissionCheck = protocol !== 'bio' || ['bio_connect', 'bio_closeSplashScreen'].includes(method) if (!skipPermissionCheck) { const accountRelatedMethods = ['bio_accounts', 'bio_selectAccount', 'bio_pickWallet'] const shouldMapToRequestAccounts = @@ -148,11 +226,7 @@ export class PostMessageBridge { this.manifestPermissions.includes(method) || method === 'bio_requestAccounts' || shouldMapToRequestAccounts if (!isDeclaredInManifest) { - this.sendResponse(createErrorResponse( - id, - BioErrorCodes.UNAUTHORIZED, - `Permission not declared in manifest: ${method}` - )) + this.sendResponse(protocol, createErrorResponse(id, BioErrorCodes.UNAUTHORIZED, `Permission not declared in manifest: ${method}`)) return } @@ -163,11 +237,7 @@ export class PostMessageBridge { // 请求权限 const approved = await this.requestPermission([permissionKey]) if (!approved) { - this.sendResponse(createErrorResponse( - id, - BioErrorCodes.USER_REJECTED, - 'Permission denied by user' - )) + this.sendResponse(protocol, createErrorResponse(id, BioErrorCodes.USER_REJECTED, 'Permission denied by user')) return } } @@ -176,19 +246,31 @@ export class PostMessageBridge { // Execute handler try { + const appIcon = miniappRuntimeStore.state.apps.get(this.appId)?.manifest.icon const context: HandlerContext = { appId: this.appId, appName: this.appName, + appIcon, origin: this.origin, permissions: this.manifestPermissions, } const result = await handler(params?.[0], context) - this.sendResponse(createSuccessResponse(id, result)) + this.sendResponse(protocol, { + type: `${protocol}_response` as 'bio_response', + id, + success: true, + result, + }) } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' const code = (error as { code?: number }).code ?? BioErrorCodes.INTERNAL_ERROR - this.sendResponse(createErrorResponse(id, code, message)) + this.sendResponse(protocol, { + type: `${protocol}_response` as 'bio_response', + id, + success: false, + error: { code, message }, + }) } } @@ -215,10 +297,10 @@ export class PostMessageBridge { } } - private sendResponse(response: BioResponseMessage): void { + private sendResponse(protocol: Protocol, response: BioResponseMessage | EthResponseMessage | TronResponseMessage): void { if (!this.iframe?.contentWindow) return - console.log('[BioProvider] Response:', response) + console.log(`[BioProvider] ${protocol.toUpperCase()} Response:`, response) this.iframe.contentWindow.postMessage(response, this.origin) } } diff --git a/src/services/ecosystem/handlers/context.ts b/src/services/ecosystem/handlers/context.ts index f157d27ae..0e602d547 100644 --- a/src/services/ecosystem/handlers/context.ts +++ b/src/services/ecosystem/handlers/context.ts @@ -5,6 +5,28 @@ import type { BioAccount, BioSignedTransaction, BioUnsignedTransaction, TransferParams } from '../types' +/** EVM 交易请求类型 */ +export interface EvmTransactionRequest { + from: string + to?: string + value?: string + data?: string + gas?: string + gasPrice?: string + maxFeePerGas?: string + maxPriorityFeePerGas?: string + nonce?: string + chainId?: string +} + +/** TRON 交易类型 */ +export interface TronTransaction { + txID: string + raw_data: unknown + raw_data_hex: string + signature?: string[] +} + /** 小程序信息(用于在 Sheet 中显示) */ export interface MiniappInfo { name: string @@ -32,13 +54,42 @@ export interface SignTransactionParams { app: MiniappInfo } +/** EVM 签名参数 */ +export interface EvmSigningParams { + message: string + address: string + appName: string +} + +/** EVM 交易参数 */ +export interface EvmTransactionParams { + tx: EvmTransactionRequest + appName: string +} + +/** TRON 签名参数 */ +export interface TronSigningParams { + transaction: TronTransaction + appName: string +} + /** Handler 回调接口 */ export interface HandlerCallbacks { + // Bio (BioChain) callbacks showWalletPicker: (opts?: { chain?: string; exclude?: string; app?: MiniappInfo }) => Promise getConnectedAccounts: () => BioAccount[] showSigningDialog: (params: SigningParams) => Promise showTransferDialog: (params: TransferParams & { app: MiniappInfo }) => Promise<{ txHash: string } | null> showSignTransactionDialog: (params: SignTransactionParams) => Promise + + // EVM (Ethereum/BSC) callbacks + showEvmWalletPicker?: (opts: { chainId: string; app?: MiniappInfo }) => Promise + showEvmSigningDialog?: (params: EvmSigningParams) => Promise<{ signature: string } | null> + showEvmTransactionDialog?: (params: EvmTransactionParams) => Promise<{ txHash: string } | null> + + // TRON callbacks + showTronWalletPicker?: (opts?: { app?: MiniappInfo }) => Promise + showTronSigningDialog?: (params: TronSigningParams) => Promise<{ signedTransaction: TronTransaction } | null> } /** 回调注册表 */ diff --git a/src/services/ecosystem/handlers/evm.ts b/src/services/ecosystem/handlers/evm.ts new file mode 100644 index 000000000..afbfa7f5e --- /dev/null +++ b/src/services/ecosystem/handlers/evm.ts @@ -0,0 +1,355 @@ +/** + * EVM (Ethereum/BSC) method handlers + * + * Handles window.ethereum requests from miniapps. + * Maps EIP-1193 methods to KeyApp wallet operations. + */ + +import type { MethodHandler, BioAccount } from '../types' +import { BioErrorCodes } from '../types' +import { HandlerContext } from './context' +import { + toHexChainId, + parseHexChainId, + getKeyAppChainId, + getEvmChainId, + EVM_CHAIN_IDS, +} from '@biochain/bio-sdk' + +import type { EvmTransactionRequest, MiniappInfo } from './context' + +// Re-export for convenience +export type { EvmTransactionRequest } from './context' + +// ============================================ +// Types +// ============================================ + +/** EVM Sign Request */ +export interface EvmSignRequest { + address: string + message: string +} + +/** Typed Data (EIP-712) */ +export interface TypedDataV4 { + types: Record> + primaryType: string + domain: { + name?: string + version?: string + chainId?: number + verifyingContract?: string + salt?: string + } + message: Record +} + +// ============================================ +// State +// ============================================ + +/** Current selected chain for each app */ +const appChainState = new Map() + +/** Connected accounts for each app */ +const appAccountState = new Map() + +// ============================================ +// Callback setters (for UI integration) +// ============================================ + +let _showEvmWalletPicker: ((opts: { chainId: string; app?: MiniappInfo }) => Promise) | null = null +let _showChainSwitchConfirm: ((opts: { fromChainId: string; toChainId: string; appName: string; appIcon?: string }) => Promise) | null = null +let _showEvmSigningDialog: ((opts: { message: string; address: string; appName: string }) => Promise<{ signature: string } | null>) | null = null +let _showEvmTransactionDialog: ((opts: { tx: EvmTransactionRequest; appName: string }) => Promise<{ txHash: string } | null>) | null = null + +export function setEvmWalletPicker(picker: typeof _showEvmWalletPicker): void { + _showEvmWalletPicker = picker +} + +export function setChainSwitchConfirm(confirm: typeof _showChainSwitchConfirm): void { + _showChainSwitchConfirm = confirm +} + +export function setEvmSigningDialog(dialog: typeof _showEvmSigningDialog): void { + _showEvmSigningDialog = dialog +} + +export function setEvmTransactionDialog(dialog: typeof _showEvmTransactionDialog): void { + _showEvmTransactionDialog = dialog +} + +// ============================================ +// Helper functions +// ============================================ + +function getWalletPicker(appId: string) { + const callbacks = HandlerContext.get(appId) + return callbacks?.showEvmWalletPicker ?? _showEvmWalletPicker +} + +function getSigningDialog(appId: string) { + const callbacks = HandlerContext.get(appId) + return callbacks?.showEvmSigningDialog ?? _showEvmSigningDialog +} + +function getTransactionDialog(appId: string) { + const callbacks = HandlerContext.get(appId) + return callbacks?.showEvmTransactionDialog ?? _showEvmTransactionDialog +} + +function getCurrentChainId(appId: string): string { + // Default to BSC + return appChainState.get(appId) ?? toHexChainId(EVM_CHAIN_IDS.binance) +} + +function setCurrentChainId(appId: string, chainId: string): void { + appChainState.set(appId, chainId) +} + +function getConnectedAccounts(appId: string): string[] { + return appAccountState.get(appId) ?? [] +} + +function setConnectedAccounts(appId: string, accounts: string[]): void { + appAccountState.set(appId, accounts) +} + +// ============================================ +// Handlers +// ============================================ + +/** eth_chainId - Get current chain ID */ +export const handleEthChainId: MethodHandler = async (_params, context) => { + return getCurrentChainId(context.appId) +} + +/** eth_requestAccounts - Request wallet connection */ +export const handleEthRequestAccounts: MethodHandler = async (_params, context) => { + const showWalletPicker = getWalletPicker(context.appId) + if (!showWalletPicker) { + throw Object.assign(new Error('Wallet picker not available'), { code: BioErrorCodes.INTERNAL_ERROR }) + } + + const chainId = getCurrentChainId(context.appId) + const wallet = await showWalletPicker({ chainId, app: { name: context.appName, icon: context.appIcon } }) + if (!wallet) { + throw Object.assign(new Error('User rejected'), { code: BioErrorCodes.USER_REJECTED }) + } + + const accounts = [wallet.address] + setConnectedAccounts(context.appId, accounts) + return accounts +} + +/** eth_accounts - Get connected accounts (no UI) */ +export const handleEthAccounts: MethodHandler = async (_params, context) => { + return getConnectedAccounts(context.appId) +} + +/** wallet_switchEthereumChain - Switch to a different chain */ +export const handleSwitchChain: MethodHandler = async (params, context) => { + const request = params as { chainId: string } | undefined + if (!request?.chainId) { + throw Object.assign(new Error('Missing chainId'), { code: BioErrorCodes.INVALID_PARAMS }) + } + + const targetChainId = request.chainId + const keyAppChainId = getKeyAppChainId(targetChainId) + + if (!keyAppChainId) { + // Chain not supported + throw Object.assign( + new Error(`Chain ${targetChainId} not supported`), + { code: 4902 } // EIP-3326 chain not added + ) + } + + const currentChainId = getCurrentChainId(context.appId) + if (currentChainId === targetChainId) { + // Already on this chain + return null + } + + // Show confirmation dialog + if (_showChainSwitchConfirm) { + const approved = await _showChainSwitchConfirm({ + fromChainId: currentChainId, + toChainId: targetChainId, + appName: context.appName, + appIcon: context.appIcon, + }) + if (!approved) { + throw Object.assign(new Error('User rejected'), { code: BioErrorCodes.USER_REJECTED }) + } + } + + setCurrentChainId(context.appId, targetChainId) + + // Note: chainChanged event should be emitted by the bridge + return null +} + +/** wallet_addEthereumChain - Add a new chain (not supported) */ +export const handleAddChain: MethodHandler = async (_params, _context) => { + throw Object.assign( + new Error('Adding custom chains is not supported'), + { code: BioErrorCodes.UNSUPPORTED_METHOD } + ) +} + +/** personal_sign - Sign a message */ +export const handlePersonalSign: MethodHandler = async (params, context) => { + // personal_sign params: [message, address] + const [message, address] = params as [string, string] + if (!message || !address) { + throw Object.assign(new Error('Missing message or address'), { code: BioErrorCodes.INVALID_PARAMS }) + } + + const showSigningDialog = getSigningDialog(context.appId) + if (!showSigningDialog) { + throw Object.assign(new Error('Signing dialog not available'), { code: BioErrorCodes.INTERNAL_ERROR }) + } + + const result = await showSigningDialog({ message, address, appName: context.appName }) + if (!result) { + throw Object.assign(new Error('User rejected'), { code: BioErrorCodes.USER_REJECTED }) + } + + return result.signature +} + +/** eth_sign - Sign data (deprecated, same as personal_sign) */ +export const handleEthSign: MethodHandler = async (params, context) => { + // eth_sign params: [address, message] (order is reversed from personal_sign) + const [address, message] = params as [string, string] + return handlePersonalSign([message, address], context) +} + +/** eth_signTypedData_v4 - Sign EIP-712 typed data */ +export const handleSignTypedDataV4: MethodHandler = async (params, context) => { + const [address, typedData] = params as [string, string | TypedDataV4] + if (!address || !typedData) { + throw Object.assign(new Error('Missing address or typedData'), { code: BioErrorCodes.INVALID_PARAMS }) + } + + const data = typeof typedData === 'string' ? JSON.parse(typedData) : typedData + + const showSigningDialog = getSigningDialog(context.appId) + if (!showSigningDialog) { + throw Object.assign(new Error('Signing dialog not available'), { code: BioErrorCodes.INTERNAL_ERROR }) + } + + // Format typed data for display + const displayMessage = JSON.stringify(data, null, 2) + + const result = await showSigningDialog({ + message: displayMessage, + address, + appName: context.appName, + }) + if (!result) { + throw Object.assign(new Error('User rejected'), { code: BioErrorCodes.USER_REJECTED }) + } + + return result.signature +} + +/** eth_sendTransaction - Send a transaction */ +export const handleEthSendTransaction: MethodHandler = async (params, context) => { + const tx = params as EvmTransactionRequest | undefined + if (!tx?.from) { + throw Object.assign(new Error('Missing transaction data'), { code: BioErrorCodes.INVALID_PARAMS }) + } + + const showTransactionDialog = getTransactionDialog(context.appId) + if (!showTransactionDialog) { + throw Object.assign(new Error('Transaction dialog not available'), { code: BioErrorCodes.INTERNAL_ERROR }) + } + + // Add current chainId if not specified + if (!tx.chainId) { + tx.chainId = getCurrentChainId(context.appId) + } + + const result = await showTransactionDialog({ tx, appName: context.appName }) + if (!result) { + throw Object.assign(new Error('User rejected'), { code: BioErrorCodes.USER_REJECTED }) + } + + return result.txHash +} + +/** eth_signTransaction - Sign transaction without broadcasting */ +export const handleEthSignTransaction: MethodHandler = async (params, context) => { + const tx = params as EvmTransactionRequest | undefined + if (!tx?.from) { + throw Object.assign(new Error('Missing transaction data'), { code: BioErrorCodes.INVALID_PARAMS }) + } + + // TODO: Implement sign-only (no broadcast) + // For now, not supported + throw Object.assign( + new Error('eth_signTransaction not yet supported, use eth_sendTransaction'), + { code: BioErrorCodes.UNSUPPORTED_METHOD } + ) +} + +/** eth_getBalance - Get account balance */ +export const handleEthGetBalance: MethodHandler = async (params, context) => { + const [address, _blockTag] = params as [string, string?] + if (!address) { + throw Object.assign(new Error('Missing address'), { code: BioErrorCodes.INVALID_PARAMS }) + } + + // TODO: Query balance from chain adapter + // For now, return 0 + return '0x0' +} + +/** net_version - Get network version (decimal chain ID) */ +export const handleNetVersion: MethodHandler = async (_params, context) => { + const hexChainId = getCurrentChainId(context.appId) + const decimal = parseHexChainId(hexChainId) + return String(decimal) +} + +/** eth_blockNumber - Get current block number */ +export const handleEthBlockNumber: MethodHandler = async (_params, _context) => { + // TODO: Query from RPC + return '0x0' +} + +/** web3_clientVersion - Get client version */ +export const handleWeb3ClientVersion: MethodHandler = async (_params, _context) => { + return 'KeyApp/1.0.0' +} + +// ============================================ +// Export all handlers map +// ============================================ + +export const evmHandlers: Record = { + eth_chainId: handleEthChainId, + eth_requestAccounts: handleEthRequestAccounts, + eth_accounts: handleEthAccounts, + wallet_switchEthereumChain: handleSwitchChain, + wallet_addEthereumChain: handleAddChain, + personal_sign: handlePersonalSign, + eth_sign: handleEthSign, + eth_signTypedData_v4: handleSignTypedDataV4, + eth_sendTransaction: handleEthSendTransaction, + eth_signTransaction: handleEthSignTransaction, + eth_getBalance: handleEthGetBalance, + net_version: handleNetVersion, + eth_blockNumber: handleEthBlockNumber, + web3_clientVersion: handleWeb3ClientVersion, +} + +/** Register all EVM handlers with the bridge */ +export function registerEvmHandlers(registerHandler: (method: string, handler: MethodHandler) => void): void { + for (const [method, handler] of Object.entries(evmHandlers)) { + registerHandler(method, handler) + } +} diff --git a/src/services/ecosystem/handlers/index.ts b/src/services/ecosystem/handlers/index.ts index ebefa2e35..f5312a847 100644 --- a/src/services/ecosystem/handlers/index.ts +++ b/src/services/ecosystem/handlers/index.ts @@ -2,7 +2,18 @@ * Handler exports */ -export { HandlerContext, getCallbacksOrThrow, type HandlerCallbacks, type SigningParams, type SignTransactionParams } from './context' +export { + HandlerContext, + getCallbacksOrThrow, + type HandlerCallbacks, + type SigningParams, + type SignTransactionParams, + type EvmTransactionRequest, + type TronTransaction, + type EvmSigningParams, + type EvmTransactionParams, + type TronSigningParams, +} from './context' export { handleCloseSplashScreen } from './system' @@ -35,3 +46,22 @@ export { setSignTransactionDialog, signUnsignedTransaction, } from './transaction' + +// EVM handlers +export { + evmHandlers, + registerEvmHandlers, + setEvmWalletPicker, + setChainSwitchConfirm, + setEvmSigningDialog, + setEvmTransactionDialog, +} from './evm' + +// TRON handlers +export { + tronHandlers, + registerTronHandlers, + setTronWalletPicker, + setTronSigningDialog, + type TronAddress, +} from './tron' diff --git a/src/services/ecosystem/handlers/tron.ts b/src/services/ecosystem/handlers/tron.ts new file mode 100644 index 000000000..437c6c42b --- /dev/null +++ b/src/services/ecosystem/handlers/tron.ts @@ -0,0 +1,214 @@ +/** + * TRON method handlers + * + * Handles window.tronLink/tronWeb requests from miniapps. + * Maps TronLink API to KeyApp wallet operations. + */ + +import type { MethodHandler, BioAccount } from '../types' +import { BioErrorCodes } from '../types' +import { HandlerContext, type TronTransaction, type MiniappInfo } from './context' + +// Re-export for convenience +export type { TronTransaction } from './context' + +// ============================================ +// Types +// ============================================ + +/** Tron address format */ +export interface TronAddress { + base58: string + hex: string +} + +// ============================================ +// State +// ============================================ + +/** Connected address for each app */ +const appAddressState = new Map() + +// ============================================ +// Callback setters (for UI integration) +// ============================================ + +let _showTronWalletPicker: ((opts?: { app?: MiniappInfo }) => Promise) | null = null +let _showTronSigningDialog: ((opts: { transaction: TronTransaction; appName: string }) => Promise<{ signedTransaction: TronTransaction } | null>) | null = null + +export function setTronWalletPicker(picker: typeof _showTronWalletPicker): void { + _showTronWalletPicker = picker +} + +export function setTronSigningDialog(dialog: typeof _showTronSigningDialog): void { + _showTronSigningDialog = dialog +} + +// ============================================ +// Helper functions +// ============================================ + +function getWalletPicker(appId: string) { + const callbacks = HandlerContext.get(appId) + return callbacks?.showTronWalletPicker ?? _showTronWalletPicker +} + +function getSigningDialog(appId: string) { + const callbacks = HandlerContext.get(appId) + return callbacks?.showTronSigningDialog ?? _showTronSigningDialog +} + +function getCurrentAddress(appId: string): TronAddress | null { + return appAddressState.get(appId) ?? null +} + +function setCurrentAddress(appId: string, address: TronAddress): void { + appAddressState.set(appId, address) +} + +/** + * Convert KeyApp BioAccount to TronAddress + * Note: KeyApp stores Tron addresses in base58 format + */ +function bioAccountToTronAddress(account: BioAccount): TronAddress { + return { + base58: account.address, + // Hex conversion would require TronWeb library + // For now, we return base58 for both (host will convert) + hex: account.address, + } +} + +// ============================================ +// Handlers +// ============================================ + +/** tron_requestAccounts - Request wallet connection */ +export const handleTronRequestAccounts: MethodHandler = async (_params, context) => { + const showWalletPicker = getWalletPicker(context.appId) + if (!showWalletPicker) { + throw Object.assign(new Error('Wallet picker not available'), { code: BioErrorCodes.INTERNAL_ERROR }) + } + + const wallet = await showWalletPicker({ app: { name: context.appName, icon: context.appIcon } }) + if (!wallet) { + throw Object.assign(new Error('User rejected'), { code: BioErrorCodes.USER_REJECTED }) + } + + const tronAddress = bioAccountToTronAddress(wallet) + setCurrentAddress(context.appId, tronAddress) + + return { + code: 200, + message: 'ok', + data: tronAddress, + } +} + +/** tron_accounts - Get connected accounts */ +export const handleTronAccounts: MethodHandler = async (_params, context) => { + const address = getCurrentAddress(context.appId) + if (!address) { + return { + code: 4000, + message: 'Not connected', + data: null, + } + } + + return { + code: 200, + message: 'ok', + data: address, + } +} + +/** tron_getDefaultAddress - Get current default address */ +export const handleTronGetDefaultAddress: MethodHandler = async (_params, context) => { + return getCurrentAddress(context.appId) +} + +/** tron_signTransaction - Sign a transaction */ +export const handleTronSignTransaction: MethodHandler = async (params, context) => { + const transaction = params as TronTransaction | undefined + if (!transaction?.txID) { + throw Object.assign(new Error('Invalid transaction'), { code: BioErrorCodes.INVALID_PARAMS }) + } + + const showSigningDialog = getSigningDialog(context.appId) + if (!showSigningDialog) { + throw Object.assign(new Error('Signing dialog not available'), { code: BioErrorCodes.INTERNAL_ERROR }) + } + + const result = await showSigningDialog({ + transaction, + appName: context.appName, + }) + + if (!result) { + throw Object.assign(new Error('User rejected'), { code: BioErrorCodes.USER_REJECTED }) + } + + return result.signedTransaction +} + +/** tron_sendRawTransaction - Broadcast signed transaction */ +export const handleTronSendRawTransaction: MethodHandler = async (params, _context) => { + const signedTransaction = params as TronTransaction | undefined + if (!signedTransaction?.txID || !signedTransaction.signature) { + throw Object.assign(new Error('Invalid signed transaction'), { code: BioErrorCodes.INVALID_PARAMS }) + } + + // TODO: Broadcast to TRON network via chain adapter + // For now, return success with txID + return { + result: true, + txid: signedTransaction.txID, + } +} + +/** tron_getBalance - Get TRX balance */ +export const handleTronGetBalance: MethodHandler = async (params, _context) => { + const address = params as string | undefined + if (!address) { + throw Object.assign(new Error('Missing address'), { code: BioErrorCodes.INVALID_PARAMS }) + } + + // TODO: Query balance from chain adapter + return 0 +} + +/** tron_getAccount - Get account info */ +export const handleTronGetAccount: MethodHandler = async (params, _context) => { + const address = params as string | undefined + if (!address) { + throw Object.assign(new Error('Missing address'), { code: BioErrorCodes.INVALID_PARAMS }) + } + + // TODO: Query account from chain adapter + return { + address, + balance: 0, + } +} + +// ============================================ +// Export all handlers map +// ============================================ + +export const tronHandlers: Record = { + tron_requestAccounts: handleTronRequestAccounts, + tron_accounts: handleTronAccounts, + tron_getDefaultAddress: handleTronGetDefaultAddress, + tron_signTransaction: handleTronSignTransaction, + tron_sendRawTransaction: handleTronSendRawTransaction, + tron_getBalance: handleTronGetBalance, + tron_getAccount: handleTronGetAccount, +} + +/** Register all TRON handlers with the bridge */ +export function registerTronHandlers(registerHandler: (method: string, handler: MethodHandler) => void): void { + for (const [method, handler] of Object.entries(tronHandlers)) { + registerHandler(method, handler) + } +} diff --git a/src/services/ecosystem/provider.ts b/src/services/ecosystem/provider.ts index 481b99bcd..b1878e4b3 100644 --- a/src/services/ecosystem/provider.ts +++ b/src/services/ecosystem/provider.ts @@ -19,11 +19,13 @@ import { handleCreateTransaction, handleSignTransaction, handleSendTransaction, + registerEvmHandlers, + registerTronHandlers, } from './handlers' /** Initialize the Bio provider with all handlers */ export function initBioProvider(): void { - // Wallet methods + // Bio methods (BioChain + wallet features) bridge.registerHandler('bio_connect', handleConnect) bridge.registerHandler('bio_closeSplashScreen', handleCloseSplashScreen) bridge.registerHandler('bio_requestAccounts', handleRequestAccounts) @@ -44,21 +46,44 @@ export function initBioProvider(): void { // Transfer methods bridge.registerHandler('bio_sendTransaction', handleSendTransaction) - console.log('[BioProvider] Initialized with handlers:', [ - 'bio_connect', - 'bio_closeSplashScreen', - 'bio_requestAccounts', - 'bio_accounts', - 'bio_selectAccount', - 'bio_pickWallet', - 'bio_chainId', - 'bio_getBalance', - 'bio_signMessage', - 'bio_signTypedData', - 'bio_createTransaction', - 'bio_signTransaction', - 'bio_sendTransaction', - ]) + // EVM methods (Ethereum/BSC via window.ethereum) + registerEvmHandlers((method, handler) => bridge.registerHandler(method, handler)) + + // TRON methods (via window.tronLink/tronWeb) + registerTronHandlers((method, handler) => bridge.registerHandler(method, handler)) + + console.log('[BioProvider] Initialized with handlers:', { + bio: [ + 'bio_connect', + 'bio_closeSplashScreen', + 'bio_requestAccounts', + 'bio_accounts', + 'bio_selectAccount', + 'bio_pickWallet', + 'bio_chainId', + 'bio_getBalance', + 'bio_signMessage', + 'bio_signTypedData', + 'bio_createTransaction', + 'bio_signTransaction', + 'bio_sendTransaction', + ], + evm: [ + 'eth_chainId', + 'eth_requestAccounts', + 'eth_accounts', + 'wallet_switchEthereumChain', + 'personal_sign', + 'eth_sendTransaction', + // ... + ], + tron: [ + 'tron_requestAccounts', + 'tron_accounts', + 'tron_signTransaction', + // ... + ], + }) } /** Get the bridge instance */ @@ -73,4 +98,12 @@ export { setSigningDialog, setTransferDialog, setSignTransactionDialog, + // EVM setters + setEvmWalletPicker, + setChainSwitchConfirm, + setEvmSigningDialog, + setEvmTransactionDialog, + // TRON setters + setTronWalletPicker, + setTronSigningDialog, } from './handlers' diff --git a/src/services/ecosystem/types.ts b/src/services/ecosystem/types.ts index 3f6db33b9..021414c3a 100644 --- a/src/services/ecosystem/types.ts +++ b/src/services/ecosystem/types.ts @@ -266,6 +266,7 @@ export type MethodHandler = ( export interface HandlerContext { appId: string appName: string + appIcon?: string origin: string permissions: string[] } diff --git a/src/stackflow/activities/MainTabsActivity.tsx b/src/stackflow/activities/MainTabsActivity.tsx index a60dbd5c2..6d847fc2e 100644 --- a/src/stackflow/activities/MainTabsActivity.tsx +++ b/src/stackflow/activities/MainTabsActivity.tsx @@ -11,12 +11,19 @@ import type { BioAccount, BioSignedTransaction, TransferParams } from "@/service import { getBridge, initBioProvider, + setChainSwitchConfirm, + setEvmSigningDialog, + setEvmTransactionDialog, + setEvmWalletPicker, setGetAccounts, setSigningDialog, setSignTransactionDialog, + setTronWalletPicker, setTransferDialog, setWalletPicker, } from "@/services/ecosystem"; +import { getKeyAppChainId } from "@biochain/bio-sdk"; +import { formatUnits } from "viem"; import { walletSelectors, walletStore, type ChainAddress } from "@/stores"; import { miniappRuntimeStore } from "@/services/miniapp-runtime"; @@ -182,9 +189,165 @@ export const MainTabsActivity: ActivityComponentType = ({ params }); }); + setEvmWalletPicker(async (opts) => { + const appName = opts.app?.name; + const appIcon = opts.app?.icon; + + return new Promise((resolve) => { + const timeout = window.setTimeout(() => resolve(null), 30_000); + + const cleanup = () => { + window.clearTimeout(timeout); + window.removeEventListener("wallet-picker-select", handleSelect); + window.removeEventListener("wallet-picker-cancel", handleCancel); + }; + + const handleSelect = (e: Event) => { + cleanup(); + const detail = (e as CustomEvent).detail as { address: string; chain: string; name?: string }; + resolve({ address: detail.address, chain: detail.chain, name: detail.name }); + }; + + const handleCancel = () => { + cleanup(); + resolve(null); + }; + + window.addEventListener("wallet-picker-select", handleSelect); + window.addEventListener("wallet-picker-cancel", handleCancel); + + push("WalletPickerJob", { + chain: opts.chainId, + ...(appName ? { appName } : {}), + ...(appIcon ? { appIcon } : {}), + }); + }); + }); + + setChainSwitchConfirm(async (opts) => { + return new Promise((resolve) => { + const timeout = window.setTimeout(() => resolve(false), 30_000); + + const handleResult = (e: Event) => { + window.clearTimeout(timeout); + const detail = (e as CustomEvent).detail as { approved?: boolean } | undefined; + resolve(detail?.approved === true); + }; + + window.addEventListener("chain-switch-confirm", handleResult, { once: true }); + + push("ChainSwitchConfirmJob", { + fromChainId: opts.fromChainId, + toChainId: opts.toChainId, + appName: opts.appName, + appIcon: opts.appIcon, + }); + }); + }); + + setEvmSigningDialog(async (params) => { + return new Promise<{ signature: string } | null>((resolve) => { + const timeout = window.setTimeout(() => resolve(null), 60_000); + + const handleResult = (e: Event) => { + window.clearTimeout(timeout); + const detail = (e as CustomEvent).detail as + | { confirmed?: boolean; signature?: string } + | undefined; + if (detail?.confirmed && detail.signature) { + resolve({ signature: detail.signature }); + return; + } + resolve(null); + }; + + window.addEventListener("signing-confirm", handleResult, { once: true }); + push("SigningConfirmJob", { + message: params.message, + address: params.address, + appName: params.appName, + chainName: "ethereum", + }); + }); + }); + + setEvmTransactionDialog(async (params) => { + const { from, to, value, chainId } = params.tx; + if (!from || !to) { + return null; + } + + const valueBigInt = BigInt(value ?? "0x0"); + const amount = formatUnits(valueBigInt, 18); + const keyAppChainId = chainId ? getKeyAppChainId(chainId) : null; + + return new Promise<{ txHash: string } | null>((resolve) => { + const timeout = window.setTimeout(() => resolve(null), 60_000); + + const handleResult = (e: Event) => { + window.clearTimeout(timeout); + const detail = (e as CustomEvent).detail as { confirmed?: boolean; txHash?: string } | undefined; + if (detail?.confirmed && detail.txHash) { + resolve({ txHash: detail.txHash }); + return; + } + resolve(null); + }; + + window.addEventListener("miniapp-transfer-confirm", handleResult, { once: true }); + push("MiniappTransferConfirmJob", { + appName: params.appName, + from, + to, + amount, + chain: keyAppChainId ?? "ethereum", + }); + }); + }); + + setTronWalletPicker(async (opts) => { + const appName = opts?.app?.name; + const appIcon = opts?.app?.icon; + + return new Promise((resolve) => { + const timeout = window.setTimeout(() => resolve(null), 30_000); + + const cleanup = () => { + window.clearTimeout(timeout); + window.removeEventListener("wallet-picker-select", handleSelect); + window.removeEventListener("wallet-picker-cancel", handleCancel); + }; + + const handleSelect = (e: Event) => { + cleanup(); + const detail = (e as CustomEvent).detail as { address: string; chain: string; name?: string }; + resolve({ address: detail.address, chain: detail.chain, name: detail.name }); + }; + + const handleCancel = () => { + cleanup(); + resolve(null); + }; + + window.addEventListener("wallet-picker-select", handleSelect); + window.addEventListener("wallet-picker-cancel", handleCancel); + + push("WalletPickerJob", { + chain: "tron", + ...(appName ? { appName } : {}), + ...(appIcon ? { appIcon } : {}), + }); + }); + }); + return () => { getBridge().setPermissionRequestCallback(null); setWalletPicker(null); + setEvmWalletPicker(null); + setChainSwitchConfirm(null); + setEvmSigningDialog(null); + setEvmTransactionDialog(null); + setTronWalletPicker(null); setGetAccounts(null); setSigningDialog(null); setTransferDialog(null); diff --git a/src/stackflow/activities/sheets/ChainSwitchConfirmJob.tsx b/src/stackflow/activities/sheets/ChainSwitchConfirmJob.tsx new file mode 100644 index 000000000..9dc2dd964 --- /dev/null +++ b/src/stackflow/activities/sheets/ChainSwitchConfirmJob.tsx @@ -0,0 +1,157 @@ +/** + * ChainSwitchConfirmJob - 链切换确认弹窗 + * 当 DApp 请求 wallet_switchEthereumChain 时显示 + */ + +import type { ActivityComponentType } from '@stackflow/react' +import { BottomSheet } from '@/components/layout/bottom-sheet' +import { useTranslation } from 'react-i18next' +import { IconArrowRight, IconAlertTriangle } from '@tabler/icons-react' +import { ChainIcon } from '@/components/wallet/chain-icon' +import { MiniappSheetHeader } from '@/components/ecosystem' +import { useFlow } from '../../stackflow' +import { ActivityParamsProvider, useActivityParams } from '../../hooks' +import { parseHexChainId, getKeyAppChainId, CHAIN_DISPLAY_NAMES } from '@biochain/bio-sdk' +import type { ChainType } from '@/stores' + +type ChainSwitchConfirmJobParams = { + /** 当前链 ID (hex, e.g., '0x38') */ + fromChainId: string + /** 目标链 ID (hex, e.g., '0x1') */ + toChainId: string + /** 请求来源小程序名称 */ + appName?: string + /** 请求来源小程序图标 */ + appIcon?: string +} + +/** 获取链的显示名称 */ +function getChainDisplayName(hexChainId: string): string { + const keyAppId = getKeyAppChainId(hexChainId) + if (keyAppId && CHAIN_DISPLAY_NAMES[keyAppId]) { + return CHAIN_DISPLAY_NAMES[keyAppId] + } + // Fallback: 显示 decimal chainId + try { + const decimal = parseHexChainId(hexChainId) + return `Chain ${decimal}` + } catch { + return hexChainId + } +} + +/** 获取 KeyApp 链类型 */ +function getChainType(hexChainId: string): ChainType | null { + const keyAppId = getKeyAppChainId(hexChainId) + return keyAppId as ChainType | null +} + +function ChainSwitchConfirmJobContent() { + const { t } = useTranslation('common') + const { pop } = useFlow() + const { fromChainId, toChainId, appName, appIcon } = useActivityParams() + + const fromChainName = getChainDisplayName(fromChainId) + const toChainName = getChainDisplayName(toChainId) + const fromChainType = getChainType(fromChainId) + const toChainType = getChainType(toChainId) + + const handleConfirm = () => { + const event = new CustomEvent('chain-switch-confirm', { + detail: { approved: true, toChainId }, + }) + window.dispatchEvent(event) + pop() + } + + const handleCancel = () => { + const event = new CustomEvent('chain-switch-confirm', { + detail: { approved: false }, + }) + window.dispatchEvent(event) + pop() + } + + return ( + +
+ {/* Handle */} +
+
+
+ + {/* App Info */} + + + {/* Chain Switch Visual */} +
+ {/* From Chain */} +
+ {fromChainType ? ( + + ) : ( +
+ +
+ )} + {fromChainName} +
+ + {/* Arrow */} + + + {/* To Chain */} +
+ {toChainType ? ( + + ) : ( +
+ +
+ )} + {toChainName} +
+
+ + {/* Warning */} +
+

+ {t('chainSwitchWarning', '切换网络后,您的交易将在新网络上进行。请确保您了解此操作的影响。')} +

+
+ + {/* Buttons */} +
+ + +
+ + {/* Safe area */} +
+
+ + ) +} + +export const ChainSwitchConfirmJob: ActivityComponentType = ({ params }) => { + return ( + + + + ) +} diff --git a/src/stackflow/activities/sheets/WalletPickerJob.tsx b/src/stackflow/activities/sheets/WalletPickerJob.tsx index 14354c0ea..d921cffce 100644 --- a/src/stackflow/activities/sheets/WalletPickerJob.tsx +++ b/src/stackflow/activities/sheets/WalletPickerJob.tsx @@ -8,15 +8,15 @@ import type { ActivityComponentType } from '@stackflow/react' import { BottomSheet } from '@/components/layout/bottom-sheet' import { useTranslation } from 'react-i18next' import { useStore } from '@tanstack/react-store' -import { IconApps } from '@tabler/icons-react' import { walletStore, walletSelectors, type Wallet, type ChainAddress } from '@/stores' import { useFlow } from '../../stackflow' import { ActivityParamsProvider, useActivityParams } from '../../hooks' import { WalletList, type WalletListItem } from '@/components/wallet/wallet-list' -import { MiniappIcon } from '@/components/ecosystem' +import { MiniappSheetHeader } from '@/components/ecosystem' +import { getKeyAppChainId, normalizeChainId, CHAIN_DISPLAY_NAMES } from '@biochain/bio-sdk' type WalletPickerJobParams = { - /** 限定链类型 */ + /** 限定链类型 (支持: KeyApp 内部 ID, EVM hex chainId, API 名称如 BSC) */ chain?: string /** 排除的地址(不显示在列表中) */ exclude?: string @@ -26,10 +26,41 @@ type WalletPickerJobParams = { appIcon?: string } +/** + * 将任意链标识符转换为 KeyApp 内部 ID + * 支持: + * - EVM hex chainId: '0x38' -> 'binance' + * - API 名称: 'BSC', 'ETH' -> 'binance', 'ethereum' + * - 已有的 KeyApp ID: 'binance' -> 'binance' + */ +function resolveChainId(chain: string | undefined): string | undefined { + if (!chain) return undefined + + // Try EVM hex chainId first (e.g., '0x38') + if (chain.startsWith('0x')) { + const keyAppId = getKeyAppChainId(chain) + if (keyAppId) return keyAppId + } + + // Try API name normalization (e.g., 'BSC' -> 'binance') + const normalized = normalizeChainId(chain) + + // Check if it's a known chain + if (CHAIN_DISPLAY_NAMES[normalized]) { + return normalized + } + + // Return as-is (might be already a KeyApp ID like 'binance') + return normalized +} + function WalletPickerJobContent() { const { t } = useTranslation('common') const { pop } = useFlow() - const { chain, exclude, appName, appIcon } = useActivityParams() + const { chain: rawChain, exclude, appName, appIcon } = useActivityParams() + + // Resolve chain to KeyApp internal ID + const chain = useMemo(() => resolveChainId(rawChain), [rawChain]) const walletState = useStore(walletStore) const currentWallet = walletSelectors.getCurrentWallet(walletState) @@ -106,24 +137,12 @@ function WalletPickerJobContent() {
{/* Title with App Icon */} -
- {(appName || appIcon) && ( -
- -
- )} -

- {t('selectWallet', '选择钱包')} -

-

- {appName || t('unknownDApp', '未知 DApp')} {t('requestsAccess', '请求访问')} -

-
+ {/* Wallet List */}
diff --git a/src/stackflow/activities/sheets/__stories__/ChainSwitchConfirmJob.stories.tsx b/src/stackflow/activities/sheets/__stories__/ChainSwitchConfirmJob.stories.tsx new file mode 100644 index 000000000..cc194f1c0 --- /dev/null +++ b/src/stackflow/activities/sheets/__stories__/ChainSwitchConfirmJob.stories.tsx @@ -0,0 +1,123 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { useState } from 'react' +import { getKeyAppChainId } from '@biochain/bio-sdk' + +const meta: Meta = { + title: 'Sheets/ChainSwitchConfirmJob', + parameters: { + layout: 'fullscreen', + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export default meta +type Story = StoryObj + +function ChainSwitchPreview({ + fromChainId, + toChainId, + appName, +}: { + fromChainId: string + toChainId: string + appName?: string +}) { + const [result, setResult] = useState<'approved' | 'rejected' | null>(null) + const from = getKeyAppChainId(fromChainId) ?? fromChainId + const to = getKeyAppChainId(toChainId) ?? toChainId + + return ( +
+
+
+ +

切换网络确认

+

+ {appName || '未知 DApp'} 请求切换网络 +

+ +
+
+ 当前网络 + {from} +
+
+ 目标网络 + {to} +
+
+ + {result && ( +
+ 结果:{result === 'approved' ? '已确认' : '已取消'} +
+ )} + +
+ + +
+
+
+
+ ) +} + +/** BSC 切换到 Ethereum */ +export const BSCToEthereum: Story = { + render: () => ( + + ), +} + +/** Ethereum 切换到 BSC */ +export const EthereumToBSC: Story = { + render: () => ( + + ), +} + +/** 无应用信息 */ +export const NoAppInfo: Story = { + render: () => ( + + ), +} + +/** 未知链 ID */ +export const UnknownChain: Story = { + render: () => ( + + ), +} diff --git a/src/stackflow/activities/sheets/index.ts b/src/stackflow/activities/sheets/index.ts index 158b560c3..b39947719 100644 --- a/src/stackflow/activities/sheets/index.ts +++ b/src/stackflow/activities/sheets/index.ts @@ -22,3 +22,4 @@ export { SigningConfirmJob } from "./SigningConfirmJob"; export { PermissionRequestJob } from "./PermissionRequestJob"; export { MiniappTransferConfirmJob } from "./MiniappTransferConfirmJob"; export { MiniappSignTransactionJob } from "./MiniappSignTransactionJob"; +export { ChainSwitchConfirmJob } from "./ChainSwitchConfirmJob"; diff --git a/src/stackflow/stackflow.ts b/src/stackflow/stackflow.ts index 065ec9f45..8167c4e63 100644 --- a/src/stackflow/stackflow.ts +++ b/src/stackflow/stackflow.ts @@ -58,6 +58,7 @@ import { PermissionRequestJob, MiniappTransferConfirmJob, MiniappSignTransactionJob, + ChainSwitchConfirmJob, } from './activities/sheets'; export const { Stack, useFlow, useStepFlow, activities } = stackflow({ @@ -122,6 +123,7 @@ export const { Stack, useFlow, useStepFlow, activities } = stackflow({ PermissionRequestJob: '/job/permission-request', MiniappTransferConfirmJob: '/job/miniapp-transfer-confirm', MiniappSignTransactionJob: '/job/miniapp-sign-transaction', + ChainSwitchConfirmJob: '/job/chain-switch-confirm', }, fallbackActivity: () => 'MainTabsActivity', useHash: true, @@ -185,6 +187,7 @@ export const { Stack, useFlow, useStepFlow, activities } = stackflow({ PermissionRequestJob, MiniappTransferConfirmJob, MiniappSignTransactionJob, + ChainSwitchConfirmJob, }, // Note: Don't set initialActivity when using historySyncPlugin // The plugin will determine the initial activity based on the URL