refactor: move mnemonic verification from identity-service to blockchain-service

- Add /internal/verify-mnemonic API to blockchain-service
- Add /internal/derive-from-mnemonic API to blockchain-service
- Create MnemonicDerivationAdapter for BIP39 mnemonic address derivation
- Create BlockchainClientService in identity-service to call blockchain-service
- Remove WalletGeneratorService from identity-service
- Update recover-by-mnemonic handler to use blockchain-service API

This enforces proper domain boundaries - all blockchain/crypto operations
are now handled by blockchain-service.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-07 00:11:06 -08:00
parent a181fd0d2d
commit 852073ae11
18 changed files with 366 additions and 715 deletions

View File

@ -1,7 +1,8 @@
import { Controller, Post, Body, Get, Param } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { AddressDerivationService } from '@/application/services/address-derivation.service';
import { DeriveAddressDto } from '../dto/request';
import { MnemonicDerivationAdapter } from '@/infrastructure/blockchain';
import { DeriveAddressDto, VerifyMnemonicDto } from '../dto/request';
import { DeriveAddressResponseDto } from '../dto/response';
/**
@ -11,7 +12,10 @@ import { DeriveAddressResponseDto } from '../dto/response';
@ApiTags('Internal')
@Controller('internal')
export class InternalController {
constructor(private readonly addressDerivationService: AddressDerivationService) {}
constructor(
private readonly addressDerivationService: AddressDerivationService,
private readonly mnemonicDerivation: MnemonicDerivationAdapter,
) {}
@Post('derive-address')
@ApiOperation({ summary: '从公钥派生地址' })
@ -45,4 +49,29 @@ export class InternalController {
})),
};
}
@Post('verify-mnemonic')
@ApiOperation({ summary: '验证助记词是否匹配指定地址' })
@ApiResponse({ status: 200, description: '验证结果' })
async verifyMnemonic(@Body() dto: VerifyMnemonicDto) {
const result = this.mnemonicDerivation.verifyMnemonic(dto.mnemonic, dto.expectedAddresses);
return {
valid: result.valid,
matchedAddresses: result.matchedAddresses,
mismatchedAddresses: result.mismatchedAddresses,
};
}
@Post('derive-from-mnemonic')
@ApiOperation({ summary: '从助记词派生所有链地址' })
@ApiResponse({ status: 200, description: '派生的地址列表' })
async deriveFromMnemonic(@Body() dto: { mnemonic: string }) {
const addresses = this.mnemonicDerivation.deriveAllAddresses(dto.mnemonic);
return {
addresses: addresses.map((a) => ({
chainType: a.chainType,
address: a.address,
})),
};
}
}

View File

@ -1,2 +1,3 @@
export * from './query-balance.dto';
export * from './derive-address.dto';
export * from './verify-mnemonic.dto';

View File

@ -0,0 +1,22 @@
import { IsString, IsArray, ArrayMinSize } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class VerifyMnemonicDto {
@ApiProperty({
description: '助记词 (12或24个单词空格分隔)',
example: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
})
@IsString()
mnemonic: string;
@ApiProperty({
description: '期望的钱包地址列表,用于验证助记词',
example: [{ chainType: 'KAVA', address: 'kava1abc...' }],
})
@IsArray()
@ArrayMinSize(1)
expectedAddresses: Array<{
chainType: string;
address: string;
}>;
}

View File

@ -1,3 +1,4 @@
export * from './evm-provider.adapter';
export * from './address-derivation.adapter';
export * from './mnemonic-derivation.adapter';
export * from './block-scanner.service';

View File

@ -0,0 +1,148 @@
import { Injectable, Logger } from '@nestjs/common';
import { HDKey } from '@scure/bip32';
import { mnemonicToSeedSync, validateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { createHash } from 'crypto';
import { bech32 } from 'bech32';
import { Wallet } from 'ethers';
import { ChainTypeEnum } from '@/domain/enums';
export interface MnemonicDerivedAddress {
chainType: ChainTypeEnum;
address: string;
}
/**
*
*/
const CHAIN_CONFIG: Record<ChainTypeEnum, { derivationPath: string; prefix?: string }> = {
[ChainTypeEnum.KAVA]: {
derivationPath: "m/44'/459'/0'/0/0",
prefix: 'kava',
},
[ChainTypeEnum.DST]: {
derivationPath: "m/44'/118'/0'/0/0",
prefix: 'dst',
},
[ChainTypeEnum.BSC]: {
derivationPath: "m/44'/60'/0'/0/0",
},
};
/**
*
* BIP39
*/
@Injectable()
export class MnemonicDerivationAdapter {
private readonly logger = new Logger(MnemonicDerivationAdapter.name);
/**
*
*/
validateMnemonic(mnemonic: string): boolean {
return validateMnemonic(mnemonic, wordlist);
}
/**
*
*/
deriveAllAddresses(mnemonic: string): MnemonicDerivedAddress[] {
if (!this.validateMnemonic(mnemonic)) {
throw new Error('Invalid mnemonic');
}
const seed = mnemonicToSeedSync(mnemonic);
const addresses: MnemonicDerivedAddress[] = [];
this.logger.log('[DERIVE] Starting mnemonic address derivation');
// KAVA
const kavaAddress = this.deriveCosmosAddress(seed, CHAIN_CONFIG[ChainTypeEnum.KAVA]);
addresses.push({ chainType: ChainTypeEnum.KAVA, address: kavaAddress });
this.logger.log(`[DERIVE] KAVA address: ${kavaAddress}`);
// DST
const dstAddress = this.deriveCosmosAddress(seed, CHAIN_CONFIG[ChainTypeEnum.DST]);
addresses.push({ chainType: ChainTypeEnum.DST, address: dstAddress });
this.logger.log(`[DERIVE] DST address: ${dstAddress}`);
// BSC
const bscAddress = this.deriveEvmAddress(seed, CHAIN_CONFIG[ChainTypeEnum.BSC]);
addresses.push({ chainType: ChainTypeEnum.BSC, address: bscAddress });
this.logger.log(`[DERIVE] BSC address: ${bscAddress}`);
return addresses;
}
/**
*
*/
verifyMnemonic(
mnemonic: string,
expectedAddresses: Array<{ chainType: string; address: string }>,
): { valid: boolean; matchedAddresses: string[]; mismatchedAddresses: string[] } {
if (!this.validateMnemonic(mnemonic)) {
return { valid: false, matchedAddresses: [], mismatchedAddresses: [] };
}
const derivedAddresses = this.deriveAllAddresses(mnemonic);
const derivedMap = new Map(derivedAddresses.map((a) => [a.chainType, a.address.toLowerCase()]));
const matchedAddresses: string[] = [];
const mismatchedAddresses: string[] = [];
for (const expected of expectedAddresses) {
const chainType = expected.chainType as ChainTypeEnum;
const derivedAddress = derivedMap.get(chainType);
if (derivedAddress && derivedAddress === expected.address.toLowerCase()) {
matchedAddresses.push(expected.chainType);
} else {
mismatchedAddresses.push(expected.chainType);
}
}
return {
valid: mismatchedAddresses.length === 0 && matchedAddresses.length > 0,
matchedAddresses,
mismatchedAddresses,
};
}
/**
* Cosmos (bech32 )
*/
private deriveCosmosAddress(
seed: Uint8Array,
config: { derivationPath: string; prefix?: string },
): string {
const hdkey = HDKey.fromMasterSeed(seed);
const childKey = hdkey.derive(config.derivationPath);
if (!childKey.publicKey) {
throw new Error('Failed to derive public key');
}
const hash = createHash('sha256').update(childKey.publicKey).digest();
const addressHash = createHash('ripemd160').update(hash).digest();
const words = bech32.toWords(addressHash);
return bech32.encode(config.prefix!, words);
}
/**
* EVM
*/
private deriveEvmAddress(seed: Uint8Array, config: { derivationPath: string }): string {
const hdkey = HDKey.fromMasterSeed(seed);
const childKey = hdkey.derive(config.derivationPath);
if (!childKey.privateKey) {
throw new Error('Failed to derive private key');
}
const wallet = new Wallet(Buffer.from(childKey.privateKey).toString('hex'));
return wallet.address;
}
}

View File

@ -2,7 +2,7 @@ import { Global, Module } from '@nestjs/common';
import { PrismaService } from './persistence/prisma/prisma.service';
import { RedisService, AddressCacheService } from './redis';
import { EventPublisherService, MpcEventConsumerService } from './kafka';
import { EvmProviderAdapter, AddressDerivationAdapter, BlockScannerService } from './blockchain';
import { EvmProviderAdapter, AddressDerivationAdapter, MnemonicDerivationAdapter, BlockScannerService } from './blockchain';
import { DomainModule } from '@/domain/domain.module';
import {
DEPOSIT_TRANSACTION_REPOSITORY,
@ -30,6 +30,7 @@ import {
// 区块链适配器
EvmProviderAdapter,
AddressDerivationAdapter,
MnemonicDerivationAdapter,
BlockScannerService,
// 缓存服务
@ -60,6 +61,7 @@ import {
MpcEventConsumerService,
EvmProviderAdapter,
AddressDerivationAdapter,
MnemonicDerivationAdapter,
BlockScannerService,
AddressCacheService,
DEPOSIT_TRANSACTION_REPOSITORY,

View File

@ -30,7 +30,10 @@
"Bash(powershell -Command:*)",
"Bash(git pull:*)",
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" add -A)",
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" status)"
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" status)",
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" commit -m \"$(cat <<''EOF''\nfix(mpc-service): change healthcheck from wget to curl\n\nDocker compose healthcheck was using wget which is not installed in the\nnode:20-slim image. Changed to use curl and corrected endpoint path.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" push)",
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" show cf308ef --stat)"
],
"deny": [],
"ask": []

View File

@ -1,19 +1,21 @@
import { Injectable, Inject } from '@nestjs/common';
import { Injectable, Inject, Logger } from '@nestjs/common';
import { RecoverByMnemonicCommand } from './recover-by-mnemonic.command';
import { UserAccountRepository, USER_ACCOUNT_REPOSITORY } from '@/domain/repositories/user-account.repository.interface';
import { WalletGeneratorService } from '@/domain/services';
import { AccountSequence, ChainType, Mnemonic } from '@/domain/value-objects';
import { AccountSequence, ChainType } from '@/domain/value-objects';
import { TokenService } from '@/application/services/token.service';
import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service';
import { BlockchainClientService } from '@/infrastructure/external/blockchain/blockchain-client.service';
import { ApplicationError } from '@/shared/exceptions/domain.exception';
import { RecoverAccountResult } from '../index';
@Injectable()
export class RecoverByMnemonicHandler {
private readonly logger = new Logger(RecoverByMnemonicHandler.name);
constructor(
@Inject(USER_ACCOUNT_REPOSITORY)
private readonly userRepository: UserAccountRepository,
private readonly walletGenerator: WalletGeneratorService,
private readonly blockchainClient: BlockchainClientService,
private readonly tokenService: TokenService,
private readonly eventPublisher: EventPublisherService,
) {}
@ -24,18 +26,31 @@ export class RecoverByMnemonicHandler {
if (!account) throw new ApplicationError('账户序列号不存在');
if (!account.isActive) throw new ApplicationError('账户已冻结或注销');
const mnemonic = Mnemonic.create(command.mnemonic);
const wallets = this.walletGenerator.recoverWalletSystem({
userId: account.userId,
mnemonic,
deviceId: command.newDeviceId,
// 获取账户的钱包地址用于验证
const expectedAddresses: Array<{ chainType: string; address: string }> = [];
const kavaWallet = account.getWalletAddress(ChainType.KAVA);
if (kavaWallet) {
expectedAddresses.push({ chainType: 'KAVA', address: kavaWallet.address });
}
if (expectedAddresses.length === 0) {
throw new ApplicationError('账户没有关联的钱包地址');
}
// 调用 blockchain-service 验证助记词
this.logger.log(`Verifying mnemonic for account ${command.accountSequence}`);
const verifyResult = await this.blockchainClient.verifyMnemonic({
mnemonic: command.mnemonic,
expectedAddresses,
});
const kavaWallet = account.getWalletAddress(ChainType.KAVA);
if (!kavaWallet || kavaWallet.address !== wallets.get(ChainType.KAVA)!.address) {
if (!verifyResult.valid) {
this.logger.warn(`Mnemonic verification failed for account ${command.accountSequence}`);
throw new ApplicationError('助记词错误');
}
this.logger.log(`Mnemonic verified successfully for account ${command.accountSequence}`);
account.addDevice(command.newDeviceId, command.deviceName);
account.recordLogin();
await this.userRepository.save(account);

View File

@ -4,16 +4,17 @@ import { MpcKeyShareRepository, MPC_KEY_SHARE_REPOSITORY } from '@/domain/reposi
import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate';
import { WalletAddress } from '@/domain/entities/wallet-address.entity';
import {
AccountSequenceGeneratorService, UserValidatorService, WalletGeneratorService,
AccountSequenceGeneratorService, UserValidatorService,
} from '@/domain/services';
import {
UserId, PhoneNumber, ReferralCode, AccountSequence, ProvinceCode, CityCode,
ChainType, Mnemonic, KYCInfo, HardwareInfo,
ChainType, KYCInfo, HardwareInfo,
} from '@/domain/value-objects';
import { TokenService } from './token.service';
import { RedisService } from '@/infrastructure/redis/redis.service';
import { SmsService } from '@/infrastructure/external/sms/sms.service';
import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service';
import { BlockchainClientService } from '@/infrastructure/external/blockchain/blockchain-client.service';
import { MpcWalletService } from '@/infrastructure/external/mpc';
import { ApplicationError } from '@/shared/exceptions/domain.exception';
import { generateRandomIdentity } from '@/shared/utils';
@ -40,7 +41,7 @@ export class UserApplicationService {
private readonly mpcKeyShareRepository: MpcKeyShareRepository,
private readonly sequenceGenerator: AccountSequenceGeneratorService,
private readonly validatorService: UserValidatorService,
private readonly walletGenerator: WalletGeneratorService,
private readonly blockchainClient: BlockchainClientService,
private readonly mpcWalletService: MpcWalletService,
private readonly tokenService: TokenService,
private readonly redisService: RedisService,
@ -155,15 +156,24 @@ export class UserApplicationService {
if (!account) throw new ApplicationError('账户序列号不存在');
if (!account.isActive) throw new ApplicationError('账户已冻结或注销');
const mnemonic = Mnemonic.create(command.mnemonic);
const wallets = this.walletGenerator.recoverWalletSystem({
userId: account.userId,
mnemonic,
deviceId: command.newDeviceId,
// 获取账户的钱包地址用于验证
const expectedAddresses: Array<{ chainType: string; address: string }> = [];
const kavaWallet = account.getWalletAddress(ChainType.KAVA);
if (kavaWallet) {
expectedAddresses.push({ chainType: 'KAVA', address: kavaWallet.address });
}
if (expectedAddresses.length === 0) {
throw new ApplicationError('账户没有关联的钱包地址');
}
// 调用 blockchain-service 验证助记词
const verifyResult = await this.blockchainClient.verifyMnemonic({
mnemonic: command.mnemonic,
expectedAddresses,
});
const kavaWallet = account.getWalletAddress(ChainType.KAVA);
if (!kavaWallet || kavaWallet.address !== wallets.get(ChainType.KAVA)!.address) {
if (!verifyResult.valid) {
throw new ApplicationError('助记词错误');
}

View File

@ -11,14 +11,12 @@ import { InfrastructureModule } from '@/infrastructure/infrastructure.module';
{ provide: USER_ACCOUNT_REPOSITORY, useClass: UserAccountRepositoryImpl },
AccountSequenceGeneratorService,
UserValidatorService,
// WalletGeneratorService 由 InfrastructureModule 提供
UserAccountFactory,
],
exports: [
USER_ACCOUNT_REPOSITORY,
AccountSequenceGeneratorService,
UserValidatorService,
// WalletGeneratorService 由 InfrastructureModule 导出
UserAccountFactory,
],
})

View File

@ -2,14 +2,6 @@ import { Injectable, Inject } from '@nestjs/common';
import { UserAccountRepository, USER_ACCOUNT_REPOSITORY } from '@/domain/repositories/user-account.repository.interface';
import { AccountSequence, PhoneNumber, ReferralCode, ChainType } from '@/domain/value-objects';
// 导出 WalletGeneratorService 和相关类型
export {
WalletGeneratorService,
MpcWalletGenerationParams,
MpcWalletGenerationResult,
ChainWalletInfo,
} from './wallet-generator.service';
// ============ ValidationResult ============
export class ValidationResult {
private constructor(

View File

@ -1,87 +0,0 @@
import { WalletAddress, MpcSignature } from '@/domain/entities/wallet-address.entity';
import { ChainType, Mnemonic, UserId } from '@/domain/value-objects';
/**
* MPC
*/
export interface MpcWalletGenerationParams {
userId: string;
username: string; // 用户名 (用于 MPC keygen)
deviceId: string;
}
/**
*
*/
export interface ChainWalletInfo {
chainType: 'KAVA' | 'DST' | 'BSC';
address: string;
publicKey: string;
addressDigest: string;
signature: MpcSignature; // 64 bytes hex (R + S)
}
/**
* MPC
*/
export interface MpcWalletGenerationResult {
publicKey: string;
delegateShare: string; // delegate share (加密的用户分片)
serverParties: string[]; // 服务器 party IDs
wallets: ChainWalletInfo[];
sessionId: string;
}
/**
* ()
*
*
*/
export abstract class WalletGeneratorService {
/**
* 使 MPC 2-of-3
*
* @param params ID和设备ID
* @returns MPC
*/
abstract generateMpcWalletSystem(params: MpcWalletGenerationParams): Promise<MpcWalletGenerationResult>;
/**
* MPC
*
* @param userId ID
* @param walletInfos MPC
* @returns Map
*/
convertToWalletEntities(
userId: UserId,
walletInfos: ChainWalletInfo[],
): Map<ChainType, WalletAddress> {
const wallets = new Map<ChainType, WalletAddress>();
for (const info of walletInfos) {
const chainType = ChainType[info.chainType as keyof typeof ChainType];
const wallet = WalletAddress.createMpc({
userId,
chainType,
address: info.address,
publicKey: info.publicKey,
addressDigest: info.addressDigest,
signature: info.signature,
});
wallets.set(chainType, wallet);
}
return wallets;
}
/**
* @deprecated MPC 使
*
*/
abstract recoverWalletSystem(params: {
userId: UserId;
mnemonic: Mnemonic;
deviceId: string;
}): Map<ChainType, WalletAddress>;
}

View File

@ -0,0 +1,104 @@
/**
* Blockchain Client Service
*
* identity-service blockchain-service API
* -
* -
*/
import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';
export interface VerifyMnemonicParams {
mnemonic: string;
expectedAddresses: Array<{
chainType: string;
address: string;
}>;
}
export interface VerifyMnemonicResult {
valid: boolean;
matchedAddresses: string[];
mismatchedAddresses: string[];
}
export interface DerivedAddress {
chainType: string;
address: string;
}
@Injectable()
export class BlockchainClientService {
private readonly logger = new Logger(BlockchainClientService.name);
private readonly blockchainServiceUrl: string;
constructor(
private readonly httpService: HttpService,
private readonly configService: ConfigService,
) {
this.blockchainServiceUrl = this.configService.get<string>(
'BLOCKCHAIN_SERVICE_URL',
'http://blockchain-service:3000',
);
this.logger.log(`[INIT] BlockchainClientService initialized`);
this.logger.log(`[INIT] URL: ${this.blockchainServiceUrl}`);
}
/**
*
*/
async verifyMnemonic(params: VerifyMnemonicParams): Promise<VerifyMnemonicResult> {
this.logger.log(`Verifying mnemonic against ${params.expectedAddresses.length} addresses`);
try {
const response = await firstValueFrom(
this.httpService.post<VerifyMnemonicResult>(
`${this.blockchainServiceUrl}/internal/verify-mnemonic`,
{
mnemonic: params.mnemonic,
expectedAddresses: params.expectedAddresses,
},
{
headers: { 'Content-Type': 'application/json' },
timeout: 30000,
},
),
);
this.logger.log(`Mnemonic verification result: valid=${response.data.valid}`);
return response.data;
} catch (error) {
this.logger.error('Failed to verify mnemonic', error);
throw error;
}
}
/**
*
*/
async deriveFromMnemonic(mnemonic: string): Promise<DerivedAddress[]> {
this.logger.log('Deriving addresses from mnemonic');
try {
const response = await firstValueFrom(
this.httpService.post<{ addresses: DerivedAddress[] }>(
`${this.blockchainServiceUrl}/internal/derive-from-mnemonic`,
{ mnemonic },
{
headers: { 'Content-Type': 'application/json' },
timeout: 30000,
},
),
);
this.logger.log(`Derived ${response.data.addresses.length} addresses from mnemonic`);
return response.data.addresses;
} catch (error) {
this.logger.error('Failed to derive addresses from mnemonic', error);
throw error;
}
}
}

View File

@ -1,8 +0,0 @@
import { Module } from '@nestjs/common';
import { WalletGeneratorServiceImpl } from './wallet-generator.service.impl';
@Module({
providers: [WalletGeneratorServiceImpl],
exports: [WalletGeneratorServiceImpl],
})
export class BlockchainModule {}

View File

@ -1,79 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { createHash } from 'crypto';
import {
WalletGeneratorService,
MpcWalletGenerationParams,
MpcWalletGenerationResult,
} from '@/domain/services/wallet-generator.service';
import { WalletAddress } from '@/domain/entities/wallet-address.entity';
import { ChainType, Mnemonic, UserId } from '@/domain/value-objects';
import { MpcWalletService } from '@/infrastructure/external/mpc/mpc-wallet.service';
/**
*
*
* 使 MPC 2-of-3
*/
@Injectable()
export class WalletGeneratorServiceImpl extends WalletGeneratorService {
private readonly logger = new Logger(WalletGeneratorServiceImpl.name);
constructor(private readonly mpcWalletService: MpcWalletService) {
super();
}
/**
* 使 MPC 2-of-3
*/
async generateMpcWalletSystem(
params: MpcWalletGenerationParams,
): Promise<MpcWalletGenerationResult> {
this.logger.log(`Generating MPC wallet system for user=${params.userId}`);
const result = await this.mpcWalletService.generateMpcWallet(params);
this.logger.log(
`MPC wallet system generated: ${result.wallets.length} wallets, sessionId=${result.sessionId}`,
);
return result;
}
/**
* @deprecated MPC 使
*
*/
recoverWalletSystem(params: {
userId: UserId;
mnemonic: Mnemonic;
deviceId: string;
}): Map<ChainType, WalletAddress> {
this.logger.warn(
'recoverWalletSystem is deprecated - MPC mode does not use mnemonic recovery',
);
const encryptionKey = this.deriveEncryptionKey(
params.deviceId,
params.userId.toString(),
);
const wallets = new Map<ChainType, WalletAddress>();
const chains = [ChainType.KAVA, ChainType.DST, ChainType.BSC];
for (const chainType of chains) {
const wallet = WalletAddress.createFromMnemonic({
userId: params.userId,
chainType,
mnemonic: params.mnemonic,
encryptionKey,
});
wallets.set(chainType, wallet);
}
return wallets;
}
private deriveEncryptionKey(deviceId: string, userId: string): string {
const input = `${deviceId}:${userId}`;
return createHash('sha256').update(input).digest('hex');
}
}

View File

@ -1,296 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { WalletGeneratorService } from './wallet-generator.service';
import { ChainType, Mnemonic } from '@/domain/value-objects';
describe('WalletGeneratorService', () => {
let service: WalletGeneratorService;
let configService: ConfigService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
WalletGeneratorService,
{
provide: ConfigService,
useValue: {
get: jest.fn().mockReturnValue('test-salt'),
},
},
],
}).compile();
service = module.get<WalletGeneratorService>(WalletGeneratorService);
configService = module.get<ConfigService>(ConfigService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('generateWalletSystem', () => {
it('应该生成完整的钱包系统', () => {
const result = service.generateWalletSystem({
userId: '12345',
deviceId: 'test-device-id',
});
expect(result).toBeDefined();
expect(result.mnemonic).toBeDefined();
expect(result.mnemonic).toBeInstanceOf(Mnemonic);
expect(result.wallets).toBeDefined();
expect(result.wallets.size).toBe(3);
});
it('应该为所有链生成钱包地址', () => {
const result = service.generateWalletSystem({
userId: '12345',
deviceId: 'test-device-id',
});
expect(result.wallets.has(ChainType.KAVA)).toBe(true);
expect(result.wallets.has(ChainType.DST)).toBe(true);
expect(result.wallets.has(ChainType.BSC)).toBe(true);
});
it('生成的 KAVA 地址应该有正确的格式', () => {
const result = service.generateWalletSystem({
userId: '12345',
deviceId: 'test-device-id',
});
const kavaWallet = result.wallets.get(ChainType.KAVA);
expect(kavaWallet).toBeDefined();
expect(kavaWallet!.address).toMatch(/^kava1[a-z0-9]{38}$/);
});
it('生成的 DST 地址应该有正确的格式', () => {
const result = service.generateWalletSystem({
userId: '12345',
deviceId: 'test-device-id',
});
const dstWallet = result.wallets.get(ChainType.DST);
expect(dstWallet).toBeDefined();
expect(dstWallet!.address).toMatch(/^dst1[a-z0-9]{38}$/);
});
it('生成的 BSC 地址应该有正确的格式', () => {
const result = service.generateWalletSystem({
userId: '12345',
deviceId: 'test-device-id',
});
const bscWallet = result.wallets.get(ChainType.BSC);
expect(bscWallet).toBeDefined();
expect(bscWallet!.address).toMatch(/^0x[a-fA-F0-9]{40}$/);
});
it('每次生成的钱包应该不同', () => {
const result1 = service.generateWalletSystem({
userId: '11111',
deviceId: 'test-device-id-1',
});
const result2 = service.generateWalletSystem({
userId: '22222',
deviceId: 'test-device-id-2',
});
expect(result1.mnemonic.value).not.toBe(result2.mnemonic.value);
expect(result1.wallets.get(ChainType.KAVA)!.address).not.toBe(
result2.wallets.get(ChainType.KAVA)!.address
);
});
});
describe('recoverWalletSystem', () => {
it('应该使用助记词恢复钱包系统', () => {
// 先生成一个钱包系统
const original = service.generateWalletSystem({
userId: '12345',
deviceId: 'test-device-id',
});
// 使用助记词恢复
const recovered = service.recoverWalletSystem({
userId: '12345',
mnemonic: original.mnemonic,
deviceId: 'new-device-id',
});
expect(recovered.size).toBe(3);
// 验证恢复的地址与原地址相同
expect(recovered.get(ChainType.KAVA)!.address).toBe(
original.wallets.get(ChainType.KAVA)!.address
);
expect(recovered.get(ChainType.DST)!.address).toBe(
original.wallets.get(ChainType.DST)!.address
);
expect(recovered.get(ChainType.BSC)!.address).toBe(
original.wallets.get(ChainType.BSC)!.address
);
});
it('不同设备应该生成相同的地址(相同助记词)', () => {
const original = service.generateWalletSystem({
userId: '12345',
deviceId: 'device-1',
});
const recovered1 = service.recoverWalletSystem({
userId: '12345',
mnemonic: original.mnemonic,
deviceId: 'device-2',
});
const recovered2 = service.recoverWalletSystem({
userId: '12345',
mnemonic: original.mnemonic,
deviceId: 'device-3',
});
expect(recovered1.get(ChainType.KAVA)!.address).toBe(
recovered2.get(ChainType.KAVA)!.address
);
});
});
describe('deriveAddress', () => {
it('相同的助记词应该派生相同的地址', () => {
const mnemonic = Mnemonic.generate();
const address1 = service.deriveAddress(ChainType.KAVA, mnemonic);
const address2 = service.deriveAddress(ChainType.KAVA, mnemonic);
expect(address1).toBe(address2);
});
it('不同的助记词应该派生不同的地址', () => {
const mnemonic1 = Mnemonic.generate();
const mnemonic2 = Mnemonic.generate();
const address1 = service.deriveAddress(ChainType.KAVA, mnemonic1);
const address2 = service.deriveAddress(ChainType.KAVA, mnemonic2);
expect(address1).not.toBe(address2);
});
it('应该为不同的链派生不同的地址', () => {
const mnemonic = Mnemonic.generate();
const kavaAddress = service.deriveAddress(ChainType.KAVA, mnemonic);
const dstAddress = service.deriveAddress(ChainType.DST, mnemonic);
const bscAddress = service.deriveAddress(ChainType.BSC, mnemonic);
expect(kavaAddress).not.toBe(dstAddress);
expect(kavaAddress).not.toBe(bscAddress);
expect(dstAddress).not.toBe(bscAddress);
});
});
describe('verifyMnemonic', () => {
it('应该验证正确的助记词', () => {
const result = service.generateWalletSystem({
userId: '12345',
deviceId: 'test-device-id',
});
const kavaWallet = result.wallets.get(ChainType.KAVA)!;
const isValid = service.verifyMnemonic(
result.mnemonic,
ChainType.KAVA,
kavaWallet.address
);
expect(isValid).toBe(true);
});
it('应该拒绝错误的助记词', () => {
const result1 = service.generateWalletSystem({
userId: '11111',
deviceId: 'test-device-id-1',
});
const result2 = service.generateWalletSystem({
userId: '22222',
deviceId: 'test-device-id-2',
});
const kavaWallet1 = result1.wallets.get(ChainType.KAVA)!;
const isValid = service.verifyMnemonic(
result2.mnemonic,
ChainType.KAVA,
kavaWallet1.address
);
expect(isValid).toBe(false);
});
});
describe('加密和解密', () => {
it('应该正确加密和解密助记词', () => {
const mnemonic = 'test mnemonic phrase for encryption';
const key = 'encryption-key';
const encrypted = service.encryptMnemonic(mnemonic, key);
expect(encrypted).toBeDefined();
expect(encrypted.encrypted).toBeDefined();
expect(encrypted.iv).toBeDefined();
expect(encrypted.authTag).toBeDefined();
const decrypted = service.decryptMnemonic(encrypted, key);
expect(decrypted).toBe(mnemonic);
});
it('使用错误的密钥应该解密失败', () => {
const mnemonic = 'test mnemonic phrase';
const key = 'correct-key';
const wrongKey = 'wrong-key';
const encrypted = service.encryptMnemonic(mnemonic, key);
expect(() => {
service.decryptMnemonic(encrypted, wrongKey);
}).toThrow();
});
it('相同的助记词和密钥应该生成不同的密文(因为随机 IV', () => {
const mnemonic = 'test mnemonic phrase';
const key = 'encryption-key';
const encrypted1 = service.encryptMnemonic(mnemonic, key);
const encrypted2 = service.encryptMnemonic(mnemonic, key);
// 密文应该不同(因为 IV 不同)
expect(encrypted1.encrypted).not.toBe(encrypted2.encrypted);
expect(encrypted1.iv).not.toBe(encrypted2.iv);
// 但解密后应该相同
const decrypted1 = service.decryptMnemonic(encrypted1, key);
const decrypted2 = service.decryptMnemonic(encrypted2, key);
expect(decrypted1).toBe(mnemonic);
expect(decrypted2).toBe(mnemonic);
});
});
describe('deriveEncryptionKey', () => {
it('相同的输入应该生成相同的密钥', () => {
const key1 = service.deriveEncryptionKey('device-1', 'user-1');
const key2 = service.deriveEncryptionKey('device-1', 'user-1');
expect(key1).toBe(key2);
});
it('不同的输入应该生成不同的密钥', () => {
const key1 = service.deriveEncryptionKey('device-1', 'user-1');
const key2 = service.deriveEncryptionKey('device-2', 'user-1');
const key3 = service.deriveEncryptionKey('device-1', 'user-2');
expect(key1).not.toBe(key2);
expect(key1).not.toBe(key3);
expect(key2).not.toBe(key3);
});
});
});

View File

@ -1,200 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { HDKey } from '@scure/bip32';
import {
createHash,
createCipheriv,
createDecipheriv,
randomBytes,
scryptSync,
} from 'crypto';
import { bech32 } from 'bech32';
import { ethers } from 'ethers';
import { ConfigService } from '@nestjs/config';
import { Mnemonic, UserId, ChainType, CHAIN_CONFIG } from '@/domain/value-objects';
import { WalletAddress } from '@/domain/entities/wallet-address.entity';
import { DomainError } from '@/shared/exceptions/domain.exception';
export interface WalletSystemResult {
mnemonic: Mnemonic;
wallets: Map<ChainType, WalletAddress>;
}
export interface EncryptedMnemonicData {
encrypted: string;
authTag: string;
iv: string;
}
@Injectable()
export class WalletGeneratorService {
private readonly logger = new Logger(WalletGeneratorService.name);
private readonly encryptionSalt: string;
constructor(private readonly configService: ConfigService) {
this.encryptionSalt = configService.get(
'WALLET_ENCRYPTION_SALT',
'rwa-wallet-salt',
);
}
generateWalletSystem(params: {
userId: string;
deviceId: string;
}): WalletSystemResult {
const mnemonic = Mnemonic.generate();
const encryptionKey = this.deriveEncryptionKey(
params.deviceId,
params.userId,
);
const wallets = new Map<ChainType, WalletAddress>();
const chains = [ChainType.KAVA, ChainType.DST, ChainType.BSC];
const userId = UserId.create(params.userId);
for (const chainType of chains) {
const wallet = WalletAddress.createFromMnemonic({
userId,
chainType,
mnemonic,
encryptionKey,
});
wallets.set(chainType, wallet);
}
this.logger.debug(`Generated wallet system for user: ${params.userId}`);
return { mnemonic, wallets };
}
recoverWalletSystem(params: {
userId: string;
mnemonic: Mnemonic;
deviceId: string;
}): Map<ChainType, WalletAddress> {
const encryptionKey = this.deriveEncryptionKey(
params.deviceId,
params.userId,
);
const wallets = new Map<ChainType, WalletAddress>();
const chains = [ChainType.KAVA, ChainType.DST, ChainType.BSC];
const userId = UserId.create(params.userId);
for (const chainType of chains) {
const wallet = WalletAddress.createFromMnemonic({
userId,
chainType,
mnemonic: params.mnemonic,
encryptionKey,
});
wallets.set(chainType, wallet);
}
this.logger.debug(`Recovered wallet system for user: ${params.userId}`);
return wallets;
}
deriveAddress(chainType: ChainType, mnemonic: Mnemonic): string {
const seed = Buffer.from(mnemonic.toSeed());
const config = CHAIN_CONFIG[chainType];
switch (chainType) {
case ChainType.KAVA:
case ChainType.DST:
return this.deriveCosmosAddress(
seed,
config.derivationPath,
config.prefix,
);
case ChainType.BSC:
return this.deriveEVMAddress(seed, config.derivationPath);
default:
throw new DomainError(`不支持的链类型: ${chainType}`);
}
}
verifyMnemonic(
mnemonic: Mnemonic,
chainType: ChainType,
expectedAddress: string,
): boolean {
const derivedAddress = this.deriveAddress(chainType, mnemonic);
return derivedAddress.toLowerCase() === expectedAddress.toLowerCase();
}
private deriveCosmosAddress(
seed: Buffer,
path: string,
prefix: string,
): string {
const hdkey = HDKey.fromMasterSeed(seed);
const childKey = hdkey.derive(path);
if (!childKey.publicKey) {
throw new DomainError('无法派生公钥');
}
const pubkey = childKey.publicKey;
const hash = createHash('sha256').update(pubkey).digest();
const addressHash = createHash('ripemd160').update(hash).digest();
const words = bech32.toWords(addressHash);
return bech32.encode(prefix, words);
}
private deriveEVMAddress(seed: Buffer, path: string): string {
const hdkey = HDKey.fromMasterSeed(seed);
const childKey = hdkey.derive(path);
if (!childKey.privateKey) {
throw new DomainError('无法派生私钥');
}
// 将 Uint8Array 转换为十六进制字符串
const privateKeyHex = '0x' + Buffer.from(childKey.privateKey).toString('hex');
const wallet = new ethers.Wallet(privateKeyHex);
return wallet.address;
}
encryptMnemonic(mnemonic: string, key: string): EncryptedMnemonicData {
const derivedKey = scryptSync(key, this.encryptionSalt, 32);
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-gcm', derivedKey, iv);
let encrypted = cipher.update(mnemonic, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
authTag: authTag.toString('hex'),
iv: iv.toString('hex'),
};
}
decryptMnemonic(
encryptedData: EncryptedMnemonicData,
key: string,
): string {
const derivedKey = scryptSync(key, this.encryptionSalt, 32);
const iv = Buffer.from(encryptedData.iv, 'hex');
const authTag = Buffer.from(encryptedData.authTag, 'hex');
const decipher = createDecipheriv('aes-256-gcm', derivedKey, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
deriveEncryptionKey(deviceId: string, userId: string): string {
const input = `${deviceId}:${userId}`;
return createHash('sha256').update(input).digest('hex');
}
}

View File

@ -1,5 +1,6 @@
import { Module, Global } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { ConfigModule } from '@nestjs/config';
import { PrismaService } from './persistence/prisma/prisma.service';
import { UserAccountRepositoryImpl } from './persistence/repositories/user-account.repository.impl';
import { MpcKeyShareRepositoryImpl } from './persistence/repositories/mpc-key-share.repository.impl';
@ -8,14 +9,14 @@ import { RedisService } from './redis/redis.service';
import { EventPublisherService } from './kafka/event-publisher.service';
import { MpcEventConsumerService } from './kafka/mpc-event-consumer.service';
import { SmsService } from './external/sms/sms.service';
import { WalletGeneratorServiceImpl } from './external/blockchain/wallet-generator.service.impl';
import { BlockchainClientService } from './external/blockchain/blockchain-client.service';
import { MpcClientService, MpcWalletService } from './external/mpc';
import { MPC_KEY_SHARE_REPOSITORY } from '@/domain/repositories/mpc-key-share.repository.interface';
import { WalletGeneratorService } from '@/domain/services/wallet-generator.service';
@Global()
@Module({
imports: [
ConfigModule,
HttpModule.register({
timeout: 300000,
maxRedirects: 5,
@ -33,12 +34,8 @@ import { WalletGeneratorService } from '@/domain/services/wallet-generator.servi
EventPublisherService,
MpcEventConsumerService,
SmsService,
// WalletGeneratorService 抽象类由 WalletGeneratorServiceImpl 实现
WalletGeneratorServiceImpl,
{
provide: WalletGeneratorService,
useExisting: WalletGeneratorServiceImpl,
},
// BlockchainClientService 调用 blockchain-service API
BlockchainClientService,
MpcClientService,
MpcWalletService,
],
@ -54,8 +51,7 @@ import { WalletGeneratorService } from '@/domain/services/wallet-generator.servi
EventPublisherService,
MpcEventConsumerService,
SmsService,
WalletGeneratorServiceImpl,
WalletGeneratorService,
BlockchainClientService,
MpcClientService,
MpcWalletService,
],