diff --git "a/docs/white-book/04-\346\234\215\345\212\241\347\257\207/02-\351\223\276\346\234\215\345\212\241/IIdentityService.md" "b/docs/white-book/04-\346\234\215\345\212\241\347\257\207/02-\351\223\276\346\234\215\345\212\241/IIdentityService.md"
index 0905a8fec..55d9bb2a0 100644
--- "a/docs/white-book/04-\346\234\215\345\212\241\347\257\207/02-\351\223\276\346\234\215\345\212\241/IIdentityService.md"
+++ "b/docs/white-book/04-\346\234\215\345\212\241\347\257\207/02-\351\223\276\346\234\215\345\212\241/IIdentityService.md"
@@ -102,11 +102,14 @@ IIdentityService {
## 派生路径规范
-| 链类型 | 派生路径 | 算法 |
-|-------|---------|------|
-| EVM 兼容 | m/44'/60'/0'/0/{index} | secp256k1 |
-| Bitcoin | m/44'/0'/0'/0/{index} | secp256k1 |
-| BFM 链 | m/44'/9999'/0'/0/{index} | Ed25519 |
+| 链类型 | 派生路径 | 算法 | 地址格式 |
+|-------|---------|------|---------|
+| EVM 兼容 | m/44'/60'/0'/0/{index} | secp256k1 | 0x... (checksum) |
+| Bitcoin | m/84'/0'/0'/0/{index} | secp256k1 | bc1q... (Native SegWit) |
+| Tron | m/44'/195'/0'/0/{index} | secp256k1 | T... (Base58Check) |
+| BioForest | m/44'/9999'/0'/0/{index} | Ed25519 | b... (Base58) |
+
+> **注意**: Bitcoin 使用 BIP84 路径(Native SegWit),不再使用 BIP44 Legacy 路径。
### 派生流程
diff --git "a/docs/white-book/04-\346\234\215\345\212\241\347\257\207/07-\351\223\276\351\205\215\347\275\256\347\256\241\347\220\206/index.md" "b/docs/white-book/04-\346\234\215\345\212\241\347\257\207/07-\351\223\276\351\205\215\347\275\256\347\256\241\347\220\206/index.md"
index c209d1b1e..53b9701fb 100644
--- "a/docs/white-book/04-\346\234\215\345\212\241\347\257\207/07-\351\223\276\351\205\215\347\275\256\347\256\241\347\220\206/index.md"
+++ "b/docs/white-book/04-\346\234\215\345\212\241\347\257\207/07-\351\223\276\351\205\215\347\275\256\347\256\241\347\220\206/index.md"
@@ -97,7 +97,7 @@ interface ChainConfig {
// 必需字段
id: string // 唯一标识,格式: /^[a-z0-9-]+$/
version: string // 配置版本,格式: "major.minor"
- type: ChainConfigType // 链类型
+ chainKind: ChainKind // 链类型(严格枚举,不支持 custom)
name: string // 显示名称
symbol: string // 原生代币符号
decimals: number // 原生代币精度
@@ -113,7 +113,7 @@ interface ChainConfig {
source: ChainConfigSource // 配置来源
}
-type ChainConfigType = 'bioforest' | 'evm' | 'bip39' | 'custom'
+type ChainKind = 'bioforest' | 'evm' | 'bitcoin' | 'tron'
type ChainConfigSource = 'default' | 'subscription' | 'manual'
```
@@ -156,18 +156,37 @@ EVM 兼容链,使用 BIP44 派生(coin type 60):
**支持的链**: Ethereum, BSC, Polygon 等
-### bip39
+### bitcoin
-其他使用 BIP39/BIP44 的链:
+Bitcoin 网络,使用 BIP84 派生(Native SegWit):
-| 链 | Coin Type | 说明 |
-|---|-----------|------|
-| Bitcoin | 0 | UTXO 模型 |
-| Tron | 195 | 账户模型 |
+| 字段 | 说明 |
+|-----|------|
+| decimals | 8 |
+| 地址格式 | `bc1q...` (Native SegWit P2WPKH) |
+
+**派生路径**: `m/84'/0'/account'/0/index`
+
+### tron
+
+Tron 网络,使用 BIP44 派生(coin type 195):
+
+| 字段 | 说明 |
+|-----|------|
+| decimals | 6 |
+| 地址格式 | `T...` (Base58Check) |
+
+**派生路径**: `m/44'/195'/account'/0/index`
+
+---
+
+## 严格类型验证
-### custom
+从版本 2.0.0 起,`chainKind` 采用严格枚举验证:
-未知类型,用于向前兼容。应用 **MAY** 显示但 **MUST NOT** 执行交易操作。
+- **MUST** 使用 `bioforest`、`evm`、`bitcoin`、`tron` 之一
+- **MUST NOT** 使用 `custom` 或 `bip39`(已废弃)
+- 未知类型将导致配置加载失败,而不是静默降级
---
diff --git a/public/configs/testnet-chains.json b/public/configs/testnet-chains.json
index 383c901a7..4f1490b5f 100644
--- a/public/configs/testnet-chains.json
+++ b/public/configs/testnet-chains.json
@@ -1,62 +1,73 @@
-[
- {
- "id": "ethereum-sepolia",
- "version": "1.0",
- "type": "evm",
- "name": "Ethereum Sepolia",
- "symbol": "SepoliaETH",
- "icon": "../icons/ethereum/chain.svg",
- "decimals": 18,
- "api": { "url": "https://rpc.sepolia.org", "path": "eth" },
- "explorer": {
- "url": "https://sepolia.etherscan.io",
- "queryTx": "https://sepolia.etherscan.io/tx/:hash",
- "queryAddress": "https://sepolia.etherscan.io/address/:address"
+{
+ "version": "2.0.0",
+ "chains": [
+ {
+ "id": "ethereum-sepolia",
+ "version": "1.0",
+ "chainKind": "evm",
+ "name": "Ethereum Sepolia",
+ "symbol": "SepoliaETH",
+ "icon": "../icons/ethereum/chain.svg",
+ "decimals": 18,
+ "api": {
+ "ethereum-rpc": "https://rpc.sepolia.org"
+ },
+ "explorer": {
+ "url": "https://sepolia.etherscan.io",
+ "queryTx": "https://sepolia.etherscan.io/tx/:hash",
+ "queryAddress": "https://sepolia.etherscan.io/address/:address"
+ }
+ },
+ {
+ "id": "bsc-testnet",
+ "version": "1.0",
+ "chainKind": "evm",
+ "name": "BSC Testnet",
+ "symbol": "tBNB",
+ "icon": "../icons/binance/chain.svg",
+ "decimals": 18,
+ "api": {
+ "ethereum-rpc": "https://bsc-testnet-rpc.publicnode.com"
+ },
+ "explorer": {
+ "url": "https://testnet.bscscan.com",
+ "queryTx": "https://testnet.bscscan.com/tx/:hash",
+ "queryAddress": "https://testnet.bscscan.com/address/:address"
+ }
+ },
+ {
+ "id": "tron-nile",
+ "version": "1.0",
+ "chainKind": "tron",
+ "name": "Tron Nile Testnet",
+ "symbol": "TRX",
+ "icon": "../icons/tron/chain.svg",
+ "decimals": 6,
+ "api": {
+ "tron-rpc": "https://nile.trongrid.io"
+ },
+ "explorer": {
+ "url": "https://nile.tronscan.org",
+ "queryTx": "https://nile.tronscan.org/#/transaction/:hash",
+ "queryAddress": "https://nile.tronscan.org/#/address/:address"
+ }
+ },
+ {
+ "id": "bitcoin-signet",
+ "version": "1.0",
+ "chainKind": "bitcoin",
+ "name": "Bitcoin Signet",
+ "symbol": "sBTC",
+ "icon": "../icons/bitcoin/chain.svg",
+ "decimals": 8,
+ "api": {
+ "mempool-v1": "https://mempool.space/signet/api"
+ },
+ "explorer": {
+ "url": "https://mempool.space/signet",
+ "queryTx": "https://mempool.space/signet/tx/:hash",
+ "queryAddress": "https://mempool.space/signet/address/:address"
+ }
}
- },
- {
- "id": "bsc-testnet",
- "version": "1.0",
- "type": "evm",
- "name": "BSC Testnet",
- "symbol": "tBNB",
- "icon": "../icons/binance/chain.svg",
- "decimals": 18,
- "api": { "url": "https://bsc-testnet-rpc.publicnode.com", "path": "bnb" },
- "explorer": {
- "url": "https://testnet.bscscan.com",
- "queryTx": "https://testnet.bscscan.com/tx/:hash",
- "queryAddress": "https://testnet.bscscan.com/address/:address"
- }
- },
- {
- "id": "tron-nile",
- "version": "1.0",
- "type": "bip39",
- "name": "Tron Nile Testnet",
- "symbol": "TRX",
- "icon": "../icons/tron/chain.svg",
- "decimals": 6,
- "api": { "url": "https://nile.trongrid.io", "path": "tron" },
- "explorer": {
- "url": "https://nile.tronscan.org",
- "queryTx": "https://nile.tronscan.org/#/transaction/:hash",
- "queryAddress": "https://nile.tronscan.org/#/address/:address"
- }
- },
- {
- "id": "bitcoin-signet",
- "version": "1.0",
- "type": "bip39",
- "name": "Bitcoin Signet",
- "symbol": "sBTC",
- "icon": "../icons/bitcoin/chain.svg",
- "decimals": 8,
- "api": { "url": "https://mempool.space/signet/api", "path": "btc" },
- "explorer": {
- "url": "https://mempool.space/signet",
- "queryTx": "https://mempool.space/signet/tx/:hash",
- "queryAddress": "https://mempool.space/signet/address/:address"
- }
- }
-]
+ ]
+}
diff --git a/src/lib/crypto/address-derivation.test.ts b/src/lib/crypto/address-derivation.test.ts
deleted file mode 100644
index f72667ed6..000000000
--- a/src/lib/crypto/address-derivation.test.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import { deriveAddressesForChains, canDeriveForChainType } from './address-derivation'
-import type { ChainConfig } from '@/services/chain-config'
-
-const TEST_MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
-
-const makeBioforestConfig = (id: string, prefix = 'b'): ChainConfig => ({
- id,
- version: '1.0',
- chainKind: 'bioforest',
- name: id,
- symbol: id.toUpperCase(),
- prefix,
- decimals: 8,
- enabled: true,
- source: 'default',
-})
-
-const makeEvmConfig = (id: string): ChainConfig => ({
- id,
- version: '1.0',
- chainKind: 'evm',
- name: id,
- symbol: id.toUpperCase(),
- decimals: 18,
- enabled: true,
- source: 'default',
-})
-
-const makeBip39Config = (id: string, decimals = 8): ChainConfig => ({
- id,
- version: '1.0',
- chainKind: 'bitcoin',
- name: id,
- symbol: id.toUpperCase(),
- decimals,
- enabled: true,
- source: 'default',
-})
-
-describe('deriveAddressesForChains', () => {
- describe('bioforest chains', () => {
- it('derives addresses for bioforest chains', () => {
- const configs = [makeBioforestConfig('bfmeta'), makeBioforestConfig('ccchain')]
- const addresses = deriveAddressesForChains(TEST_MNEMONIC, configs)
-
- expect(addresses).toHaveLength(2)
- expect(addresses.every((a) => a.address.startsWith('b'))).toBe(true)
- expect(addresses.map((a) => a.chainId)).toEqual(['bfmeta', 'ccchain'])
- })
-
- it('derives addresses with correct prefixes', () => {
- const configs = [
- makeBioforestConfig('chain-b', 'b'),
- makeBioforestConfig('chain-c', 'c'),
- ]
- const addresses = deriveAddressesForChains(TEST_MNEMONIC, configs)
-
- expect(addresses).toHaveLength(2)
- expect(addresses[0]!.address.startsWith('b')).toBe(true)
- expect(addresses[1]!.address.startsWith('c')).toBe(true)
- // Same keypair derives to same base address, only prefix differs
- expect(addresses[0]!.address.slice(1)).toBe(addresses[1]!.address.slice(1))
- })
- })
-
- describe('evm chains', () => {
- it('derives addresses for EVM chains', () => {
- const configs = [makeEvmConfig('ethereum'), makeEvmConfig('binance')]
- const addresses = deriveAddressesForChains(TEST_MNEMONIC, configs)
-
- expect(addresses).toHaveLength(2)
- expect(addresses.every((a) => a.address.startsWith('0x'))).toBe(true)
- // All EVM chains share the same address
- expect(addresses[0]!.address).toBe(addresses[1]!.address)
- })
-
- it('generates checksum addresses', () => {
- const configs = [makeEvmConfig('ethereum')]
- const addresses = deriveAddressesForChains(TEST_MNEMONIC, configs)
-
- // Checksum address has mixed case
- const address = addresses[0]!.address
- expect(address).toMatch(/^0x[a-fA-F0-9]{40}$/)
- expect(address !== address.toLowerCase()).toBe(true)
- })
- })
-
- describe('bip39 chains', () => {
- it('derives Bitcoin address (Native SegWit)', () => {
- const configs = [makeBip39Config('bitcoin')]
- const addresses = deriveAddressesForChains(TEST_MNEMONIC, configs)
-
- expect(addresses).toHaveLength(1)
- // Native SegWit addresses start with bc1q
- expect(addresses[0]!.address.startsWith('bc1q')).toBe(true)
- })
-
- it('derives Tron address', () => {
- const configs = [makeBip39Config('tron', 6)]
- const addresses = deriveAddressesForChains(TEST_MNEMONIC, configs)
-
- expect(addresses).toHaveLength(1)
- // Tron addresses start with T
- expect(addresses[0]!.address.startsWith('T')).toBe(true)
- })
-
- it('skips unknown bip39 chains', () => {
- const configs = [makeBip39Config('unknown-chain')]
- const addresses = deriveAddressesForChains(TEST_MNEMONIC, configs)
-
- expect(addresses).toHaveLength(0)
- })
- })
-
- describe('mixed chain types', () => {
- it('derives addresses for all chain types', () => {
- const configs = [
- makeBioforestConfig('bfmeta'),
- makeEvmConfig('ethereum'),
- makeBip39Config('bitcoin'),
- makeBip39Config('tron', 6),
- ]
- const addresses = deriveAddressesForChains(TEST_MNEMONIC, configs)
-
- expect(addresses).toHaveLength(4)
-
- const addressMap = new Map(addresses.map((a) => [a.chainId, a.address]))
- expect(addressMap.get('bfmeta')!.startsWith('b')).toBe(true)
- expect(addressMap.get('ethereum')!.startsWith('0x')).toBe(true)
- expect(addressMap.get('bitcoin')!.startsWith('bc1q')).toBe(true)
- expect(addressMap.get('tron')!.startsWith('T')).toBe(true)
- })
-
- it('returns empty array for empty input', () => {
- const addresses = deriveAddressesForChains(TEST_MNEMONIC, [])
- expect(addresses).toEqual([])
- })
- })
-})
-
-describe('canDeriveForChainType', () => {
- it('returns true for supported chain types', () => {
- expect(canDeriveForChainType('bioforest')).toBe(true)
- expect(canDeriveForChainType('evm')).toBe(true)
- expect(canDeriveForChainType('bip39')).toBe(true)
- })
-
- it('returns false for unsupported chain types', () => {
- expect(canDeriveForChainType('custom')).toBe(false)
- })
-})
diff --git a/src/lib/crypto/address-derivation.ts b/src/lib/crypto/address-derivation.ts
deleted file mode 100644
index 368fbfa12..000000000
--- a/src/lib/crypto/address-derivation.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-/**
- * Unified Address Derivation
- *
- * Supports deriving addresses for all chain types:
- * - bioforest: Ed25519-based chains (BFMeta, CCChain, etc.)
- * - evm: Ethereum-compatible chains (Ethereum, BSC, etc.)
- * - bip39: BIP44-based chains (Bitcoin, Tron)
- */
-
-import type { ChainConfig } from '@/services/chain-config'
-import { deriveBioforestAddressesFromChainConfigs } from './bioforest'
-import { deriveKey, deriveBitcoinKey, type BitcoinPurpose } from './derivation'
-
-export interface DerivedAddress {
- chainId: string
- address: string
- /** 公钥 (hex) */
- publicKey: string
-}
-
-/**
- * Derive addresses for multiple chains based on their type
- *
- * @param secret - Mnemonic phrase or secret key
- * @param chainConfigs - Array of chain configurations to derive addresses for
- * @returns Array of derived addresses with their chain IDs
- */
-export function deriveAddressesForChains(
- secret: string,
- chainConfigs: readonly ChainConfig[],
-): DerivedAddress[] {
- const results: DerivedAddress[] = []
-
- // Group chains by type
- const bioforestChains = chainConfigs.filter((c) => c.chainKind === 'bioforest')
- const evmChains = chainConfigs.filter((c) => c.chainKind === 'evm')
- const bip39Chains = chainConfigs.filter((c) => c.chainKind === 'bitcoin')
-
- // Derive bioforest addresses (Ed25519-based, same address for all chains with same prefix)
- if (bioforestChains.length > 0) {
- const bioforestAddresses = deriveBioforestAddressesFromChainConfigs(secret, bioforestChains)
- results.push(...bioforestAddresses)
- }
-
- // Derive EVM addresses (secp256k1-based, same address for all EVM chains)
- if (evmChains.length > 0) {
- const evmKey = deriveKey(secret, 'ethereum', 0, 0)
- for (const chain of evmChains) {
- results.push({
- chainId: chain.id,
- address: evmKey.address,
- publicKey: evmKey.publicKey,
- })
- }
- }
-
- // Derive BIP39 addresses (chain-specific derivation paths)
- for (const chain of bip39Chains) {
- const derived = deriveBip39Key(secret, chain)
- if (derived) {
- results.push({
- chainId: chain.id,
- address: derived.address,
- publicKey: derived.publicKey,
- })
- }
- }
-
- return results
-}
-
-/**
- * Derive key for a BIP39-based chain
- */
-function deriveBip39Key(secret: string, chain: ChainConfig): { address: string; publicKey: string } | null {
- switch (chain.id) {
- case 'bitcoin': {
- // Use Native SegWit (BIP84) as default for Bitcoin
- const btcKey = deriveBitcoinKey(secret, 84 as BitcoinPurpose, 0, 0)
- return { address: btcKey.address, publicKey: btcKey.publicKey }
- }
- case 'tron': {
- const tronKey = deriveKey(secret, 'tron', 0, 0)
- return { address: tronKey.address, publicKey: tronKey.publicKey }
- }
- default:
- // Unknown BIP39 chain - skip
- console.warn(`[deriveAddressesForChains] Unknown BIP39 chain: ${chain.id}`)
- return null
- }
-}
-
-/**
- * Check if a mnemonic can derive addresses for the given chain type
- */
-export function canDeriveForChainType(chainType: ChainConfig['type']): boolean {
- return chainType === 'bioforest' || chainType === 'evm' || chainType === 'bip39'
-}
diff --git a/src/lib/crypto/index.ts b/src/lib/crypto/index.ts
index c1e42824b..6af5c4e59 100644
--- a/src/lib/crypto/index.ts
+++ b/src/lib/crypto/index.ts
@@ -33,6 +33,7 @@ export {
deriveKey,
deriveMultiChainKeys,
deriveHDKey,
+ deriveBitcoinKey,
getBIP44Path,
toChecksumAddress,
isValidAddress,
@@ -40,15 +41,9 @@ export {
deriveEncryptionKeyFromSecret,
type DerivedKey,
type ChainType as DeriveChainType,
+ type BitcoinPurpose,
} from './derivation'
-// Unified address derivation for all chain types
-export {
- deriveAddressesForChains,
- canDeriveForChainType,
- type DerivedAddress,
-} from './address-derivation'
-
// BioForestChain (Ed25519-based chains)
export {
createBioforestKeypair,
diff --git a/src/pages/onboarding/recover.tsx b/src/pages/onboarding/recover.tsx
index 579f30af6..316e0f57c 100644
--- a/src/pages/onboarding/recover.tsx
+++ b/src/pages/onboarding/recover.tsx
@@ -17,8 +17,7 @@ import { GradientButton } from '@/components/common/gradient-button';
import { IconCircle } from '@/components/common/icon-circle';
import { LoadingSpinner } from '@/components/common/loading-spinner';
import { useDuplicateDetection } from '@/hooks/use-duplicate-detection';
-import { deriveBioforestAddresses } from '@/lib/crypto';
-import { buildWalletChainAddresses } from '@/services/wallet/chain-derivation';
+import { deriveWalletChainAddresses } from '@/services/chain-adapter';
import { deriveThemeHue } from '@/hooks/useWalletTheme';
import { useChainConfigs, useChainConfigState, useEnabledBioforestChainConfigs, walletActions } from '@/stores';
import type { IWalletQuery } from '@/services/wallet/types';
@@ -187,16 +186,17 @@ export function OnboardingRecoverPage() {
try {
const mnemonicStr = words.join(' ');
- // 使用统一的 chain-derivation 模块派生所有地址
- const derivedAddresses = buildWalletChainAddresses({
+ // 使用 chain-adapter 统一派生所有地址(单一真相)
+ const derivedAddresses = await deriveWalletChainAddresses({
mnemonic: mnemonicStr,
selectedChainIds,
chainConfigs,
});
- const chainAddresses = derivedAddresses.map(({ chainId, address }) => ({
+ const chainAddresses = derivedAddresses.map(({ chainId, address, publicKey }) => ({
chain: chainId,
address,
+ publicKey: publicKey ?? '',
tokens: [],
}));
@@ -247,6 +247,7 @@ export function OnboardingRecoverPage() {
const chainAddresses = arbitraryDerivedAddresses.map((item) => ({
chain: item.chainId,
address: item.address,
+ publicKey: '',
tokens: [],
}));
diff --git a/src/pages/settings/chain-config.tsx b/src/pages/settings/chain-config.tsx
index aa8b14ac8..121e8d187 100644
--- a/src/pages/settings/chain-config.tsx
+++ b/src/pages/settings/chain-config.tsx
@@ -328,7 +328,7 @@ export function ChainConfigPage() {
{config.id}
v{config.version}
- {config.type}
+ {config.chainKind}
{config.symbol}
{warning?.kind === 'incompatible_major' && (
diff --git a/src/pages/wallet/create.tsx b/src/pages/wallet/create.tsx
index 040e01502..d8060a16a 100644
--- a/src/pages/wallet/create.tsx
+++ b/src/pages/wallet/create.tsx
@@ -23,7 +23,7 @@ import {
} from '@tabler/icons-react';
import { useChainConfigs, walletActions } from '@/stores';
import { generateMnemonic } from '@/lib/crypto';
-import { buildWalletChainAddresses } from '@/services/wallet/chain-derivation';
+import { deriveWalletChainAddresses } from '@/services/chain-adapter';
import { deriveThemeHue } from '@/hooks/useWalletTheme';
import type { ChainConfig } from '@/services/chain-config';
@@ -103,16 +103,17 @@ export function WalletCreatePage() {
try {
const mnemonicStr = mnemonic.join(' ');
- // 使用统一的 chain-derivation 模块派生所有地址
- const derivedAddresses = buildWalletChainAddresses({
+ // 使用 chain-adapter 统一派生所有地址(单一真相)
+ const derivedAddresses = await deriveWalletChainAddresses({
mnemonic: mnemonicStr,
selectedChainIds,
chainConfigs,
});
- const chainAddresses = derivedAddresses.map(({ chainId, address }) => ({
+ const chainAddresses = derivedAddresses.map(({ chainId, address, publicKey }) => ({
chain: chainId,
address,
+ publicKey: publicKey ?? '',
tokens: [],
}));
diff --git a/src/services/chain-adapter/__tests__/bitcoin-adapter.test.ts b/src/services/chain-adapter/__tests__/bitcoin-adapter.test.ts
index c9c263c3d..4f4fe5a44 100644
--- a/src/services/chain-adapter/__tests__/bitcoin-adapter.test.ts
+++ b/src/services/chain-adapter/__tests__/bitcoin-adapter.test.ts
@@ -1,9 +1,10 @@
import { describe, it, expect } from 'vitest'
import { BitcoinAdapter } from '../bitcoin'
import type { ChainConfig } from '@/services/chain-config'
-import { mnemonicToSeedSync } from '@scure/bip39'
const TEST_MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
+// Identity service expects UTF-8 encoded mnemonic
+const mnemonicAsUint8 = new TextEncoder().encode(TEST_MNEMONIC)
const btcConfig: ChainConfig = {
id: 'bitcoin',
@@ -21,19 +22,17 @@ describe('BitcoinAdapter', () => {
describe('BitcoinIdentityService', () => {
it('derives correct P2WPKH address from mnemonic', async () => {
- const seed = mnemonicToSeedSync(TEST_MNEMONIC)
- const address = await adapter.identity.deriveAddress(seed, 0)
-
+ const address = await adapter.identity.deriveAddress(mnemonicAsUint8, 0)
+
// P2WPKH (bc1q...) address for this mnemonic at BIP84 path m/84'/0'/0'/0/0
expect(address).toMatch(/^bc1q[a-z0-9]{38,}$/)
expect(address.startsWith('bc1q')).toBe(true)
})
it('derives different addresses for different indices', async () => {
- const seed = mnemonicToSeedSync(TEST_MNEMONIC)
- const addr0 = await adapter.identity.deriveAddress(seed, 0)
- const addr1 = await adapter.identity.deriveAddress(seed, 1)
-
+ const addr0 = await adapter.identity.deriveAddress(mnemonicAsUint8, 0)
+ const addr1 = await adapter.identity.deriveAddress(mnemonicAsUint8, 1)
+
expect(addr0).not.toBe(addr1)
})
diff --git a/src/services/chain-adapter/__tests__/tron-adapter.test.ts b/src/services/chain-adapter/__tests__/tron-adapter.test.ts
index bb3051cd2..0fd701b1a 100644
--- a/src/services/chain-adapter/__tests__/tron-adapter.test.ts
+++ b/src/services/chain-adapter/__tests__/tron-adapter.test.ts
@@ -1,9 +1,10 @@
import { describe, it, expect } from 'vitest'
import { TronAdapter } from '../tron'
import type { ChainConfig } from '@/services/chain-config'
-import { mnemonicToSeedSync } from '@scure/bip39'
const TEST_MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
+// Identity service expects UTF-8 encoded mnemonic
+const mnemonicAsUint8 = new TextEncoder().encode(TEST_MNEMONIC)
const tronConfig: ChainConfig = {
id: 'tron',
@@ -21,9 +22,8 @@ describe('TronAdapter', () => {
describe('TronIdentityService', () => {
it('derives correct Tron address from mnemonic', async () => {
- const seed = mnemonicToSeedSync(TEST_MNEMONIC)
- const address = await adapter.identity.deriveAddress(seed, 0)
-
+ const address = await adapter.identity.deriveAddress(mnemonicAsUint8, 0)
+
// Tron addresses start with 'T' and are 34 characters
expect(address).toMatch(/^T[A-Za-z0-9]{33}$/)
expect(address.startsWith('T')).toBe(true)
@@ -31,10 +31,9 @@ describe('TronAdapter', () => {
})
it('derives different addresses for different indices', async () => {
- const seed = mnemonicToSeedSync(TEST_MNEMONIC)
- const addr0 = await adapter.identity.deriveAddress(seed, 0)
- const addr1 = await adapter.identity.deriveAddress(seed, 1)
-
+ const addr0 = await adapter.identity.deriveAddress(mnemonicAsUint8, 0)
+ const addr1 = await adapter.identity.deriveAddress(mnemonicAsUint8, 1)
+
expect(addr0).not.toBe(addr1)
})
diff --git a/src/services/chain-adapter/bioforest/adapter.ts b/src/services/chain-adapter/bioforest/adapter.ts
index 3f003f842..c5cb65321 100644
--- a/src/services/chain-adapter/bioforest/adapter.ts
+++ b/src/services/chain-adapter/bioforest/adapter.ts
@@ -2,7 +2,7 @@
* BioForest Chain Adapter
*/
-import type { ChainConfigType } from '@/services/chain-config'
+import type { ChainKind } from '@/services/chain-config'
import type { IChainAdapter, IStakingService } from '../types'
import { BioforestIdentityService } from './identity-service'
import { BioforestAssetService } from './asset-service'
@@ -11,7 +11,7 @@ import { BioforestChainService } from './chain-service'
export class BioforestAdapter implements IChainAdapter {
readonly chainId: string
- readonly chainType: ChainConfigType = 'bioforest'
+ readonly chainType: ChainKind = 'bioforest'
readonly identity: BioforestIdentityService
readonly asset: BioforestAssetService
diff --git a/src/services/chain-adapter/bip39/adapter.ts b/src/services/chain-adapter/bip39/adapter.ts
deleted file mode 100644
index bdf1c785d..000000000
--- a/src/services/chain-adapter/bip39/adapter.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * BIP39 Chain Adapter (Bitcoin, Tron)
- */
-
-import type { ChainConfig, ChainConfigType } from '@/services/chain-config'
-import type { IChainAdapter, IStakingService } from '../types'
-import { Bip39IdentityService } from './identity-service'
-import { Bip39AssetService } from './asset-service'
-import { Bip39TransactionService } from './transaction-service'
-import { Bip39ChainService } from './chain-service'
-
-export class Bip39Adapter implements IChainAdapter {
- readonly chainId: string
- readonly chainType: ChainConfigType = 'bip39'
-
- readonly identity: Bip39IdentityService
- readonly asset: Bip39AssetService
- readonly transaction: Bip39TransactionService
- readonly chain: Bip39ChainService
- readonly staking: IStakingService | null = null
-
- private initialized = false
-
- constructor(config: ChainConfig) {
- this.chainId = config.id
- this.identity = new Bip39IdentityService(config)
- this.asset = new Bip39AssetService(config)
- this.transaction = new Bip39TransactionService(config)
- this.chain = new Bip39ChainService(config)
- }
-
- async initialize(_config: ChainConfig): Promise {
- if (this.initialized) return
- this.initialized = true
- }
-
- dispose(): void {
- this.initialized = false
- }
-}
-
-export function createBip39Adapter(config: ChainConfig): IChainAdapter {
- return new Bip39Adapter(config)
-}
diff --git a/src/services/chain-adapter/bip39/asset-service.ts b/src/services/chain-adapter/bip39/asset-service.ts
deleted file mode 100644
index b5f392bd5..000000000
--- a/src/services/chain-adapter/bip39/asset-service.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-/**
- * BIP39 Asset Service (使用 BioWallet API)
- */
-
-import type { ChainConfig } from '@/services/chain-config'
-import { chainConfigService } from '@/services/chain-config'
-import type { IAssetService, Address, Balance, TokenMetadata } from '../types'
-import { Amount } from '@/types/amount'
-import { ChainServiceError, ChainErrorCodes } from '../types'
-
-export class Bip39AssetService implements IAssetService {
- private readonly config: ChainConfig
- private readonly apiUrl: string
- private readonly apiPath: string
-
- constructor(config: ChainConfig) {
- this.config = config
- const biowalletApi = chainConfigService.getBiowalletApi(config.id)
- this.apiUrl = biowalletApi?.endpoint ?? ''
- this.apiPath = biowalletApi?.path ?? config.id
- }
-
- private async fetch(endpoint: string, body?: unknown): Promise {
- const url = `${this.apiUrl}/wallet/${this.apiPath}${endpoint}`
- const init: RequestInit = body
- ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
- : { method: 'GET' }
- const response = await fetch(url, init)
-
- if (!response.ok) {
- throw new ChainServiceError(
- ChainErrorCodes.NETWORK_ERROR,
- `HTTP ${response.status}: ${response.statusText}`,
- )
- }
-
- const json = await response.json() as { success: boolean; result?: T; error?: { message: string } }
- if (!json.success) {
- throw new ChainServiceError(
- ChainErrorCodes.NETWORK_ERROR,
- json.error?.message ?? 'API request failed',
- )
- }
-
- return json.result as T
- }
-
- async getNativeBalance(address: Address): Promise {
- try {
- const result = await this.fetch<{ balance: string }>('/address/balance', { address })
- return {
- amount: Amount.fromRaw(result.balance, this.config.decimals, this.config.symbol),
- symbol: this.config.symbol,
- }
- } catch {
- return {
- amount: Amount.fromRaw('0', this.config.decimals, this.config.symbol),
- symbol: this.config.symbol,
- }
- }
- }
-
- async getTokenBalance(address: Address, tokenAddress: Address): Promise {
- // Bitcoin doesn't have tokens, Tron has TRC20
- if (this.config.id !== 'tron') {
- return {
- amount: Amount.fromRaw('0', 18, 'UNKNOWN'),
- symbol: 'UNKNOWN',
- }
- }
-
- try {
- const result = await this.fetch<{ balance: string; decimals: number; symbol: string }>(
- '/token/balance',
- { address, tokenAddress },
- )
- return {
- amount: Amount.fromRaw(result.balance, result.decimals, result.symbol),
- symbol: result.symbol,
- }
- } catch {
- return {
- amount: Amount.fromRaw('0', 18, 'UNKNOWN'),
- symbol: 'UNKNOWN',
- }
- }
- }
-
- async getTokenBalances(address: Address): Promise {
- const nativeBalance = await this.getNativeBalance(address)
- return [nativeBalance]
- }
-
- async getTokenMetadata(tokenAddress: Address): Promise {
- return {
- address: tokenAddress,
- name: 'Unknown Token',
- symbol: 'UNKNOWN',
- decimals: 18,
- }
- }
-}
diff --git a/src/services/chain-adapter/bip39/chain-service.ts b/src/services/chain-adapter/bip39/chain-service.ts
deleted file mode 100644
index 343b3e0b0..000000000
--- a/src/services/chain-adapter/bip39/chain-service.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * BIP39 Chain Service (Bitcoin)
- */
-
-import type { ChainConfig } from '@/services/chain-config'
-import { chainConfigService } from '@/services/chain-config'
-import type { IChainService, ChainInfo, GasPrice, HealthStatus } from '../types'
-import { Amount } from '@/types/amount'
-import { ChainServiceError, ChainErrorCodes } from '../types'
-
-export class Bip39ChainService implements IChainService {
- private readonly config: ChainConfig
- private readonly apiUrl: string
-
- constructor(config: ChainConfig) {
- this.config = config
- // 使用 mempool-* API
- this.apiUrl = chainConfigService.getMempoolApi(config.id) ?? 'https://mempool.space/api'
- }
-
- private async fetch(endpoint: string): Promise {
- const url = `${this.apiUrl}${endpoint}`
- const response = await fetch(url)
-
- if (!response.ok) {
- throw new ChainServiceError(
- ChainErrorCodes.NETWORK_ERROR,
- `HTTP ${response.status}: ${response.statusText}`,
- )
- }
-
- const json = await response.json() as { success: boolean; result?: T; error?: { message: string } }
- if (!json.success) {
- throw new ChainServiceError(
- ChainErrorCodes.NETWORK_ERROR,
- json.error?.message ?? 'API request failed',
- )
- }
-
- return json.result as T
- }
-
- getChainInfo(): ChainInfo {
- const blockTime = this.config.id === 'bitcoin' ? 600 : 3 // BTC ~10min, TRX ~3s
- const confirmations = this.config.id === 'bitcoin' ? 6 : 19
-
- return {
- chainId: this.config.id,
- name: this.config.name,
- symbol: this.config.symbol,
- decimals: this.config.decimals,
- blockTime,
- confirmations,
- explorerUrl: this.config.explorer?.url,
- }
- }
-
- async getBlockHeight(): Promise {
- try {
- const result = await this.fetch<{ height: number }>('/lastblock')
- return BigInt(result.height)
- } catch {
- return 0n
- }
- }
-
- async getGasPrice(): Promise {
- // Bitcoin uses sat/vB, Tron uses bandwidth/energy
- const defaultFee = Amount.fromRaw('1000', this.config.decimals, this.config.symbol)
- return {
- slow: defaultFee,
- standard: defaultFee,
- fast: defaultFee,
- lastUpdated: Date.now(),
- }
- }
-
- async healthCheck(): Promise {
- const startTime = Date.now()
- try {
- const height = await this.getBlockHeight()
- return {
- isHealthy: true,
- latency: Date.now() - startTime,
- blockHeight: height,
- isSyncing: false,
- lastUpdated: Date.now(),
- }
- } catch {
- return {
- isHealthy: false,
- latency: Date.now() - startTime,
- blockHeight: 0n,
- isSyncing: false,
- lastUpdated: Date.now(),
- }
- }
- }
-}
diff --git a/src/services/chain-adapter/bip39/identity-service.ts b/src/services/chain-adapter/bip39/identity-service.ts
deleted file mode 100644
index fcf6ad993..000000000
--- a/src/services/chain-adapter/bip39/identity-service.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * BIP39 Identity Service
- */
-
-import type { ChainConfig } from '@/services/chain-config'
-import type { IIdentityService, Address, Signature } from '../types'
-import { isValidAddress } from '@/lib/crypto'
-
-export class Bip39IdentityService implements IIdentityService {
- private readonly chainId: string
-
- constructor(config: ChainConfig) {
- this.chainId = config.id
- }
-
- async deriveAddress(_seed: Uint8Array, _index = 0): Promise {
- throw new Error('Use deriveAddressesForChains from @/lib/crypto instead')
- }
-
- async deriveAddresses(_seed: Uint8Array, _startIndex: number, _count: number): Promise {
- throw new Error('Use deriveAddressesForChains from @/lib/crypto instead')
- }
-
- isValidAddress(address: string): boolean {
- if (this.chainId === 'bitcoin') {
- return isValidAddress(address, 'bitcoin')
- }
- if (this.chainId === 'tron') {
- return isValidAddress(address, 'tron')
- }
- return false
- }
-
- normalizeAddress(address: string): Address {
- return address // BIP39 addresses are case-sensitive
- }
-
- async signMessage(_message: string | Uint8Array, _privateKey: Uint8Array): Promise {
- throw new Error('Not implemented')
- }
-
- async verifyMessage(
- _message: string | Uint8Array,
- _signature: Signature,
- _address: Address,
- ): Promise {
- throw new Error('Not implemented')
- }
-}
diff --git a/src/services/chain-adapter/bip39/index.ts b/src/services/chain-adapter/bip39/index.ts
deleted file mode 100644
index add772989..000000000
--- a/src/services/chain-adapter/bip39/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export { Bip39Adapter, createBip39Adapter } from './adapter'
-export { Bip39IdentityService } from './identity-service'
-export { Bip39AssetService } from './asset-service'
-export { Bip39TransactionService } from './transaction-service'
-export { Bip39ChainService } from './chain-service'
-export * from './types'
diff --git a/src/services/chain-adapter/bip39/transaction-service.ts b/src/services/chain-adapter/bip39/transaction-service.ts
deleted file mode 100644
index cae371352..000000000
--- a/src/services/chain-adapter/bip39/transaction-service.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-/**
- * BIP39 Transaction Service (使用 BioWallet API)
- */
-
-import type { ChainConfig } from '@/services/chain-config'
-import { chainConfigService } from '@/services/chain-config'
-import type {
- ITransactionService,
- TransferParams,
- UnsignedTransaction,
- SignedTransaction,
- TransactionHash,
- TransactionStatus,
- Transaction,
- FeeEstimate,
- Fee,
-} from '../types'
-import { Amount } from '@/types/amount'
-import { ChainServiceError, ChainErrorCodes } from '../types'
-
-export class Bip39TransactionService implements ITransactionService {
- private readonly config: ChainConfig
- private readonly apiUrl: string
- private readonly apiPath: string
-
- constructor(config: ChainConfig) {
- this.config = config
- const biowalletApi = chainConfigService.getBiowalletApi(config.id)
- this.apiUrl = biowalletApi?.endpoint ?? ''
- this.apiPath = biowalletApi?.path ?? config.id
- }
-
- private async fetch(endpoint: string, body?: unknown): Promise {
- const url = `${this.apiUrl}/wallet/${this.apiPath}${endpoint}`
- const init: RequestInit = body
- ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
- : { method: 'GET' }
- const response = await fetch(url, init)
-
- if (!response.ok) {
- throw new ChainServiceError(
- ChainErrorCodes.NETWORK_ERROR,
- `HTTP ${response.status}: ${response.statusText}`,
- )
- }
-
- const json = await response.json() as { success: boolean; result?: T; error?: { message: string } }
- if (!json.success) {
- throw new ChainServiceError(
- ChainErrorCodes.NETWORK_ERROR,
- json.error?.message ?? 'API request failed',
- )
- }
-
- return json.result as T
- }
-
- async estimateFee(_params: TransferParams): Promise {
- // Default fee estimates
- const feeAmount = this.config.id === 'bitcoin'
- ? Amount.fromRaw('10000', this.config.decimals, this.config.symbol) // ~0.0001 BTC
- : Amount.fromRaw('1000000', this.config.decimals, this.config.symbol) // 1 TRX
-
- const fee: Fee = {
- amount: feeAmount,
- estimatedTime: this.config.id === 'bitcoin' ? 600 : 3,
- }
-
- return {
- slow: { ...fee, estimatedTime: fee.estimatedTime * 2 },
- standard: fee,
- fast: { ...fee, estimatedTime: Math.ceil(fee.estimatedTime / 2) },
- }
- }
-
- async buildTransaction(params: TransferParams): Promise {
- return {
- chainId: this.config.id,
- data: {
- from: params.from,
- to: params.to,
- amount: params.amount.raw.toString(),
- },
- }
- }
-
- async signTransaction(
- _unsignedTx: UnsignedTransaction,
- _privateKey: Uint8Array,
- ): Promise {
- throw new ChainServiceError(
- ChainErrorCodes.CHAIN_NOT_SUPPORTED,
- `${this.config.id} transaction signing not yet implemented`,
- )
- }
-
- async broadcastTransaction(_signedTx: SignedTransaction): Promise {
- throw new ChainServiceError(
- ChainErrorCodes.CHAIN_NOT_SUPPORTED,
- `${this.config.id} transaction broadcast not yet implemented`,
- )
- }
-
- async getTransactionStatus(hash: TransactionHash): Promise {
- try {
- const result = await this.fetch<{
- confirmations: number
- status: string
- }>('/transaction/status', { hash })
-
- const requiredConfirmations = this.config.id === 'bitcoin' ? 6 : 19
-
- return {
- status: result.confirmations >= requiredConfirmations ? 'confirmed' : 'confirming',
- confirmations: result.confirmations,
- requiredConfirmations,
- }
- } catch {
- return {
- status: 'pending',
- confirmations: 0,
- requiredConfirmations: this.config.id === 'bitcoin' ? 6 : 19,
- }
- }
- }
-
- async getTransaction(hash: TransactionHash): Promise {
- try {
- const result = await this.fetch<{
- hash: string
- from: string
- to: string
- amount: string
- fee: string
- status: string
- timestamp: number
- blockNumber?: string
- }>('/transaction', { hash })
-
- return {
- hash: result.hash,
- from: result.from,
- to: result.to,
- amount: Amount.fromRaw(result.amount, this.config.decimals, this.config.symbol),
- fee: Amount.fromRaw(result.fee, this.config.decimals, this.config.symbol),
- status: {
- status: result.status === 'confirmed' ? 'confirmed' : 'pending',
- confirmations: result.status === 'confirmed' ? 6 : 0,
- requiredConfirmations: this.config.id === 'bitcoin' ? 6 : 19,
- },
- timestamp: result.timestamp,
- blockNumber: result.blockNumber ? BigInt(result.blockNumber) : undefined,
- type: 'transfer',
- }
- } catch {
- return null
- }
- }
-
- async getTransactionHistory(address: string, limit = 20): Promise {
- try {
- const result = await this.fetch>('/transactions', { address, limit })
-
- return result.map((tx) => ({
- hash: tx.hash,
- from: tx.from,
- to: tx.to,
- amount: Amount.fromRaw(tx.amount, this.config.decimals, this.config.symbol),
- fee: Amount.fromRaw(tx.fee, this.config.decimals, this.config.symbol),
- status: {
- status: tx.status === 'confirmed' ? 'confirmed' as const : 'pending' as const,
- confirmations: tx.status === 'confirmed' ? 6 : 0,
- requiredConfirmations: this.config.id === 'bitcoin' ? 6 : 19,
- },
- timestamp: tx.timestamp,
- blockNumber: tx.blockNumber ? BigInt(tx.blockNumber) : undefined,
- type: 'transfer' as const,
- }))
- } catch {
- return []
- }
- }
-}
diff --git a/src/services/chain-adapter/bip39/types.ts b/src/services/chain-adapter/bip39/types.ts
deleted file mode 100644
index ca12140bc..000000000
--- a/src/services/chain-adapter/bip39/types.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * BIP39 Chain Adapter Types (Bitcoin, Tron)
- */
-
-import type { ChainConfig } from '@/services/chain-config'
-
-export interface Bip39ChainConfig extends ChainConfig {
- chainKind: 'bitcoin'
-}
-
-export type Bip39ChainId = 'bitcoin' | 'tron'
-
-export function isSupportedBip39Chain(chainId: string): chainId is Bip39ChainId {
- return chainId === 'bitcoin' || chainId === 'tron'
-}
diff --git a/src/services/chain-adapter/bitcoin/adapter.ts b/src/services/chain-adapter/bitcoin/adapter.ts
index f3f644371..7841e248d 100644
--- a/src/services/chain-adapter/bitcoin/adapter.ts
+++ b/src/services/chain-adapter/bitcoin/adapter.ts
@@ -4,7 +4,7 @@
* Full adapter for Bitcoin network using mempool.space API
*/
-import type { ChainConfigType } from '@/services/chain-config'
+import type { ChainKind } from '@/services/chain-config'
import type { IChainAdapter, IStakingService } from '../types'
import { BitcoinIdentityService } from './identity-service'
import { BitcoinAssetService } from './asset-service'
@@ -13,7 +13,7 @@ import { BitcoinTransactionService } from './transaction-service'
export class BitcoinAdapter implements IChainAdapter {
readonly chainId: string
- readonly chainType: ChainConfigType = 'bip39'
+ readonly chainType: ChainKind = 'bitcoin'
readonly identity: BitcoinIdentityService
readonly asset: BitcoinAssetService
readonly chain: BitcoinChainService
diff --git a/src/services/chain-adapter/bitcoin/identity-service.ts b/src/services/chain-adapter/bitcoin/identity-service.ts
index a8ab97930..9343830a2 100644
--- a/src/services/chain-adapter/bitcoin/identity-service.ts
+++ b/src/services/chain-adapter/bitcoin/identity-service.ts
@@ -1,44 +1,34 @@
/**
* Bitcoin Identity Service
- *
- * Supports multiple address types:
- * - P2WPKH (Native SegWit, bc1q...) - Default, BIP84
- * - P2TR (Taproot, bc1p...) - BIP86
- * - P2PKH (Legacy, 1...) - BIP44
+ *
+ * Uses unified derivation from @/lib/crypto/derivation.ts
+ * Default: BIP84 Native SegWit (bc1q...)
*/
import type { IIdentityService, Address, Signature } from '../types'
+import { deriveBitcoinKey } from '@/lib/crypto'
import { sha256 } from '@noble/hashes/sha2.js'
-import { ripemd160 } from '@noble/hashes/legacy.js'
import { secp256k1 } from '@noble/curves/secp256k1.js'
import { bytesToHex } from '@noble/hashes/utils.js'
-import { HDKey } from '@scure/bip32'
import { bech32, bech32m, base58check } from '@scure/base'
export class BitcoinIdentityService implements IIdentityService {
constructor(_chainId: string) {}
async deriveAddress(seed: Uint8Array, index = 0): Promise {
- // Default to Native SegWit (P2WPKH) - BIP84: m/84'/0'/0'/0/index
- const hdKey = HDKey.fromMasterSeed(seed)
- const derived = hdKey.derive(`m/84'/0'/0'/0/${index}`)
-
- if (!derived.publicKey) {
- throw new Error('Failed to derive public key')
- }
-
- // P2WPKH: HASH160(compressed pubkey)
- const pubKeyHash = ripemd160(sha256(derived.publicKey))
-
- // Encode as bech32 (bc1q...)
- const words = bech32.toWords(pubKeyHash)
- return bech32.encode('bc', [0, ...words])
+ // seed is UTF-8 encoded mnemonic string
+ const mnemonic = new TextDecoder().decode(seed)
+ // BIP84 Native SegWit (bc1q...)
+ const derived = deriveBitcoinKey(mnemonic, 84, index)
+ return derived.address
}
async deriveAddresses(seed: Uint8Array, startIndex: number, count: number): Promise {
+ const mnemonic = new TextDecoder().decode(seed)
const addresses: Address[] = []
for (let i = 0; i < count; i++) {
- addresses.push(await this.deriveAddress(seed, startIndex + i))
+ const derived = deriveBitcoinKey(mnemonic, 84, startIndex + i)
+ addresses.push(derived.address)
}
return addresses
}
diff --git a/src/services/chain-adapter/derive-wallet-chain-addresses.ts b/src/services/chain-adapter/derive-wallet-chain-addresses.ts
new file mode 100644
index 000000000..38720f73f
--- /dev/null
+++ b/src/services/chain-adapter/derive-wallet-chain-addresses.ts
@@ -0,0 +1,76 @@
+/**
+ * derive-wallet-chain-addresses.ts
+ *
+ * 统一入口:通过 chain-adapter 派生钱包的所有链地址
+ * 这是业务代码调用派生的唯一入口,确保"单一真相"
+ */
+
+import { getChainProvider } from './providers'
+import type { ChainConfig } from '@/services/chain-config'
+
+export interface DerivedChainAddress {
+ chainId: string
+ address: string
+ publicKey?: string
+}
+
+export interface DeriveWalletChainAddressesParams {
+ mnemonic: string
+ chainConfigs: readonly ChainConfig[]
+ selectedChainIds: string[]
+ accountIndex?: number
+}
+
+/**
+ * 从 mnemonic 派生钱包的所有链地址
+ *
+ * @param params.mnemonic - 助记词
+ * @param params.chainConfigs - 链配置列表
+ * @param params.selectedChainIds - 用户选中的链 ID 列表
+ * @param params.accountIndex - 账户索引(默认 0)
+ * @returns 派生的链地址数组
+ * @throws 如果任何链不支持派生,将抛出错误
+ */
+export async function deriveWalletChainAddresses(
+ params: DeriveWalletChainAddressesParams,
+): Promise {
+ const { mnemonic, chainConfigs, selectedChainIds, accountIndex = 0 } = params
+
+ // 构建 chainId -> config 映射
+ const configMap = new Map(chainConfigs.map((c) => [c.id, c]))
+
+ // 将 mnemonic 编码为 Uint8Array(chain-adapter 的约定)
+ const seed = new TextEncoder().encode(mnemonic)
+
+ const results: DerivedChainAddress[] = []
+
+ for (const chainId of selectedChainIds) {
+ const config = configMap.get(chainId)
+ if (!config) {
+ console.warn(`[deriveWalletChainAddresses] Chain config not found: ${chainId}`)
+ continue
+ }
+
+ const provider = getChainProvider(chainId)
+
+ if (!provider.supportsDeriveAddress) {
+ throw new Error(`Chain "${chainId}" (${config.chainKind}) does not support address derivation`)
+ }
+
+ const deriveAddress = provider.deriveAddress
+ if (!deriveAddress) {
+ throw new Error(`Chain "${chainId}" deriveAddress method not available`)
+ }
+
+ const address = await deriveAddress(seed, accountIndex)
+
+ results.push({
+ chainId,
+ address,
+ })
+ }
+
+ return results
+}
+
+
diff --git a/src/services/chain-adapter/evm/adapter.ts b/src/services/chain-adapter/evm/adapter.ts
index bc0716211..6292a0eec 100644
--- a/src/services/chain-adapter/evm/adapter.ts
+++ b/src/services/chain-adapter/evm/adapter.ts
@@ -2,7 +2,7 @@
* EVM Chain Adapter
*/
-import type { ChainConfigType } from '@/services/chain-config'
+import type { ChainKind } from '@/services/chain-config'
import type { IChainAdapter, IStakingService } from '../types'
import { EvmIdentityService } from './identity-service'
import { EvmAssetService } from './asset-service'
@@ -11,7 +11,7 @@ import { EvmChainService } from './chain-service'
export class EvmAdapter implements IChainAdapter {
readonly chainId: string
- readonly chainType: ChainConfigType = 'evm'
+ readonly chainType: ChainKind = 'evm'
readonly identity: EvmIdentityService
readonly asset: EvmAssetService
diff --git a/src/services/chain-adapter/evm/identity-service.ts b/src/services/chain-adapter/evm/identity-service.ts
index b3083b4a4..f86b26cb9 100644
--- a/src/services/chain-adapter/evm/identity-service.ts
+++ b/src/services/chain-adapter/evm/identity-service.ts
@@ -1,23 +1,30 @@
/**
* EVM Identity Service
+ *
+ * Uses unified derivation from @/lib/crypto/derivation.ts
*/
import type { IIdentityService, Address, Signature } from '../types'
-import { toChecksumAddress, isValidAddress } from '@/lib/crypto'
+import { toChecksumAddress, isValidAddress, deriveKey } from '@/lib/crypto'
export class EvmIdentityService implements IIdentityService {
- private readonly chainId: string
+ constructor(_chainId: string) {}
- constructor(chainId: string) {
- this.chainId = chainId
+ async deriveAddress(seed: Uint8Array, index = 0): Promise {
+ // seed is UTF-8 encoded mnemonic string
+ const mnemonic = new TextDecoder().decode(seed)
+ const derived = deriveKey(mnemonic, 'ethereum', index)
+ return derived.address
}
- async deriveAddress(_seed: Uint8Array, _index = 0): Promise {
- throw new Error('Use deriveAddressesForChains from @/lib/crypto instead')
- }
-
- async deriveAddresses(_seed: Uint8Array, _startIndex: number, _count: number): Promise {
- throw new Error('Use deriveAddressesForChains from @/lib/crypto instead')
+ async deriveAddresses(seed: Uint8Array, startIndex: number, count: number): Promise {
+ const mnemonic = new TextDecoder().decode(seed)
+ const addresses: Address[] = []
+ for (let i = 0; i < count; i++) {
+ const derived = deriveKey(mnemonic, 'ethereum', startIndex + i)
+ addresses.push(derived.address)
+ }
+ return addresses
}
isValidAddress(address: string): boolean {
diff --git a/src/services/chain-adapter/index.ts b/src/services/chain-adapter/index.ts
index 021d7a108..8c144cfbe 100644
--- a/src/services/chain-adapter/index.ts
+++ b/src/services/chain-adapter/index.ts
@@ -46,6 +46,13 @@ export {
clearProviderCache,
} from './providers'
+// Unified derivation helper (single source of truth)
+export {
+ deriveWalletChainAddresses,
+ type DerivedChainAddress,
+ type DeriveWalletChainAddressesParams,
+} from './derive-wallet-chain-addresses'
+
// =================================================================
// DEPRECATED: Old adapter registry API
// Use ChainProvider from './providers' instead
@@ -57,6 +64,5 @@ export { getAdapterRegistry, resetAdapterRegistry } from './registry'
// Adapters (kept for internal use by wrapped providers)
export { BioforestAdapter, createBioforestAdapter } from './bioforest'
export { EvmAdapter, createEvmAdapter } from './evm'
-export { Bip39Adapter, createBip39Adapter } from './bip39'
export { TronAdapter, createTronAdapter } from './tron'
export { BitcoinAdapter, createBitcoinAdapter } from './bitcoin'
diff --git a/src/services/chain-adapter/providers/index.ts b/src/services/chain-adapter/providers/index.ts
index ea32ad1eb..200e1102f 100644
--- a/src/services/chain-adapter/providers/index.ts
+++ b/src/services/chain-adapter/providers/index.ts
@@ -45,9 +45,6 @@ import { BitcoinTransactionService } from '../bitcoin/transaction-service'
import { BioforestIdentityService } from '../bioforest/identity-service'
import { BioforestAssetService } from '../bioforest/asset-service'
import { BioforestTransactionService } from '../bioforest/transaction-service'
-import { Bip39IdentityService } from '../bip39/identity-service'
-import { Bip39AssetService } from '../bip39/asset-service'
-import { Bip39TransactionService } from '../bip39/transaction-service'
/** 所有 Provider 工厂函数 */
const PROVIDER_FACTORIES: ApiProviderFactory[] = [
@@ -116,10 +113,6 @@ function createWrappedProviders(config: ChainConfig): ApiProvider[] {
)
break
}
- case 'custom': {
- // Custom chains not supported yet - skip
- break
- }
}
return providers
diff --git a/src/services/chain-adapter/registry.ts b/src/services/chain-adapter/registry.ts
index b1176840f..b7bc4f73d 100644
--- a/src/services/chain-adapter/registry.ts
+++ b/src/services/chain-adapter/registry.ts
@@ -2,20 +2,20 @@
* 链适配器注册表
*/
-import type { ChainConfigType } from '@/services/chain-config'
+import type { ChainKind } from '@/services/chain-config'
import type { IChainAdapter, IAdapterRegistry, AdapterFactory } from './types'
import { ChainServiceError, ChainErrorCodes } from './types'
class AdapterRegistry implements IAdapterRegistry {
- private factories = new Map()
+ private factories = new Map()
private adapters = new Map()
- private chainTypes = new Map()
+ private chainTypes = new Map()
- register(type: ChainConfigType, factory: AdapterFactory): void {
+ register(type: ChainKind, factory: AdapterFactory): void {
this.factories.set(type, factory)
}
- registerChain(chainId: string, type: ChainConfigType): void {
+ registerChain(chainId: string, type: ChainKind): void {
this.chainTypes.set(chainId, type)
}
diff --git a/src/services/chain-adapter/tron/adapter.ts b/src/services/chain-adapter/tron/adapter.ts
index 4de532d83..aa7b23fe9 100644
--- a/src/services/chain-adapter/tron/adapter.ts
+++ b/src/services/chain-adapter/tron/adapter.ts
@@ -4,7 +4,7 @@
* Full adapter for Tron network using PublicNode HTTP API
*/
-import type { ChainConfigType } from '@/services/chain-config'
+import type { ChainKind } from '@/services/chain-config'
import type { IChainAdapter, IStakingService } from '../types'
import { TronIdentityService } from './identity-service'
import { TronAssetService } from './asset-service'
@@ -13,7 +13,7 @@ import { TronTransactionService } from './transaction-service'
export class TronAdapter implements IChainAdapter {
readonly chainId: string
- readonly chainType: ChainConfigType = 'tron'
+ readonly chainType: ChainKind = 'tron'
readonly identity: TronIdentityService
readonly asset: TronAssetService
readonly chain: TronChainService
diff --git a/src/services/chain-adapter/tron/identity-service.ts b/src/services/chain-adapter/tron/identity-service.ts
index 0971f618c..418f998cb 100644
--- a/src/services/chain-adapter/tron/identity-service.ts
+++ b/src/services/chain-adapter/tron/identity-service.ts
@@ -1,13 +1,15 @@
/**
* Tron Identity Service
+ *
+ * Uses unified derivation from @/lib/crypto/derivation.ts
*/
import type { IIdentityService, Address, Signature } from '../types'
+import { deriveKey } from '@/lib/crypto'
import { sha256 } from '@noble/hashes/sha2.js'
import { secp256k1 } from '@noble/curves/secp256k1.js'
import { keccak_256 } from '@noble/hashes/sha3.js'
import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js'
-import { HDKey } from '@scure/bip32'
/** Base58 alphabet used by Tron */
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
@@ -16,31 +18,18 @@ export class TronIdentityService implements IIdentityService {
constructor(_chainId: string) {}
async deriveAddress(seed: Uint8Array, index = 0): Promise {
- // Tron uses BIP44 path: m/44'/195'/0'/0/index
- const hdKey = HDKey.fromMasterSeed(seed)
- const derived = hdKey.derive(`m/44'/195'/0'/0/${index}`)
-
- if (!derived.privateKey) {
- throw new Error('Failed to derive private key')
- }
-
- // Get public key and derive address
- const pubKey = secp256k1.getPublicKey(derived.privateKey, false)
- const pubKeyHash = keccak_256(pubKey.slice(1))
- const addressBytes = pubKeyHash.slice(-20)
-
- // Add Tron prefix (0x41) and encode to base58check
- const payload = new Uint8Array([0x41, ...addressBytes])
- const checksum = sha256(sha256(payload)).slice(0, 4)
- const full = new Uint8Array([...payload, ...checksum])
-
- return this.encodeBase58(full)
+ // seed is UTF-8 encoded mnemonic string
+ const mnemonic = new TextDecoder().decode(seed)
+ const derived = deriveKey(mnemonic, 'tron', index)
+ return derived.address
}
async deriveAddresses(seed: Uint8Array, startIndex: number, count: number): Promise {
+ const mnemonic = new TextDecoder().decode(seed)
const addresses: Address[] = []
for (let i = 0; i < count; i++) {
- addresses.push(await this.deriveAddress(seed, startIndex + i))
+ const derived = deriveKey(mnemonic, 'tron', startIndex + i)
+ addresses.push(derived.address)
}
return addresses
}
diff --git a/src/services/chain-adapter/types.ts b/src/services/chain-adapter/types.ts
index d2b3c9134..60a1ea1db 100644
--- a/src/services/chain-adapter/types.ts
+++ b/src/services/chain-adapter/types.ts
@@ -2,7 +2,7 @@
* 链适配器类型定义
*/
-import type { ChainConfig, ChainConfigType } from '@/services/chain-config'
+import type { ChainConfig, ChainKind } from '@/services/chain-config'
import { Amount } from '@/types/amount'
// ==================== 基础类型 ====================
@@ -159,7 +159,7 @@ export interface IStakingService {
export interface IChainAdapter {
readonly chainId: string
- readonly chainType: ChainConfigType
+ readonly chainType: ChainKind
readonly identity: IIdentityService
readonly asset: IAssetService
@@ -174,8 +174,8 @@ export interface IChainAdapter {
export type AdapterFactory = (chainId: string) => IChainAdapter
export interface IAdapterRegistry {
- register(type: ChainConfigType, factory: AdapterFactory): void
- registerChain(chainId: string, type: ChainConfigType): void
+ register(type: ChainKind, factory: AdapterFactory): void
+ registerChain(chainId: string, type: ChainKind): void
getAdapter(chainId: string): IChainAdapter | null
hasAdapter(chainId: string): boolean
listAdapters(): string[]
diff --git a/src/services/chain-config/__tests__/index.test.ts b/src/services/chain-config/__tests__/index.test.ts
index 2f6242b41..8ec5e327c 100644
--- a/src/services/chain-config/__tests__/index.test.ts
+++ b/src/services/chain-config/__tests__/index.test.ts
@@ -154,19 +154,17 @@ describe('chain-config service', () => {
expect(getChainById(snapshot, 'manual-two')?.source).toBe('manual')
})
- it('normalizes unknown chainKind to custom when adding manual config', async () => {
- const snapshot = await addManualConfig({
- id: 'manual-unknown',
- version: '1.0',
- chainKind: 'unknown-kind' as any,
- name: 'Manual Unknown',
- symbol: 'MU',
- decimals: 8,
- })
-
- const chain = getChainById(snapshot, 'manual-unknown')
- expect(chain?.chainKind).toBe('custom')
- expect(chain?.source).toBe('manual')
+ it('rejects unknown chainKind when adding manual config', async () => {
+ await expect(
+ addManualConfig({
+ id: 'manual-unknown',
+ version: '1.0',
+ chainKind: 'unknown-kind' as any,
+ name: 'Manual Unknown',
+ symbol: 'MU',
+ decimals: 8,
+ }),
+ ).rejects.toThrow()
})
it('persists enabled/disabled preference via setChainEnabled', async () => {
diff --git a/src/services/chain-config/__tests__/refresh-subscription.test.ts b/src/services/chain-config/__tests__/refresh-subscription.test.ts
index 6220b8075..5f9ba8846 100644
--- a/src/services/chain-config/__tests__/refresh-subscription.test.ts
+++ b/src/services/chain-config/__tests__/refresh-subscription.test.ts
@@ -44,7 +44,7 @@ describe('chain-config refreshSubscription', () => {
return new Response(
JSON.stringify([
- { id: 'remote-one', version: '1.0', chainKind: 'custom', name: 'Remote One', symbol: 'R1', decimals: 8 },
+ { id: 'remote-one', version: '1.0', chainKind: 'evm', name: 'Remote One', symbol: 'R1', decimals: 8 },
]),
{ status: 200, headers: { 'Content-Type': 'application/json', ETag: 'etag-2' } }
)
diff --git a/src/services/chain-config/__tests__/set-subscription-url.test.ts b/src/services/chain-config/__tests__/set-subscription-url.test.ts
index 5b08eda39..6d409f890 100644
--- a/src/services/chain-config/__tests__/set-subscription-url.test.ts
+++ b/src/services/chain-config/__tests__/set-subscription-url.test.ts
@@ -32,7 +32,7 @@ describe('chain-config setSubscriptionUrl', () => {
ChainConfigSchema.parse({
id: 'cached',
version: '1.0',
- chainKind: 'custom',
+ chainKind: 'evm',
name: 'Cached',
symbol: 'C',
decimals: 8,
@@ -65,7 +65,7 @@ describe('chain-config setSubscriptionUrl', () => {
ChainConfigSchema.parse({
id: 'cached',
version: '1.0',
- chainKind: 'custom',
+ chainKind: 'evm',
name: 'Cached',
symbol: 'C',
decimals: 8,
diff --git a/src/services/chain-config/__tests__/storage.test.ts b/src/services/chain-config/__tests__/storage.test.ts
index ed29752eb..4c3e4845c 100644
--- a/src/services/chain-config/__tests__/storage.test.ts
+++ b/src/services/chain-config/__tests__/storage.test.ts
@@ -29,7 +29,7 @@ describe('chain-config storage', () => {
ChainConfigSchema.parse({
id: 'custom-1',
version: '1.0',
- chainKind: 'custom',
+ chainKind: 'evm',
name: 'Custom',
symbol: 'CST',
decimals: 8,
@@ -49,7 +49,7 @@ describe('chain-config storage', () => {
ChainConfigSchema.parse({
id: 'first',
version: '1.0',
- chainKind: 'custom',
+ chainKind: 'evm',
name: 'First',
symbol: 'FST',
decimals: 8,
@@ -61,7 +61,7 @@ describe('chain-config storage', () => {
ChainConfigSchema.parse({
id: 'second',
version: '1.0',
- chainKind: 'custom',
+ chainKind: 'evm',
name: 'Second',
symbol: 'SND',
decimals: 8,
diff --git a/src/services/chain-config/__tests__/subscription.test.ts b/src/services/chain-config/__tests__/subscription.test.ts
index a16c38549..b41835979 100644
--- a/src/services/chain-config/__tests__/subscription.test.ts
+++ b/src/services/chain-config/__tests__/subscription.test.ts
@@ -16,7 +16,7 @@ describe('chain-config subscription', () => {
const single = parseAndValidate({
id: 'one',
version: '1.0',
- chainKind: 'custom',
+ chainKind: 'evm',
name: 'One',
symbol: 'ONE',
decimals: 8,
@@ -28,7 +28,7 @@ describe('chain-config subscription', () => {
{
id: 'two',
version: '1.0',
- chainKind: 'custom',
+ chainKind: 'evm',
name: 'Two',
symbol: 'TWO',
decimals: 8,
@@ -52,7 +52,7 @@ describe('chain-config subscription', () => {
ChainConfigSchema.parse({
id: 'cached',
version: '1.0',
- chainKind: 'custom',
+ chainKind: 'evm',
name: 'Cached',
symbol: 'C',
decimals: 8,
@@ -83,7 +83,7 @@ describe('chain-config subscription', () => {
const fetchMock = vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise>(async () => {
return new Response(
JSON.stringify([
- { id: 'new', version: '1.0', chainKind: 'custom', name: 'New', symbol: 'NEW', decimals: 8 },
+ { id: 'new', version: '1.0', chainKind: 'evm', name: 'New', symbol: 'NEW', decimals: 8 },
]),
{
status: 200,
@@ -111,7 +111,7 @@ describe('chain-config subscription', () => {
ChainConfigSchema.parse({
id: 'cached',
version: '1.0',
- chainKind: 'custom',
+ chainKind: 'evm',
name: 'Cached',
symbol: 'C',
decimals: 8,
diff --git a/src/services/chain-config/index.ts b/src/services/chain-config/index.ts
index d3e3dee51..2037dde90 100644
--- a/src/services/chain-config/index.ts
+++ b/src/services/chain-config/index.ts
@@ -44,14 +44,11 @@ export interface ChainConfigSnapshot {
warnings: ChainConfigWarning[]
}
-const KNOWN_KINDS: ReadonlySet = new Set(['bioforest', 'evm', 'bitcoin', 'tron', 'custom'])
-
const SUPPORTED_MAJOR_BY_KIND: Record = {
bioforest: 1,
evm: 1,
bitcoin: 1,
tron: 1,
- custom: 1,
}
const DEFAULT_CHAINS_PATH = `${import.meta.env.BASE_URL}configs/default-chains.json`
@@ -64,18 +61,6 @@ interface DefaultChainsResult {
let defaultChainsCache: DefaultChainsResult | null = null
let defaultChainsLoading: Promise | null = null
-function normalizeUnknownKind(input: unknown): unknown {
- if (typeof input !== 'object' || input === null || Array.isArray(input)) return input
- const record = input as Record
-
- const chainKind = record.chainKind
- if (typeof chainKind === 'string' && !KNOWN_KINDS.has(chainKind)) {
- return { ...record, chainKind: 'custom' }
- }
-
- return input
-}
-
function parseMajor(version: string): number | null {
const majorPart = version.split('.')[0]
const major = Number(majorPart)
@@ -116,7 +101,8 @@ async function loadDefaultChainConfigs(): Promise {
const json: unknown = await response.json()
const parsed = VersionedChainConfigFileSchema.parse(json)
- const configs = parsed.chains.map((chain) => normalizeUnknownKind(chain)).map((chain) => {
+ const configs = parsed.chains.map((chain) => {
+ // Strict validation: unknown chainKind will fail schema validation directly
const config = ChainConfigSchema.parse(chain)
const resolvedPaths = resolveIconPaths(config, jsonUrl)
return {
@@ -185,11 +171,10 @@ function resolveIconPaths(
}
function parseConfigs(input: unknown, source: ChainConfigSource, jsonFileUrl?: string): ChainConfig[] {
- const normalized: unknown = Array.isArray(input) ? input.map(normalizeUnknownKind) : normalizeUnknownKind(input)
-
- const parsed = Array.isArray(normalized)
- ? ChainConfigListSchema.parse(normalized)
- : [ChainConfigSchema.parse(normalized)]
+ // Strict validation: unknown chainKind will fail schema validation directly
+ const parsed = Array.isArray(input)
+ ? ChainConfigListSchema.parse(input)
+ : [ChainConfigSchema.parse(input)]
return parsed.map((config) => {
const resolvedPaths = jsonFileUrl ? resolveIconPaths(config, jsonFileUrl) : {}
diff --git a/src/services/chain-config/schema.ts b/src/services/chain-config/schema.ts
index 98ee86f27..0b2359474 100644
--- a/src/services/chain-config/schema.ts
+++ b/src/services/chain-config/schema.ts
@@ -17,7 +17,7 @@ export const ChainConfigVersionSchema = z
.string()
.regex(/^\d+\.\d+$/, 'version must be "major.minor" (e.g. "1.0")')
-export const ChainKindSchema = z.enum(['bioforest', 'evm', 'bitcoin', 'tron', 'custom'])
+export const ChainKindSchema = z.enum(['bioforest', 'evm', 'bitcoin', 'tron'])
export const ChainConfigSourceSchema = z.enum(['default', 'subscription', 'manual'])
diff --git a/src/services/chain-config/subscription.ts b/src/services/chain-config/subscription.ts
index 5342ac0bd..7c8120d5f 100644
--- a/src/services/chain-config/subscription.ts
+++ b/src/services/chain-config/subscription.ts
@@ -28,26 +28,11 @@ export type FetchSubscriptionResult =
const REQUEST_TIMEOUT = 10_000
-const KNOWN_TYPES = new Set(['bioforest', 'evm', 'bip39', 'custom'] as const)
-
-function normalizeUnknownType(input: unknown): unknown {
- if (typeof input !== 'object' || input === null || Array.isArray(input)) return input
- const record = input as Record
-
- const type = record.type
- if (typeof type === 'string' && !KNOWN_TYPES.has(type as never)) {
- return { ...record, chainKind: 'custom' }
- }
-
- return input
-}
-
export function parseAndValidate(json: unknown): ChainConfig[] {
- const normalized: unknown = Array.isArray(json) ? json.map(normalizeUnknownType) : normalizeUnknownType(json)
-
- const parsed = Array.isArray(normalized)
- ? ChainConfigListSchema.parse(normalized)
- : [ChainConfigSchema.parse(normalized)]
+ // Strict validation: unknown chainKind will fail schema validation directly
+ const parsed = Array.isArray(json)
+ ? ChainConfigListSchema.parse(json)
+ : [ChainConfigSchema.parse(json)]
return parsed.map((c) => ({
...c,
diff --git a/src/services/wallet/chain-derivation/build-wallet-chain-addresses.test.ts b/src/services/wallet/chain-derivation/build-wallet-chain-addresses.test.ts
deleted file mode 100644
index e547f5906..000000000
--- a/src/services/wallet/chain-derivation/build-wallet-chain-addresses.test.ts
+++ /dev/null
@@ -1,215 +0,0 @@
-/**
- * buildWalletChainAddresses Tests
- */
-
-import { describe, it, expect } from 'vitest'
-import { buildWalletChainAddresses } from './build-wallet-chain-addresses'
-import type { ChainConfig } from '@/services/chain-config'
-
-const TEST_MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
-
-const mockChainConfigs: ChainConfig[] = [
- {
- id: 'bfmeta',
- version: '1.0',
- chainKind: 'bioforest',
- name: 'BFMeta',
- symbol: 'BFM',
- decimals: 8,
- prefix: 'b',
- enabled: true,
- source: 'default',
- },
- {
- id: 'ethereum',
- version: '1.0',
- chainKind: 'evm',
- name: 'Ethereum',
- symbol: 'ETH',
- decimals: 18,
- enabled: true,
- source: 'default',
- },
- {
- id: 'binance',
- version: '1.0',
- chainKind: 'evm',
- name: 'BSC',
- symbol: 'BNB',
- decimals: 18,
- enabled: true,
- source: 'default',
- },
- {
- id: 'tron',
- version: '1.0',
- chainKind: 'tron',
- name: 'Tron',
- symbol: 'TRX',
- decimals: 6,
- enabled: true,
- source: 'default',
- },
- {
- id: 'bitcoin',
- version: '1.0',
- chainKind: 'bitcoin',
- name: 'Bitcoin',
- symbol: 'BTC',
- decimals: 8,
- enabled: true,
- source: 'default',
- },
-]
-
-describe('buildWalletChainAddresses', () => {
- it('should return empty array when no chains selected', () => {
- const result = buildWalletChainAddresses({
- mnemonic: TEST_MNEMONIC,
- selectedChainIds: [],
- chainConfigs: mockChainConfigs,
- })
- expect(result).toEqual([])
- })
-
- it('should derive Tron address when tron is selected', () => {
- const result = buildWalletChainAddresses({
- mnemonic: TEST_MNEMONIC,
- selectedChainIds: ['tron'],
- chainConfigs: mockChainConfigs,
- })
-
- expect(result).toHaveLength(1)
- expect(result[0]?.chainId).toBe('tron')
- // Tron address format: starts with T, base58check, 34 chars
- expect(result[0]?.address).toMatch(/^T[1-9A-HJ-NP-Za-km-z]{33}$/)
- })
-
- it('should derive same EVM address for multiple EVM chains', () => {
- const result = buildWalletChainAddresses({
- mnemonic: TEST_MNEMONIC,
- selectedChainIds: ['ethereum', 'binance'],
- chainConfigs: mockChainConfigs,
- })
-
- expect(result).toHaveLength(2)
- const ethAddress = result.find(r => r.chainId === 'ethereum')?.address
- const bscAddress = result.find(r => r.chainId === 'binance')?.address
-
- // EVM chains share the same address
- expect(ethAddress).toBe(bscAddress)
- // EVM address format: 0x + 40 hex chars
- expect(ethAddress).toMatch(/^0x[a-fA-F0-9]{40}$/)
- })
-
- it('should derive BioForest address', () => {
- const result = buildWalletChainAddresses({
- mnemonic: TEST_MNEMONIC,
- selectedChainIds: ['bfmeta'],
- chainConfigs: mockChainConfigs,
- })
-
- expect(result).toHaveLength(1)
- expect(result[0]?.chainId).toBe('bfmeta')
- // BioForest address: prefix + base58check
- expect(result[0]?.address).toMatch(/^b[1-9A-HJ-NP-Za-km-z]+$/)
- })
-
- it('should derive Bitcoin address', () => {
- const result = buildWalletChainAddresses({
- mnemonic: TEST_MNEMONIC,
- selectedChainIds: ['bitcoin'],
- chainConfigs: mockChainConfigs,
- })
-
- expect(result).toHaveLength(1)
- expect(result[0]?.chainId).toBe('bitcoin')
- // Bitcoin P2PKH address: starts with 1 or 3
- expect(result[0]?.address).toMatch(/^[13][1-9A-HJ-NP-Za-km-z]{25,34}$/)
- })
-
- it('should be deterministic (same input same output)', () => {
- const params = {
- mnemonic: TEST_MNEMONIC,
- selectedChainIds: ['tron', 'ethereum', 'bfmeta'],
- chainConfigs: mockChainConfigs,
- }
-
- const result1 = buildWalletChainAddresses(params)
- const result2 = buildWalletChainAddresses(params)
-
- expect(result1).toEqual(result2)
- })
-
- it('should respect selectedChainIds order', () => {
- const result = buildWalletChainAddresses({
- mnemonic: TEST_MNEMONIC,
- selectedChainIds: ['tron', 'ethereum', 'bfmeta'],
- chainConfigs: mockChainConfigs,
- })
-
- expect(result).toHaveLength(3)
- expect(result[0]?.chainId).toBe('tron')
- expect(result[1]?.chainId).toBe('ethereum')
- expect(result[2]?.chainId).toBe('bfmeta')
- })
-
- it('should skip unknown chainIds', () => {
- const result = buildWalletChainAddresses({
- mnemonic: TEST_MNEMONIC,
- selectedChainIds: ['tron', 'unknown-chain'],
- chainConfigs: mockChainConfigs,
- })
-
- expect(result).toHaveLength(1)
- expect(result[0]?.chainId).toBe('tron')
- })
-
- it('should skip custom chainKind', () => {
- const configsWithCustom: ChainConfig[] = [
- ...mockChainConfigs,
- {
- id: 'my-custom',
- version: '1.0',
- chainKind: 'custom',
- name: 'My Custom',
- symbol: 'CUSTOM',
- decimals: 18,
- enabled: true,
- source: 'manual',
- },
- ]
-
- const result = buildWalletChainAddresses({
- mnemonic: TEST_MNEMONIC,
- selectedChainIds: ['my-custom', 'tron'],
- chainConfigs: configsWithCustom,
- })
-
- // custom chain should be skipped
- expect(result).toHaveLength(1)
- expect(result[0]?.chainId).toBe('tron')
- })
-
- it('should handle all chain kinds together', () => {
- const result = buildWalletChainAddresses({
- mnemonic: TEST_MNEMONIC,
- selectedChainIds: ['bfmeta', 'ethereum', 'tron', 'bitcoin'],
- chainConfigs: mockChainConfigs,
- })
-
- expect(result).toHaveLength(4)
- expect(result.map(r => r.chainId)).toEqual(['bfmeta', 'ethereum', 'tron', 'bitcoin'])
-
- // Each address has the correct format
- const bfmeta = result.find(r => r.chainId === 'bfmeta')
- const ethereum = result.find(r => r.chainId === 'ethereum')
- const tron = result.find(r => r.chainId === 'tron')
- const bitcoin = result.find(r => r.chainId === 'bitcoin')
-
- expect(bfmeta?.address).toMatch(/^b[1-9A-HJ-NP-Za-km-z]+$/)
- expect(ethereum?.address).toMatch(/^0x[a-fA-F0-9]{40}$/)
- expect(tron?.address).toMatch(/^T[1-9A-HJ-NP-Za-km-z]{33}$/)
- expect(bitcoin?.address).toMatch(/^[13][1-9A-HJ-NP-Za-km-z]{25,34}$/)
- })
-})
diff --git a/src/services/wallet/chain-derivation/build-wallet-chain-addresses.ts b/src/services/wallet/chain-derivation/build-wallet-chain-addresses.ts
deleted file mode 100644
index b46f3c29a..000000000
--- a/src/services/wallet/chain-derivation/build-wallet-chain-addresses.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-/**
- * buildWalletChainAddresses
- *
- * 单一入口:从 mnemonic + 选中的 chainIds + chainConfigs 派生所有地址
- * 纯函数、离线可用、deterministic
- */
-
-import type { ChainConfig } from '@/services/chain-config'
-import type { ChainAddress, BuildWalletChainAddressesParams } from './types'
-import { KeyMaterialProvider } from './key-material-provider'
-import { EvmStrategy, BitcoinStrategy, TronStrategy, BioforestStrategy } from './strategies'
-
-const evmStrategy = new EvmStrategy()
-const bitcoinStrategy = new BitcoinStrategy()
-const tronStrategy = new TronStrategy()
-const bioforestStrategy = new BioforestStrategy()
-
-/**
- * 从 mnemonic 派生钱包的所有链地址
- *
- * @param params.mnemonic - 助记词
- * @param params.accountIndex - 账户索引(默认 0)
- * @param params.selectedChainIds - 用户选中的链 ID 列表
- * @param params.chainConfigs - 链配置列表
- * @returns 去重、稳定排序的 ChainAddress 数组
- */
-export function buildWalletChainAddresses(params: BuildWalletChainAddressesParams): ChainAddress[] {
- const { mnemonic, accountIndex = 0, selectedChainIds, chainConfigs } = params
-
- const selectedConfigsMap = new Map()
- for (const config of chainConfigs) {
- if (selectedChainIds.includes(config.id)) {
- selectedConfigsMap.set(config.id, config)
- }
- }
-
- if (selectedConfigsMap.size === 0) {
- return []
- }
-
- const keyProvider = new KeyMaterialProvider(mnemonic)
- const addressByChainId = new Map()
-
- // 按 chainKind 分组处理
- const evmConfigs: ChainConfig[] = []
- const bitcoinConfigs: ChainConfig[] = []
- const tronConfigs: ChainConfig[] = []
- const bioforestConfigs: ChainConfig[] = []
- const customConfigs: ChainConfig[] = []
-
- for (const config of selectedConfigsMap.values()) {
- switch (config.chainKind) {
- case 'evm':
- evmConfigs.push(config)
- break
- case 'bitcoin':
- bitcoinConfigs.push(config)
- break
- case 'tron':
- tronConfigs.push(config)
- break
- case 'bioforest':
- bioforestConfigs.push(config)
- break
- case 'custom':
- customConfigs.push(config)
- break
- }
- }
-
- // EVM:所有 EVM 链共享同一地址
- if (evmConfigs.length > 0) {
- const keyMaterial = keyProvider.getEvmKeyMaterial(accountIndex)
- const firstConfig = evmConfigs[0]!
- const result = evmStrategy.derive({ keyMaterial, config: firstConfig, accountIndex })
- for (const config of evmConfigs) {
- addressByChainId.set(config.id, result.address)
- }
- }
-
- // Bitcoin
- if (bitcoinConfigs.length > 0) {
- const keyMaterial = keyProvider.getBitcoinKeyMaterial(accountIndex)
- for (const config of bitcoinConfigs) {
- const result = bitcoinStrategy.derive({ keyMaterial, config, accountIndex })
- addressByChainId.set(config.id, result.address)
- }
- }
-
- // Tron
- if (tronConfigs.length > 0) {
- const keyMaterial = keyProvider.getTronKeyMaterial(accountIndex)
- for (const config of tronConfigs) {
- const result = tronStrategy.derive({ keyMaterial, config, accountIndex })
- addressByChainId.set(config.id, result.address)
- }
- }
-
- // BioForest:使用特殊派生方式(不使用 BIP44)
- if (bioforestConfigs.length > 0) {
- for (const config of bioforestConfigs) {
- const result = bioforestStrategy.deriveBioforest({ mnemonic, config, accountIndex })
- addressByChainId.set(config.id, result.address)
- }
- }
-
- // Custom:跳过,UI 应提示不支持
- // customConfigs 不派生地址
-
- // 按 selectedChainIds 的顺序输出,保证 deterministic
- const results: ChainAddress[] = []
- for (const chainId of selectedChainIds) {
- const address = addressByChainId.get(chainId)
- if (address) {
- results.push({ chainId, address })
- }
- }
-
- return results
-}
diff --git a/src/services/wallet/chain-derivation/index.ts b/src/services/wallet/chain-derivation/index.ts
deleted file mode 100644
index b898674d8..000000000
--- a/src/services/wallet/chain-derivation/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * Chain Derivation Module
- *
- * 链地址派生模块 - 统一入口
- */
-
-export * from './types'
-export { KeyMaterialProvider } from './key-material-provider'
-export { buildWalletChainAddresses } from './build-wallet-chain-addresses'
-export * from './strategies'
diff --git a/src/services/wallet/chain-derivation/key-material-provider.ts b/src/services/wallet/chain-derivation/key-material-provider.ts
deleted file mode 100644
index be2d8b174..000000000
--- a/src/services/wallet/chain-derivation/key-material-provider.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * KeyMaterialProvider
- *
- * 从 mnemonic 派生基础密钥材料,供各 Strategy 使用
- * 避免重复计算,提高效率
- */
-
-import { HDKey } from '@scure/bip32'
-import { mnemonicToSeedSync } from '@scure/bip39'
-import type { KeyMaterial } from './types'
-
-const COIN_TYPES = {
- ethereum: 60,
- bitcoin: 0,
- tron: 195,
- bfmeta: 9999, // bioforest
-} as const
-
-export type CoinType = keyof typeof COIN_TYPES
-
-export class KeyMaterialProvider {
- private hdKey: HDKey
- private cache = new Map()
-
- constructor(mnemonic: string, password?: string) {
- const seed = mnemonicToSeedSync(mnemonic, password)
- this.hdKey = HDKey.fromMasterSeed(seed)
- }
-
- /**
- * 获取指定 coinType 和 accountIndex 的密钥材料
- * 使用 BIP44 路径:m/44'/coinType'/accountIndex'/0/0
- */
- getKeyMaterial(coinType: CoinType, accountIndex = 0): KeyMaterial {
- const cacheKey = `${coinType}:${accountIndex}`
- const cached = this.cache.get(cacheKey)
- if (cached) return cached
-
- const coinTypeNum = COIN_TYPES[coinType]
- const path = `m/44'/${coinTypeNum}'/${accountIndex}'/0/0`
- const childKey = this.hdKey.derive(path)
-
- if (!childKey.privateKey || !childKey.publicKey) {
- throw new Error(`Key derivation failed for path: ${path}`)
- }
-
- const material: KeyMaterial = {
- privateKey: childKey.privateKey,
- publicKey: childKey.publicKey,
- }
-
- this.cache.set(cacheKey, material)
- return material
- }
-
- /**
- * 获取 EVM 密钥材料(Ethereum/BSC/Polygon 等共用同一密钥)
- */
- getEvmKeyMaterial(accountIndex = 0): KeyMaterial {
- return this.getKeyMaterial('ethereum', accountIndex)
- }
-
- /**
- * 获取 Bitcoin 密钥材料
- */
- getBitcoinKeyMaterial(accountIndex = 0): KeyMaterial {
- return this.getKeyMaterial('bitcoin', accountIndex)
- }
-
- /**
- * 获取 Tron 密钥材料
- */
- getTronKeyMaterial(accountIndex = 0): KeyMaterial {
- return this.getKeyMaterial('tron', accountIndex)
- }
-
- /**
- * 获取 BioForest 密钥材料
- */
- getBioforestKeyMaterial(accountIndex = 0): KeyMaterial {
- return this.getKeyMaterial('bfmeta', accountIndex)
- }
-}
diff --git a/src/services/wallet/chain-derivation/strategies/bioforest-strategy.ts b/src/services/wallet/chain-derivation/strategies/bioforest-strategy.ts
deleted file mode 100644
index 8f9cb0f1c..000000000
--- a/src/services/wallet/chain-derivation/strategies/bioforest-strategy.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * BioForest Derivation Strategy
- *
- * BioForest 链地址派生策略
- * 注意:BioForest 使用 Ed25519 + SHA256 seed,与 BIP44 不同
- */
-
-import { sha256 } from '@noble/hashes/sha2.js'
-import { ripemd160 } from '@noble/hashes/legacy.js'
-import { ed25519 } from '@noble/curves/ed25519.js'
-import type { ChainConfig } from '@/services/chain-config'
-import type { DerivationStrategy, ChainAddress } from '../types'
-
-const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
-
-function base58Encode(bytes: Uint8Array): string {
- const digits: number[] = [0]
-
- for (const byte of bytes) {
- let carry = byte
- for (let i = 0; i < digits.length; i++) {
- carry += digits[i]! << 8
- digits[i] = carry % 58
- carry = Math.floor(carry / 58)
- }
- while (carry > 0) {
- digits.push(carry % 58)
- carry = Math.floor(carry / 58)
- }
- }
-
- let result = ''
- for (const byte of bytes) {
- if (byte === 0) result += BASE58_ALPHABET[0]!
- else break
- }
-
- for (let i = digits.length - 1; i >= 0; i--) {
- result += BASE58_ALPHABET[digits[i]!]
- }
-
- return result
-}
-
-function base58CheckEncode(payload: Uint8Array): string {
- const checksum = sha256(sha256(payload)).slice(0, 4)
- const full = new Uint8Array(payload.length + checksum.length)
- full.set(payload, 0)
- full.set(checksum, payload.length)
- return base58Encode(full)
-}
-
-export interface BioforestDerivationParams {
- mnemonic: string
- config: ChainConfig
- accountIndex: number
-}
-
-function deriveBioforestAddress(mnemonic: string, prefix = 'b'): string {
- const encoder = new TextEncoder()
- const secretBytes = encoder.encode(mnemonic)
- const seed = sha256(secretBytes)
- const publicKey = ed25519.getPublicKey(seed)
- const hash160 = ripemd160(sha256(publicKey))
- return prefix + base58CheckEncode(hash160)
-}
-
-export class BioforestStrategy implements DerivationStrategy {
- readonly chainKind = 'bioforest' as const
-
- supports(config: ChainConfig): boolean {
- return config.chainKind === 'bioforest'
- }
-
- /**
- * BioForest 使用不同的派生方式:
- * - 不使用 BIP44 派生的 keyMaterial
- * - 直接用 mnemonic 做 SHA256 生成 Ed25519 keypair
- *
- * 因此这个 strategy 需要特殊处理
- */
- derive(_params: { keyMaterial: { privateKey: Uint8Array; publicKey: Uint8Array }; config: ChainConfig; accountIndex: number }): ChainAddress {
- throw new Error('BioForest uses special derivation. Use deriveBioforest() instead.')
- }
-
- /**
- * BioForest 专用派生方法
- */
- deriveBioforest(params: BioforestDerivationParams): ChainAddress {
- const { mnemonic, config } = params
- const prefix = config.prefix ?? 'b'
- const address = deriveBioforestAddress(mnemonic, prefix)
-
- return {
- chainId: config.id,
- address,
- }
- }
-}
diff --git a/src/services/wallet/chain-derivation/strategies/bitcoin-strategy.ts b/src/services/wallet/chain-derivation/strategies/bitcoin-strategy.ts
deleted file mode 100644
index d35688cab..000000000
--- a/src/services/wallet/chain-derivation/strategies/bitcoin-strategy.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * Bitcoin Derivation Strategy
- *
- * Bitcoin 地址派生策略(P2PKH - Legacy)
- */
-
-import { sha256 } from '@noble/hashes/sha2.js'
-import { ripemd160 } from '@noble/hashes/legacy.js'
-import type { ChainConfig } from '@/services/chain-config'
-import type { DerivationStrategy, DerivationParams, ChainAddress } from '../types'
-
-const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
-
-function base58Encode(bytes: Uint8Array): string {
- const digits = [0]
-
- for (const byte of bytes) {
- let carry = byte
- for (let i = 0; i < digits.length; i++) {
- carry += digits[i]! << 8
- digits[i] = carry % 58
- carry = (carry / 58) | 0
- }
- while (carry > 0) {
- digits.push(carry % 58)
- carry = (carry / 58) | 0
- }
- }
-
- let result = ''
- for (const byte of bytes) {
- if (byte === 0) result += BASE58_ALPHABET[0]!
- else break
- }
-
- for (let i = digits.length - 1; i >= 0; i--) {
- result += BASE58_ALPHABET[digits[i]!]
- }
-
- return result
-}
-
-function publicKeyToBitcoinAddress(publicKey: Uint8Array, network: 'mainnet' | 'testnet' = 'mainnet'): string {
- const sha = sha256(publicKey)
- const hash160 = ripemd160(sha)
-
- const prefix = network === 'mainnet' ? 0x00 : 0x6f
- const prefixed = new Uint8Array([prefix, ...hash160])
-
- const checksum = sha256(sha256(prefixed)).slice(0, 4)
- const full = new Uint8Array([...prefixed, ...checksum])
-
- return base58Encode(full)
-}
-
-export class BitcoinStrategy implements DerivationStrategy {
- readonly chainKind = 'bitcoin' as const
-
- supports(config: ChainConfig): boolean {
- return config.chainKind === 'bitcoin'
- }
-
- derive(params: DerivationParams): ChainAddress {
- const { keyMaterial, config } = params
- const address = publicKeyToBitcoinAddress(keyMaterial.publicKey)
-
- return {
- chainId: config.id,
- address,
- }
- }
-}
diff --git a/src/services/wallet/chain-derivation/strategies/evm-strategy.ts b/src/services/wallet/chain-derivation/strategies/evm-strategy.ts
deleted file mode 100644
index 518cd33ae..000000000
--- a/src/services/wallet/chain-derivation/strategies/evm-strategy.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * EVM Derivation Strategy
- *
- * EVM 链(Ethereum/BSC/Polygon 等)地址派生策略
- */
-
-import { secp256k1 } from '@noble/curves/secp256k1.js'
-import { keccak_256 } from '@noble/hashes/sha3.js'
-import { bytesToHex } from '@noble/hashes/utils.js'
-import type { ChainConfig } from '@/services/chain-config'
-import type { DerivationStrategy, DerivationParams, ChainAddress } from '../types'
-
-function privateKeyToEvmAddress(privateKey: Uint8Array): string {
- const uncompressedPubKey = secp256k1.getPublicKey(privateKey, false)
- const pubKeyWithoutPrefix = uncompressedPubKey.slice(1)
- const hash = keccak_256(pubKeyWithoutPrefix)
- const address = hash.slice(-20)
- return '0x' + bytesToHex(address)
-}
-
-function toChecksumAddress(address: string): string {
- const addr = address.toLowerCase().replace('0x', '')
- const encoder = new TextEncoder()
- const hash = bytesToHex(keccak_256(encoder.encode(addr)))
-
- let checksumAddress = '0x'
- for (let i = 0; i < addr.length; i++) {
- if (parseInt(hash[i]!, 16) >= 8) {
- checksumAddress += addr[i]!.toUpperCase()
- } else {
- checksumAddress += addr[i]!
- }
- }
-
- return checksumAddress
-}
-
-export class EvmStrategy implements DerivationStrategy {
- readonly chainKind = 'evm' as const
-
- supports(config: ChainConfig): boolean {
- return config.chainKind === 'evm'
- }
-
- derive(params: DerivationParams): ChainAddress {
- const { keyMaterial, config } = params
- const rawAddress = privateKeyToEvmAddress(keyMaterial.privateKey)
- const address = toChecksumAddress(rawAddress)
-
- return {
- chainId: config.id,
- address,
- }
- }
-}
diff --git a/src/services/wallet/chain-derivation/strategies/index.ts b/src/services/wallet/chain-derivation/strategies/index.ts
deleted file mode 100644
index b0a58af9f..000000000
--- a/src/services/wallet/chain-derivation/strategies/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-/**
- * Derivation Strategies Index
- */
-
-export { EvmStrategy } from './evm-strategy'
-export { BitcoinStrategy } from './bitcoin-strategy'
-export { TronStrategy } from './tron-strategy'
-export { BioforestStrategy } from './bioforest-strategy'
diff --git a/src/services/wallet/chain-derivation/strategies/tron-strategy.ts b/src/services/wallet/chain-derivation/strategies/tron-strategy.ts
deleted file mode 100644
index 0e1076e25..000000000
--- a/src/services/wallet/chain-derivation/strategies/tron-strategy.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * Tron Derivation Strategy
- *
- * Tron 地址派生策略
- * 使用行业标准路径: m/44'/195'/accountIndex'/0/0
- * 兼容 TronLink、Trust Wallet、Ledger 等主流钱包
- */
-
-import { secp256k1 } from '@noble/curves/secp256k1.js'
-import { keccak_256 } from '@noble/hashes/sha3.js'
-import { sha256 } from '@noble/hashes/sha2.js'
-import type { ChainConfig } from '@/services/chain-config'
-import type { DerivationStrategy, DerivationParams, ChainAddress } from '../types'
-
-const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
-
-function base58Encode(bytes: Uint8Array): string {
- const digits: number[] = [0]
-
- for (const byte of bytes) {
- let carry = byte
- for (let i = 0; i < digits.length; i++) {
- carry += digits[i]! << 8
- digits[i] = carry % 58
- carry = Math.floor(carry / 58)
- }
- while (carry > 0) {
- digits.push(carry % 58)
- carry = Math.floor(carry / 58)
- }
- }
-
- let result = ''
- for (const byte of bytes) {
- if (byte === 0) result += BASE58_ALPHABET[0]!
- else break
- }
-
- for (let i = digits.length - 1; i >= 0; i--) {
- result += BASE58_ALPHABET[digits[i]!]
- }
-
- return result
-}
-
-function privateKeyToTronAddress(privateKey: Uint8Array): string {
- const uncompressedPubKey = secp256k1.getPublicKey(privateKey, false)
- const pubKeyWithoutPrefix = uncompressedPubKey.slice(1)
-
- const hash = keccak_256(pubKeyWithoutPrefix)
- const addressBytes = hash.slice(-20)
-
- // Tron 主网前缀 0x41
- const prefixed = new Uint8Array([0x41, ...addressBytes])
-
- // Double SHA256 checksum
- const checksum = sha256(sha256(prefixed)).slice(0, 4)
- const full = new Uint8Array([...prefixed, ...checksum])
-
- return base58Encode(full)
-}
-
-export class TronStrategy implements DerivationStrategy {
- readonly chainKind = 'tron' as const
-
- supports(config: ChainConfig): boolean {
- return config.chainKind === 'tron'
- }
-
- derive(params: DerivationParams): ChainAddress {
- const { keyMaterial, config } = params
- const address = privateKeyToTronAddress(keyMaterial.privateKey)
-
- return {
- chainId: config.id,
- address,
- }
- }
-}
diff --git a/src/services/wallet/chain-derivation/types.ts b/src/services/wallet/chain-derivation/types.ts
deleted file mode 100644
index 6c664a39e..000000000
--- a/src/services/wallet/chain-derivation/types.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * Chain Derivation Types
- *
- * 链地址派生模块的类型定义
- */
-
-import type { ChainConfig, ChainKind } from '@/services/chain-config'
-
-export interface ChainAddress {
- chainId: string
- address: string
-}
-
-export interface KeyMaterial {
- privateKey: Uint8Array
- publicKey: Uint8Array
-}
-
-export interface DerivationParams {
- keyMaterial: KeyMaterial
- config: ChainConfig
- accountIndex: number
-}
-
-export interface DerivationStrategy {
- chainKind: ChainKind
- supports(config: ChainConfig): boolean
- derive(params: DerivationParams): ChainAddress
-}
-
-export interface BuildWalletChainAddressesParams {
- mnemonic: string
- accountIndex?: number
- selectedChainIds: string[]
- chainConfigs: ChainConfig[]
-}
diff --git a/src/stores/chain-config.test.ts b/src/stores/chain-config.test.ts
index 12f1e98bf..8a5044b74 100644
--- a/src/stores/chain-config.test.ts
+++ b/src/stores/chain-config.test.ts
@@ -53,7 +53,7 @@ describe('chain-config store', () => {
if (requestUrl === subscriptionUrl) {
return new Response(
JSON.stringify([
- { id: 'remote-one', version: '1.0', chainKind: 'custom', name: 'Remote One', symbol: 'R1', decimals: 8 },
+ { id: 'remote-one', version: '1.0', chainKind: 'evm', name: 'Remote One', symbol: 'R1', decimals: 8 },
]),
{
status: 200,
diff --git a/src/stores/wallet.ts b/src/stores/wallet.ts
index b3141d39b..c1ba87929 100644
--- a/src/stores/wallet.ts
+++ b/src/stores/wallet.ts
@@ -629,20 +629,21 @@ export const walletActions = {
let newAddresses: Array<{ chain: string; address: string; publicKey: string }> = []
if (chainsToAdd.length > 0) {
// 动态导入避免循环依赖
- const { deriveAddressesForChains } = await import('@/lib/crypto/address-derivation')
-
+ const { deriveWalletChainAddresses } = await import('@/services/chain-adapter')
+
// 只派生需要添加的链
const chainsToDerive = chainConfigs.filter((c) => chainsToAdd.includes(c.id))
- const derivedAddresses = deriveAddressesForChains(mnemonic, chainsToDerive)
- const addressMap = new Map(derivedAddresses.map((a) => [a.chainId, { address: a.address, publicKey: a.publicKey }]))
-
- newAddresses = chainsToAdd
- .map((chainId) => {
- const derived = addressMap.get(chainId)
- if (!derived) return null
- return { chain: chainId, address: derived.address, publicKey: derived.publicKey }
- })
- .filter((a): a is { chain: string; address: string; publicKey: string } => a !== null)
+ const derivedAddresses = await deriveWalletChainAddresses({
+ mnemonic,
+ chainConfigs: chainsToDerive,
+ selectedChainIds: chainsToAdd,
+ })
+
+ newAddresses = derivedAddresses.map((a) => ({
+ chain: a.chainId,
+ address: a.address,
+ publicKey: a.publicKey ?? '',
+ }))
}
// 更新 IndexedDB - 添加新链地址