Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/white-book/04-服务篇/02-链服务/实现规范/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,55 @@ BioForest 链支持可选的交易密码机制:
- **MUST** 处理交易密码验证
- **SHOULD** 缓存区块高度用于交易构建

### SDK 集成

BioForest 链使用官方 SDK 进行交易创建和签名:

```typescript
// SDK 位置
src/services/bioforest-sdk/
├── index.ts // SDK 包装器
├── types.d.ts // 类型定义
├── bioforest-chain-bundle.js // 预构建 SDK
├── bioforest-chain-bundle.d.ts // 类型声明
└── genesis-block.json // 创世块配置

// 关键函数
createTransferTransaction(params) // 创建转账交易
broadcastTransaction(rpcUrl, chainId, tx) // 广播交易
getAddressInfo(rpcUrl, chainId, address) // 获取地址信息
verifyPayPassword(secret, payPassword, secondPublicKey) // 验证交易密码
```

### 交易密码流程

```
用户发起转账
│
▼
getAddressInfo() 检查 secondPublicKey
│
├─ 无 secondPublicKey → 直接签名
│
└─ 有 secondPublicKey → 需要交易密码
│
▼
用户输入交易密码
│
▼
verifyPayPassword() 验证
│
├─ 失败 → 提示错误
│
└─ 成功 → 使用 paySecret 签名
│
▼
createTransferTransaction()
│
▼
broadcastTransaction()
```

---

## EVM 链
Expand Down
205 changes: 205 additions & 0 deletions docs/white-book/04-服务篇/09-联系人服务/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# 联系人服务

> 管理用户的联系人和地址簿

---

## 概述

联系人服务提供地址簿管理功能,支持:
- 一个联系人保存多个地址(不同链)
- 自动识别地址格式
- 地址输入时的联系人建议
- 联系人搜索和过滤

---

## 数据模型

### Contact(联系人)

```typescript
interface Contact {
id: string // UUID
name: string // 联系人名称
avatar?: string // 头像(可选,emoji 或图片URL)
addresses: ContactAddress[] // 多个地址
memo?: string // 备注
createdAt: number // 创建时间
updatedAt: number // 更新时间
}
```

### ContactAddress(联系人地址)

```typescript
interface ContactAddress {
id: string // UUID
address: string // 地址
chainType: ChainType // 链类型(自动检测或手动指定)
label?: string // 地址标签(如"主地址"、"交易所")
isDefault?: boolean // 是否默认地址
}
```

---

## 地址格式识别

### 支持的格式

| 链类型 | 地址格式 | 示例 |
|-------|---------|------|
| Ethereum | 0x + 40位hex | `0x742d35Cc...` |
| BSC | 同 Ethereum | `0x742d35Cc...` |
| TRON | T + 33位base58 | `TJYs...` |
| Bitcoin Legacy | 1 + 25-34位base58 | `1A1zP1...` |
| Bitcoin P2SH | 3 + 25-34位base58 | `3J98t1...` |
| Bitcoin Bech32 | bc1 + 25-89位 | `bc1qar0...` |
| BFMeta | b + base58check | `b7ADmv...` |
| CCChain | c + base58check | `cXXXXX...` |
| PMChain | p + base58check | `pXXXXX...` |

### 检测算法

```typescript
function detectAddressFormat(address: string): AddressFormatInfo {
// 1. 检查 0x 前缀 → Ethereum/BSC
// 2. 检查 T 前缀 + 长度 → TRON
// 3. 检查 1/3/bc1 前缀 → Bitcoin
// 4. 检查 BioForest 前缀 (b/c/p/B/g/w/e/m) → 对应链
// 5. 返回 unknown
}
```

---

## 服务接口

### IContactService

```typescript
interface IContactService {
// 联系人 CRUD
getContacts(): Contact[]
getContactById(id: string): Contact | undefined
addContact(contact: Omit<Contact, 'id' | 'createdAt' | 'updatedAt'>): Contact
updateContact(id: string, updates: Partial<Contact>): void
deleteContact(id: string): void

// 地址操作
addAddressToContact(contactId: string, address: ContactAddress): void
removeAddressFromContact(contactId: string, addressId: string): void

// 搜索和建议
searchContacts(query: string): Contact[]
suggestContacts(partialAddress: string): ContactSuggestion[]
getContactByAddress(address: string): Contact | undefined

// 导入导出
importContacts(contacts: Contact[]): number
exportContacts(): Contact[]
}
```

### ContactSuggestion(联系人建议)

```typescript
interface ContactSuggestion {
contact: Contact
matchedAddress: ContactAddress
matchType: 'exact' | 'prefix' | 'name'
score: number // 匹配分数,用于排序
}
```

---

## 地址输入增强

### AddressInput 组件增强

```
┌─────────────────────────────────────────┐
│ [图标] 输入地址或选择联系人 [扫描] │
├─────────────────────────────────────────┤
│ 建议联系人: │
│ ┌─────────────────────────────────────┐ │
│ │ 👤 Alice │ │
│ │ b7ADmv... (BFMeta) │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ 👤 Bob │ │
│ │ 0x742d... (Ethereum) │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘
```

### 交互流程

1. 用户开始输入地址
2. 实时搜索匹配的联系人(按名称或地址前缀)
3. 显示建议列表(最多5个)
4. 用户选择联系人后,自动填入对应地址
5. 根据当前选择的链,优先显示该链的地址

---

## 存储

使用 localStorage 持久化,key 为 `bfm_contacts`。

```typescript
// 存储格式
{
version: 2, // 数据版本,用于迁移
contacts: Contact[]
}
```

### 数据迁移

从 v1(单地址)迁移到 v2(多地址):

```typescript
function migrateV1ToV2(v1Contact: V1Contact): Contact {
return {
...v1Contact,
addresses: [{
id: crypto.randomUUID(),
address: v1Contact.address,
chainType: v1Contact.chain ?? detectAddressFormat(v1Contact.address).chainType,
}]
}
}
```

---

## 与转账流程集成

### 发送页面

1. 地址输入框显示联系人建议
2. 选择联系人时,如果有多个地址,弹出地址选择器
3. 如果联系人只有一个地址,直接填入

### 交易历史

1. 显示交易对方时,优先显示联系人名称
2. 提供"添加到联系人"操作

---

## 安全考虑

- 联系人数据不加密(非敏感数据)
- 删除联系人需要密码确认(防止误操作)
- 导出功能可选加密

---

## 相关链接

- [08-钱包数据存储](../08-钱包数据存储/) - 数据存储规范
- [ITransactionService](../02-链服务/ITransactionService/) - 交易服务
10 changes: 10 additions & 0 deletions docs/white-book/04-服务篇/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ Schema-first 的服务定义系统,提供类型安全和 Mock 控制。
- 数据迁移策略
- DWEB 生物识别增强

### [09-联系人服务](./09-联系人服务/)

定义联系人和地址簿的管理规范。

**要点**:
- 一个联系人支持多个地址
- 自动识别地址格式(检测链类型)
- 地址输入时的联系人建议
- 与转账流程的集成

---

## 设计原则
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/__screenshots__/Desktop-Chrome/home-chain-selector.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/__screenshots__/Desktop-Chrome/wallet-detail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/__screenshots__/Mobile-Chrome/history-empty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/__screenshots__/Mobile-Chrome/home-chain-selector.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 75 additions & 0 deletions e2e/pages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,13 +247,88 @@ test.describe('通知页面', () => {
})
})

// 测试地址簿数据(多地址联系人)
const TEST_CONTACTS_DATA = {
contacts: [
{
id: 'contact-1',
name: 'Alice',
addresses: [
{ id: 'addr-1', address: '0x1234567890abcdef1234567890abcdef12345678', chainType: 'ethereum', isDefault: true },
],
memo: '同事',
createdAt: Date.now() - 86400000,
updatedAt: Date.now() - 86400000,
},
{
id: 'contact-2',
name: 'Bob',
addresses: [
{ id: 'addr-2', address: '0xabcdef1234567890abcdef1234567890abcdef12', chainType: 'ethereum', isDefault: true },
{ id: 'addr-3', address: 'c7R6wVdPvHqvRxe5Q9ZvWr7CpPn5Mk5Xz3', chainType: 'bfmeta' },
],
createdAt: Date.now() - 172800000,
updatedAt: Date.now() - 172800000,
},
{
id: 'contact-3',
name: '多链用户',
addresses: [
{ id: 'addr-4', address: '0x9876543210fedcba9876543210fedcba98765432', chainType: 'ethereum', isDefault: true },
{ id: 'addr-5', address: 'c7R6wVdPvHqvRxe5Q9ZvWr7CpPn5Mk5Xz3', chainType: 'bfmeta' },
{ id: 'addr-6', address: 'TJCnKsPa7y5okkXvQAidZBzqx3QyQ6sxMW', chainType: 'tron' },
],
memo: '支持多链转账',
createdAt: Date.now() - 259200000,
updatedAt: Date.now() - 259200000,
},
],
isInitialized: true,
version: 2,
}

// 辅助函数:设置测试联系人
async function setupTestContacts(page: Page, targetUrl: string = '/address-book') {
await page.addInitScript((data) => {
localStorage.setItem('bfm_wallets', JSON.stringify(data.wallet))
localStorage.setItem('bfm_address_book', JSON.stringify(data.contacts))
}, { wallet: TEST_WALLET_DATA, contacts: TEST_CONTACTS_DATA })

const hashUrl = `/#${targetUrl}`
await page.goto(hashUrl)
await page.waitForLoadState('networkidle')
}

test.describe('地址簿页面', () => {
test('地址簿截图 - 空状态', async ({ page }) => {
await setupTestWallet(page, '/address-book')
await waitForAppReady(page)

await expect(page).toHaveScreenshot('address-book-empty.png')
})

test('地址簿截图 - 有联系人', async ({ page }) => {
await setupTestContacts(page, '/address-book')
await waitForAppReady(page)
// Wait for contacts to load and render
await expect(page.locator('text=Alice')).toBeVisible({ timeout: 10000 })

await expect(page).toHaveScreenshot('address-book-with-contacts.png')
})

test('地址簿 - 多地址联系人显示', async ({ page }) => {
await setupTestContacts(page, '/address-book')
await waitForAppReady(page)

// 验证多地址联系人显示 (+N) 后缀
const multiAddressContact = page.locator('text=Bob')
await expect(multiAddressContact).toBeVisible()
// Bob has 2 addresses, should show (+1)
await expect(page.locator('text=+1')).toBeVisible()

// 多链用户 has 3 addresses, should show (+2)
await expect(page.locator('text=+2')).toBeVisible()
})
})

test.describe('钱包列表页面', () => {
Expand Down
Loading
Loading