Skip to content

Commit 572cdc8

Browse files
committed
refactor(contact): remove chainType, use custom label for addresses
BREAKING CHANGE: ContactAddress no longer has chainType field - Remove chainType from ContactAddress interface - Add custom label field (e.g. 'Main', 'Exchange') - Use detectAddressFormat for chain validation instead of stored type - Update all components to display label or detected chain type - Add addressLabel input in ContactEditJob - Update QR protocol to use label instead of chainType - Limit contacts to max 3 addresses (QR capacity)
1 parent 25e1b9e commit 572cdc8

File tree

19 files changed

+179
-176
lines changed

19 files changed

+179
-176
lines changed

src/components/contact/contact-card.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,24 @@
66
import { QRCodeSVG } from 'qrcode.react';
77
import { ContactAvatar } from '@/components/common/contact-avatar';
88
import { generateAvatarFromAddress } from '@/lib/avatar-codec';
9+
import { detectAddressFormat } from '@/lib/address-format';
910
import type { ContactAddressInfo } from '@/lib/qr-parser';
1011

11-
const CHAIN_NAMES: Record<string, string> = {
12-
ethereum: 'ETH',
13-
bitcoin: 'BTC',
14-
tron: 'TRX',
15-
};
16-
1712
const CHAIN_COLORS: Record<string, string> = {
1813
ethereum: '#627EEA',
1914
bitcoin: '#F7931A',
2015
tron: '#FF0013',
2116
};
2217

18+
/** 获取地址显示标签和颜色 */
19+
function getAddressDisplay(addr: ContactAddressInfo): { label: string; color: string } {
20+
const detected = detectAddressFormat(addr.address);
21+
const chainType = detected.chainType;
22+
const label = addr.label || chainType?.toUpperCase() || '';
23+
const color = chainType ? CHAIN_COLORS[chainType] || '#6B7280' : '#6B7280';
24+
return { label, color };
25+
}
26+
2327
export interface ContactCardProps {
2428
name: string;
2529
avatar?: string | undefined;
@@ -38,15 +42,18 @@ export function ContactCard({ name, avatar, address, addresses, qrContent }: Con
3842
<div className="flex-1">
3943
<h3 className="text-xl font-bold text-white">{name}</h3>
4044
<div className="mt-1 flex flex-wrap gap-1.5">
41-
{addresses.map((a) => (
42-
<span
43-
key={a.chainType}
44-
className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium text-white/90"
45-
style={{ backgroundColor: CHAIN_COLORS[a.chainType] || '#6B7280' }}
46-
>
47-
{CHAIN_NAMES[a.chainType] || a.chainType.toUpperCase()}
48-
</span>
49-
))}
45+
{addresses.map((a, i) => {
46+
const { label, color } = getAddressDisplay(a);
47+
return (
48+
<span
49+
key={i}
50+
className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium text-white/90"
51+
style={{ backgroundColor: color }}
52+
>
53+
{label}
54+
</span>
55+
);
56+
})}
5057
</div>
5158
</div>
5259
</div>

src/components/transfer/address-input.stories.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,21 +93,21 @@ export const WithContactSuggestions: Story = {
9393
addressBookActions.addContact({
9494
name: 'Alice',
9595
addresses: [
96-
{ id: '1', address: '0x1234567890abcdef1234567890abcdef12345678', chainType: 'ethereum', isDefault: true },
96+
{ id: '1', address: '0x1234567890abcdef1234567890abcdef12345678', label: 'ETH', isDefault: true },
9797
],
9898
memo: '同事',
9999
});
100100
addressBookActions.addContact({
101101
name: 'Bob',
102102
addresses: [
103-
{ id: '2', address: '0xabcdef1234567890abcdef1234567890abcdef12', chainType: 'ethereum', isDefault: true },
104-
{ id: '3', address: 'c7R6wVdPvHqvRxe5Q9ZvWr7CpPn5Mk5Xz3', chainType: 'bfmeta' },
103+
{ id: '2', address: '0xabcdef1234567890abcdef1234567890abcdef12', label: 'ETH', isDefault: true },
104+
{ id: '3', address: 'c7R6wVdPvHqvRxe5Q9ZvWr7CpPn5Mk5Xz3', label: 'BFMETA' },
105105
],
106106
});
107107
addressBookActions.addContact({
108108
name: 'Charlie',
109109
addresses: [
110-
{ id: '4', address: '0x9876543210fedcba9876543210fedcba98765432', chainType: 'ethereum', isDefault: true },
110+
{ id: '4', address: '0x9876543210fedcba9876543210fedcba98765432', label: 'ETH', isDefault: true },
111111
],
112112
memo: '朋友',
113113
});
@@ -147,14 +147,14 @@ export const FilterByChain: Story = {
147147
addressBookActions.addContact({
148148
name: 'Alice',
149149
addresses: [
150-
{ id: '1', address: '0x1234567890abcdef1234567890abcdef12345678', chainType: 'ethereum', isDefault: true },
151-
{ id: '2', address: 'b7R6wVdPvHqvRxe5Q9ZvWr7CpPn5Mk5Xz3', chainType: 'bfmeta' },
150+
{ id: '1', address: '0x1234567890abcdef1234567890abcdef12345678', label: 'ETH', isDefault: true },
151+
{ id: '2', address: 'b7R6wVdPvHqvRxe5Q9ZvWr7CpPn5Mk5Xz3', label: 'BFMETA' },
152152
],
153153
});
154154
addressBookActions.addContact({
155155
name: 'Bob',
156156
addresses: [
157-
{ id: '3', address: 'c7ADmvZJJ3n3aDxkvwbXxJX1oGgeiCzL11', chainType: 'ccchain', isDefault: true },
157+
{ id: '3', address: 'c7ADmvZJJ3n3aDxkvwbXxJX1oGgeiCzL11', label: 'CCCHAIN', isDefault: true },
158158
],
159159
});
160160
return <Story />;
@@ -166,7 +166,7 @@ export const FilterByChain: Story = {
166166
return (
167167
<div className="space-y-6 p-4">
168168
<p className="text-muted-foreground text-sm">
169-
设置 chainType="bfmeta",只显示 BFMeta 链的地址
169+
设置 chainType="bfmeta",只显示 BFMeta 链有效的地址
170170
</p>
171171
<AddressInput
172172
label="BFMeta 地址"

src/components/transfer/address-input.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@ import { useStore } from '@tanstack/react-store';
55
import { IconLineScan as ScanLine, IconClipboardCopy as ClipboardPaste, IconUsers } from '@tabler/icons-react';
66
import { ContactAvatar } from '@/components/common/contact-avatar';
77
import { clipboardService } from '@/services/clipboard';
8-
import { isValidAddressForChain } from '@/lib/address-format';
9-
import { addressBookStore, addressBookSelectors, type ChainType, type ContactSuggestion } from '@/stores';
8+
import { isValidAddressForChain, detectAddressFormat } from '@/lib/address-format';
9+
import { addressBookStore, addressBookSelectors, type ChainType, type ContactSuggestion, type ContactAddress } from '@/stores';
10+
11+
/** 获取地址显示标签(优先用自定义 label,否则用检测到的链类型) */
12+
function getAddressDisplayLabel(address: ContactAddress): string {
13+
if (address.label) return address.label;
14+
const detected = detectAddressFormat(address.address);
15+
return detected.chainType?.toUpperCase() || '';
16+
}
1017

1118
interface AddressInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
1219
value?: string | undefined;
@@ -58,8 +65,7 @@ const AddressInput = forwardRef<HTMLInputElement, AddressInputProps>(
5865
// 获取联系人建议 - 显示所有联系人,用地址合法性验证标记可选地址
5966
const suggestions = useMemo(() => {
6067
if (!showSuggestions) return [];
61-
// 获取所有联系人的建议(不按 chainType 过滤)
62-
const allSuggestions = addressBookSelectors.suggestContacts(addressBookState, currentValue || '', undefined, maxSuggestions);
68+
const allSuggestions = addressBookSelectors.suggestContacts(addressBookState, currentValue || '', maxSuggestions);
6369
// 标记每个建议的地址是否对当前链有效
6470
return allSuggestions.map((s) => ({
6571
...s,
@@ -235,9 +241,9 @@ const AddressInput = forwardRef<HTMLInputElement, AddressInputProps>(
235241
)}>{suggestion.matchedAddress.address}</p>
236242
</div>
237243
<span className={cn(
238-
"shrink-0 text-xs uppercase",
244+
"shrink-0 text-xs",
239245
isDisabled ? "text-muted-foreground/50" : "text-muted-foreground"
240-
)}>{suggestion.matchedAddress.chainType}</span>
246+
)}>{getAddressDisplayLabel(suggestion.matchedAddress)}</span>
241247
</li>
242248
);
243249
})}

src/i18n/locales/en/common.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@
100100
"namePlaceholder": "Enter contact name",
101101
"noContacts": "No contacts",
102102
"noValidAddress": "No valid address",
103+
"addressLabel": "Address Label",
104+
"addressLabelPlaceholder": "e.g. Main, Exchange",
103105
"save": "Save",
104106
"selectAddress": "Select address",
105107
"selectContact": "Select Contact",

src/i18n/locales/zh-CN/common.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
"cancel": "取消",
6161
"noContacts": "暂无联系人",
6262
"noValidAddress": "无可用地址",
63+
"addressLabel": "地址标签",
64+
"addressLabelPlaceholder": "如:主账户、交易所",
6365
"viewAll": "查看全部联系人",
6466
"selectContact": "选择联系人",
6567
"selectAddress": "选择地址"

src/lib/qr-parser.test.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,8 @@ describe('qr-parser', () => {
222222
const contact = result as ParsedContact
223223
expect(contact.name).toBe('张三')
224224
expect(contact.addresses).toHaveLength(1)
225-
expect(contact.addresses[0]?.chainType).toBe('ethereum')
225+
// 旧格式的 chainType 会被用作 label
226+
expect(contact.addresses[0]?.label).toBe('ethereum')
226227
expect(contact.addresses[0]?.address).toBe('0x742d35Cc6634C0532925a3b844Bc9e7595f12345')
227228
})
228229

@@ -231,9 +232,9 @@ describe('qr-parser', () => {
231232
type: 'contact',
232233
name: '李四',
233234
addresses: [
234-
{ chainType: 'ethereum', address: '0x742d35Cc6634C0532925a3b844Bc9e7595f12345' },
235-
{ chainType: 'bitcoin', address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq' },
236-
{ chainType: 'tron', address: 'TJCnKsPa7y5okkXvQAidZBzqx3QyQ6sxMW' },
235+
{ label: 'ETH', address: '0x742d35Cc6634C0532925a3b844Bc9e7595f12345' },
236+
{ label: 'BTC', address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq' },
237+
{ label: 'TRX', address: 'TJCnKsPa7y5okkXvQAidZBzqx3QyQ6sxMW' },
237238
],
238239
})
239240
const result = parseQRContent(content)
@@ -246,7 +247,7 @@ describe('qr-parser', () => {
246247
const content = JSON.stringify({
247248
type: 'contact',
248249
name: '王五',
249-
addresses: [{ chainType: 'ethereum', address: '0x742d35Cc6634C0532925a3b844Bc9e7595f12345' }],
250+
addresses: [{ label: 'ETH', address: '0x742d35Cc6634C0532925a3b844Bc9e7595f12345' }],
250251
memo: '好友',
251252
avatar: '👨‍💼',
252253
})
@@ -291,7 +292,7 @@ describe('qr-parser', () => {
291292
const contact = result as ParsedContact
292293
expect(contact.name).toBe('张三')
293294
expect(contact.addresses).toHaveLength(1)
294-
expect(contact.addresses[0]?.chainType).toBe('ethereum')
295+
expect(contact.addresses[0]?.label).toBe('ETH')
295296
})
296297

297298
it('parses contact:// URI with multiple addresses', () => {
@@ -327,7 +328,7 @@ describe('qr-parser', () => {
327328
it('generates valid JSON for single address', () => {
328329
const content = generateContactQRContent({
329330
name: '张三',
330-
addresses: [{ chainType: 'ethereum', address: '0x742d35Cc6634C0532925a3b844Bc9e7595f12345' }],
331+
addresses: [{ label: 'ETH', address: '0x742d35Cc6634C0532925a3b844Bc9e7595f12345' }],
331332
})
332333
const parsed = JSON.parse(content)
333334
expect(parsed.type).toBe('contact')
@@ -339,8 +340,8 @@ describe('qr-parser', () => {
339340
const original = {
340341
name: '李四',
341342
addresses: [
342-
{ chainType: 'ethereum' as const, address: '0x742d35Cc6634C0532925a3b844Bc9e7595f12345' },
343-
{ chainType: 'bitcoin' as const, address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq' },
343+
{ label: 'ETH' as const, address: '0x742d35Cc6634C0532925a3b844Bc9e7595f12345' },
344+
{ label: 'BTC' as const, address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq' },
344345
],
345346
avatar: '👩‍💻',
346347
}
@@ -358,7 +359,7 @@ describe('qr-parser', () => {
358359
it('handles special characters in name', () => {
359360
const content = generateContactQRContent({
360361
name: '张三 (老板)',
361-
addresses: [{ chainType: 'ethereum', address: '0x742d35Cc6634C0532925a3b844Bc9e7595f12345' }],
362+
addresses: [{ label: 'ETH', address: '0x742d35Cc6634C0532925a3b844Bc9e7595f12345' }],
362363
})
363364
const result = parseQRContent(content)
364365
expect(result.type).toBe('contact')

src/lib/qr-parser.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ export interface ParsedDeepLink {
4040
raw: string
4141
}
4242

43-
/** 联系人地址 */
43+
/** 联系人地址(QR 协议格式) */
4444
export interface ContactAddressInfo {
45-
chainType: 'ethereum' | 'bitcoin' | 'tron'
4645
address: string
46+
/** 地址标签(用于显示) */
4747
label?: string | undefined
4848
}
4949

@@ -206,9 +206,9 @@ function parseContactURI(content: string): ParsedContact | null {
206206
type: 'contact',
207207
name: data.name,
208208
addresses: data.addresses.map((a: { chainType?: string; chain?: string; address: string; label?: string }) => ({
209-
chainType: a.chainType || a.chain,
210209
address: a.address,
211-
label: a.label,
210+
// 优先用 label,否则用旧格式的 chainType 作为 label
211+
label: a.label || a.chainType || a.chain,
212212
})),
213213
memo: data.memo,
214214
avatar: data.avatar,
@@ -233,20 +233,20 @@ function parseContactURI(content: string): ParsedContact | null {
233233
const params = new URLSearchParams(query)
234234
const addresses: ContactAddressInfo[] = []
235235

236-
// 解析各链地址
236+
// 解析各链地址(用链类型作为默认 label)
237237
const ethAddr = params.get('eth')
238238
if (ethAddr && ETH_ADDRESS_REGEX.test(ethAddr)) {
239-
addresses.push({ chainType: 'ethereum', address: ethAddr, label: params.get('eth_label') ?? undefined })
239+
addresses.push({ address: ethAddr, label: params.get('eth_label') || 'ETH' })
240240
}
241241

242242
const btcAddr = params.get('btc')
243243
if (btcAddr && BTC_ADDRESS_REGEX.test(btcAddr)) {
244-
addresses.push({ chainType: 'bitcoin', address: btcAddr, label: params.get('btc_label') ?? undefined })
244+
addresses.push({ address: btcAddr, label: params.get('btc_label') || 'BTC' })
245245
}
246246

247247
const trxAddr = params.get('trx')
248248
if (trxAddr && TRON_ADDRESS_REGEX.test(trxAddr)) {
249-
addresses.push({ chainType: 'tron', address: trxAddr, label: params.get('trx_label') ?? undefined })
249+
addresses.push({ address: trxAddr, label: params.get('trx_label') || 'TRX' })
250250
}
251251

252252
if (addresses.length === 0) return null
@@ -273,7 +273,6 @@ export function generateContactQRContent(contact: {
273273
type: 'contact',
274274
name: contact.name,
275275
addresses: contact.addresses.map(a => ({
276-
chainType: a.chainType,
277276
address: a.address,
278277
label: a.label,
279278
})),

src/pages/address-book/index.stories.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export default meta
1616
type Story = StoryObj<typeof AddressBookPage>
1717

1818
// Helper to create addresses
19-
function createAddresses(address: string, chainType = 'ethereum') {
20-
return [{ id: crypto.randomUUID(), address, chainType: chainType as 'ethereum' }]
19+
function createAddresses(address: string, label = 'ETH') {
20+
return [{ id: crypto.randomUUID(), address, label }]
2121
}
2222

2323
const resetStores = () => {
@@ -99,9 +99,9 @@ export const MultipleAddresses: Story = {
9999
addressBookActions.addContact({
100100
name: 'Multi-Chain User',
101101
addresses: [
102-
{ id: '1', address: '0x1234567890abcdef1234567890abcdef12345678', chainType: 'ethereum' },
103-
{ id: '2', address: 'b7ADmvZJJ3n3aDxkvwbXxJX1oGgeiCzL11', chainType: 'bfmeta' },
104-
{ id: '3', address: 'TJYs1234567890abcdef1234567890abc', chainType: 'tron' },
102+
{ id: '1', address: '0x1234567890abcdef1234567890abcdef12345678', label: 'ETH' },
103+
{ id: '2', address: 'b7ADmvZJJ3n3aDxkvwbXxJX1oGgeiCzL11', label: 'BFMETA' },
104+
{ id: '3', address: 'TJYs1234567890abcdef1234567890abc', label: 'TRX' },
105105
],
106106
memo: '多链用户',
107107
})

src/pages/address-book/index.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { addressBookActions, walletStore } from '@/stores'
66
const mockPush = vi.fn()
77

88
// Helper to create addresses
9-
function createAddresses(address: string, chainType = 'ethereum') {
10-
return [{ id: crypto.randomUUID(), address, chainType: chainType as 'ethereum' }]
9+
function createAddresses(address: string, label = 'ETH') {
10+
return [{ id: crypto.randomUUID(), address, label }]
1111
}
1212

1313
// Mock dependencies
@@ -148,8 +148,8 @@ describe('AddressBookPage', () => {
148148
addressBookActions.addContact({
149149
name: 'Multi',
150150
addresses: [
151-
{ id: '1', address: '0x1111', chainType: 'ethereum' },
152-
{ id: '2', address: 'b7ADmv...', chainType: 'bfmeta' },
151+
{ id: '1', address: '0x1111', label: 'ETH' },
152+
{ id: '2', address: 'b7ADmv...', label: 'BFMETA' },
153153
],
154154
})
155155

src/pages/address-book/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ export function AddressBookPage() {
6565
name: contact.name,
6666
addresses: JSON.stringify(
6767
contact.addresses.map((a) => ({
68-
chainType: a.chainType,
6968
address: a.address,
7069
label: a.label,
7170
})),

0 commit comments

Comments
 (0)