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:
parent
a181fd0d2d
commit
852073ae11
|
|
@ -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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export * from './query-balance.dto';
|
||||
export * from './derive-address.dto';
|
||||
export * from './verify-mnemonic.dto';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}>;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './evm-provider.adapter';
|
||||
export * from './address-derivation.adapter';
|
||||
export * from './mnemonic-derivation.adapter';
|
||||
export * from './block-scanner.service';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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('助记词错误');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { WalletGeneratorServiceImpl } from './wallet-generator.service.impl';
|
||||
|
||||
@Module({
|
||||
providers: [WalletGeneratorServiceImpl],
|
||||
exports: [WalletGeneratorServiceImpl],
|
||||
})
|
||||
export class BlockchainModule {}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in New Issue