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 - 添加新链地址