refactor(mnemonic): move recovery_mnemonics to blockchain-service (DDD)
- Add RecoveryMnemonic model to blockchain-service with accountSequence - Add MnemonicVerificationService for mnemonic verification logic - Update verify-mnemonic-hash endpoint to accept accountSequence - Remove PrismaService dependency from identity-service handler - identity-service now calls blockchain-service for mnemonic verification This follows DDD principles: blockchain-service owns all mnemonic-related data and operations, identity-service only handles account identity. 🤖 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
0311ecf498
commit
e95dc4ca57
|
|
@ -147,6 +147,37 @@ model TransactionRequest {
|
||||||
@@map("transaction_requests")
|
@@map("transaction_requests")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 账户恢复助记词
|
||||||
|
// 与账户序列号关联,用于账户恢复验证
|
||||||
|
// ============================================
|
||||||
|
model RecoveryMnemonic {
|
||||||
|
id BigInt @id @default(autoincrement())
|
||||||
|
accountSequence Int @map("account_sequence") // 8位账户序列号
|
||||||
|
publicKey String @map("public_key") @db.VarChar(130) // 关联的钱包公钥
|
||||||
|
|
||||||
|
// 助记词存储 (加密)
|
||||||
|
encryptedMnemonic String @map("encrypted_mnemonic") @db.Text // AES加密的助记词
|
||||||
|
mnemonicHash String @map("mnemonic_hash") @db.VarChar(64) // SHA256哈希(用于验证)
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, REVOKED, REPLACED
|
||||||
|
isBackedUp Boolean @default(false) @map("is_backed_up") // 用户是否已备份
|
||||||
|
|
||||||
|
// 挂失/更换相关
|
||||||
|
revokedAt DateTime? @map("revoked_at")
|
||||||
|
revokedReason String? @map("revoked_reason") @db.VarChar(200)
|
||||||
|
replacedById BigInt? @map("replaced_by_id") // 被哪个新助记词替代
|
||||||
|
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
|
@@unique([accountSequence, status], name: "uk_account_active_mnemonic") // 一个账户只有一个ACTIVE助记词
|
||||||
|
@@index([accountSequence], name: "idx_recovery_account")
|
||||||
|
@@index([publicKey], name: "idx_recovery_public_key")
|
||||||
|
@@index([status], name: "idx_recovery_status")
|
||||||
|
@@map("recovery_mnemonics")
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 区块链事件日志 (Append-Only 审计)
|
// 区块链事件日志 (Append-Only 审计)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { Controller, Post, Body, Get, Param } from '@nestjs/common';
|
import { Controller, Post, Body, Get, Param } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||||
import { AddressDerivationService } from '@/application/services/address-derivation.service';
|
import { AddressDerivationService } from '@/application/services/address-derivation.service';
|
||||||
import { MnemonicDerivationAdapter, RecoveryMnemonicAdapter } from '@/infrastructure/blockchain';
|
import { MnemonicVerificationService } from '@/application/services/mnemonic-verification.service';
|
||||||
|
import { MnemonicDerivationAdapter } from '@/infrastructure/blockchain';
|
||||||
import { DeriveAddressDto, VerifyMnemonicDto, VerifyMnemonicHashDto } from '../dto/request';
|
import { DeriveAddressDto, VerifyMnemonicDto, VerifyMnemonicHashDto } from '../dto/request';
|
||||||
import { DeriveAddressResponseDto } from '../dto/response';
|
import { DeriveAddressResponseDto } from '../dto/response';
|
||||||
|
|
||||||
|
|
@ -14,8 +15,8 @@ import { DeriveAddressResponseDto } from '../dto/response';
|
||||||
export class InternalController {
|
export class InternalController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly addressDerivationService: AddressDerivationService,
|
private readonly addressDerivationService: AddressDerivationService,
|
||||||
|
private readonly mnemonicVerification: MnemonicVerificationService,
|
||||||
private readonly mnemonicDerivation: MnemonicDerivationAdapter,
|
private readonly mnemonicDerivation: MnemonicDerivationAdapter,
|
||||||
private readonly recoveryMnemonic: RecoveryMnemonicAdapter,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('derive-address')
|
@Post('derive-address')
|
||||||
|
|
@ -77,10 +78,13 @@ export class InternalController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('verify-mnemonic-hash')
|
@Post('verify-mnemonic-hash')
|
||||||
@ApiOperation({ summary: '验证助记词哈希是否匹配' })
|
@ApiOperation({ summary: '通过账户序列号验证助记词' })
|
||||||
@ApiResponse({ status: 200, description: '验证结果' })
|
@ApiResponse({ status: 200, description: '验证结果' })
|
||||||
async verifyMnemonicHash(@Body() dto: VerifyMnemonicHashDto) {
|
async verifyMnemonicHash(@Body() dto: VerifyMnemonicHashDto) {
|
||||||
const result = this.recoveryMnemonic.verifyMnemonic(dto.mnemonic, dto.expectedHash);
|
const result = await this.mnemonicVerification.verifyMnemonicByAccount({
|
||||||
|
accountSequence: dto.accountSequence,
|
||||||
|
mnemonic: dto.mnemonic,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
valid: result.valid,
|
valid: result.valid,
|
||||||
message: result.message,
|
message: result.message,
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import { IsString } from 'class-validator';
|
import { IsString, IsInt } from 'class-validator';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class VerifyMnemonicHashDto {
|
export class VerifyMnemonicHashDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '账户序列号 (8位数字)',
|
||||||
|
example: 10000001,
|
||||||
|
})
|
||||||
|
@IsInt()
|
||||||
|
accountSequence: number;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: '助记词 (12个单词,空格分隔)',
|
description: '助记词 (12个单词,空格分隔)',
|
||||||
example: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
example: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
mnemonic: string;
|
mnemonic: string;
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
description: '期望的助记词哈希',
|
|
||||||
example: 'a1b2c3d4e5f6...',
|
|
||||||
})
|
|
||||||
@IsString()
|
|
||||||
expectedHash: string;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { InfrastructureModule } from '@/infrastructure/infrastructure.module';
|
import { InfrastructureModule } from '@/infrastructure/infrastructure.module';
|
||||||
import { DomainModule } from '@/domain/domain.module';
|
import { DomainModule } from '@/domain/domain.module';
|
||||||
import { AddressDerivationService, DepositDetectionService, BalanceQueryService } from './services';
|
import {
|
||||||
|
AddressDerivationService,
|
||||||
|
DepositDetectionService,
|
||||||
|
BalanceQueryService,
|
||||||
|
MnemonicVerificationService,
|
||||||
|
} from './services';
|
||||||
import { MpcKeygenCompletedHandler } from './event-handlers';
|
import { MpcKeygenCompletedHandler } from './event-handlers';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|
@ -11,6 +16,7 @@ import { MpcKeygenCompletedHandler } from './event-handlers';
|
||||||
AddressDerivationService,
|
AddressDerivationService,
|
||||||
DepositDetectionService,
|
DepositDetectionService,
|
||||||
BalanceQueryService,
|
BalanceQueryService,
|
||||||
|
MnemonicVerificationService,
|
||||||
|
|
||||||
// 事件处理器
|
// 事件处理器
|
||||||
MpcKeygenCompletedHandler,
|
MpcKeygenCompletedHandler,
|
||||||
|
|
@ -19,6 +25,7 @@ import { MpcKeygenCompletedHandler } from './event-handlers';
|
||||||
AddressDerivationService,
|
AddressDerivationService,
|
||||||
DepositDetectionService,
|
DepositDetectionService,
|
||||||
BalanceQueryService,
|
BalanceQueryService,
|
||||||
|
MnemonicVerificationService,
|
||||||
MpcKeygenCompletedHandler,
|
MpcKeygenCompletedHandler,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './address-derivation.service';
|
export * from './address-derivation.service';
|
||||||
export * from './deposit-detection.service';
|
export * from './deposit-detection.service';
|
||||||
export * from './balance-query.service';
|
export * from './balance-query.service';
|
||||||
|
export * from './mnemonic-verification.service';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service';
|
||||||
|
import { RecoveryMnemonicAdapter } from '@/infrastructure/blockchain/recovery-mnemonic.adapter';
|
||||||
|
|
||||||
|
export interface VerifyMnemonicByAccountParams {
|
||||||
|
accountSequence: number;
|
||||||
|
mnemonic: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifyMnemonicResult {
|
||||||
|
valid: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 助记词验证服务
|
||||||
|
* 通过账户序列号查询存储的哈希并验证助记词
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class MnemonicVerificationService {
|
||||||
|
private readonly logger = new Logger(MnemonicVerificationService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly recoveryMnemonic: RecoveryMnemonicAdapter,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证助记词是否匹配指定账户
|
||||||
|
*/
|
||||||
|
async verifyMnemonicByAccount(params: VerifyMnemonicByAccountParams): Promise<VerifyMnemonicResult> {
|
||||||
|
const { accountSequence, mnemonic } = params;
|
||||||
|
this.logger.log(`Verifying mnemonic for account ${accountSequence}`);
|
||||||
|
|
||||||
|
// 1. 查询账户的 ACTIVE 助记词记录
|
||||||
|
const recoveryRecord = await this.prisma.recoveryMnemonic.findFirst({
|
||||||
|
where: {
|
||||||
|
accountSequence,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!recoveryRecord) {
|
||||||
|
this.logger.warn(`No active recovery mnemonic found for account ${accountSequence}`);
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Account has no recovery mnemonic configured',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 使用 RecoveryMnemonicAdapter 验证哈希
|
||||||
|
const result = this.recoveryMnemonic.verifyMnemonic(mnemonic, recoveryRecord.mnemonicHash);
|
||||||
|
|
||||||
|
if (result.valid) {
|
||||||
|
this.logger.log(`Mnemonic verified successfully for account ${accountSequence}`);
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`Mnemonic verification failed for account ${accountSequence}: ${result.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存助记词记录(创建账户时调用)
|
||||||
|
*/
|
||||||
|
async saveRecoveryMnemonic(params: {
|
||||||
|
accountSequence: number;
|
||||||
|
publicKey: string;
|
||||||
|
encryptedMnemonic: string;
|
||||||
|
mnemonicHash: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
this.logger.log(`Saving recovery mnemonic for account ${params.accountSequence}`);
|
||||||
|
|
||||||
|
await this.prisma.recoveryMnemonic.create({
|
||||||
|
data: {
|
||||||
|
accountSequence: params.accountSequence,
|
||||||
|
publicKey: params.publicKey,
|
||||||
|
encryptedMnemonic: params.encryptedMnemonic,
|
||||||
|
mnemonicHash: params.mnemonicHash,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
isBackedUp: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`Recovery mnemonic saved for account ${params.accountSequence}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记助记词已备份
|
||||||
|
*/
|
||||||
|
async markAsBackedUp(accountSequence: number): Promise<void> {
|
||||||
|
await this.prisma.recoveryMnemonic.updateMany({
|
||||||
|
where: {
|
||||||
|
accountSequence,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
isBackedUp: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.log(`Recovery mnemonic marked as backed up for account ${accountSequence}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,6 @@ import { UserAccountRepository, USER_ACCOUNT_REPOSITORY } from '@/domain/reposit
|
||||||
import { AccountSequence } from '@/domain/value-objects';
|
import { AccountSequence } from '@/domain/value-objects';
|
||||||
import { TokenService } from '@/application/services/token.service';
|
import { TokenService } from '@/application/services/token.service';
|
||||||
import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service';
|
import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service';
|
||||||
import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service';
|
|
||||||
import { BlockchainClientService } from '@/infrastructure/external/blockchain/blockchain-client.service';
|
import { BlockchainClientService } from '@/infrastructure/external/blockchain/blockchain-client.service';
|
||||||
import { ApplicationError } from '@/shared/exceptions/domain.exception';
|
import { ApplicationError } from '@/shared/exceptions/domain.exception';
|
||||||
import { RecoverAccountResult } from '../index';
|
import { RecoverAccountResult } from '../index';
|
||||||
|
|
@ -16,7 +15,6 @@ export class RecoverByMnemonicHandler {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(USER_ACCOUNT_REPOSITORY)
|
@Inject(USER_ACCOUNT_REPOSITORY)
|
||||||
private readonly userRepository: UserAccountRepository,
|
private readonly userRepository: UserAccountRepository,
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly tokenService: TokenService,
|
private readonly tokenService: TokenService,
|
||||||
private readonly eventPublisher: EventPublisherService,
|
private readonly eventPublisher: EventPublisherService,
|
||||||
private readonly blockchainClient: BlockchainClientService,
|
private readonly blockchainClient: BlockchainClientService,
|
||||||
|
|
@ -28,29 +26,16 @@ export class RecoverByMnemonicHandler {
|
||||||
if (!account) throw new ApplicationError('账户序列号不存在');
|
if (!account) throw new ApplicationError('账户序列号不存在');
|
||||||
if (!account.isActive) throw new ApplicationError('账户已冻结或注销');
|
if (!account.isActive) throw new ApplicationError('账户已冻结或注销');
|
||||||
|
|
||||||
// 从 recovery_mnemonics 表获取存储的助记词哈希
|
// 调用 blockchain-service 验证助记词(blockchain-service 内部查询哈希并验证)
|
||||||
const recoveryMnemonic = await this.prisma.recoveryMnemonic.findFirst({
|
this.logger.log(`Verifying mnemonic for account ${command.accountSequence}`);
|
||||||
where: {
|
const verifyResult = await this.blockchainClient.verifyMnemonicByAccount({
|
||||||
userId: account.userId.value,
|
accountSequence: command.accountSequence,
|
||||||
status: 'ACTIVE',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!recoveryMnemonic) {
|
|
||||||
this.logger.error(`No recovery mnemonic found for account ${command.accountSequence}`);
|
|
||||||
throw new ApplicationError('账户未设置恢复助记词');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用 blockchain-service 验证助记词哈希
|
|
||||||
this.logger.log(`Verifying mnemonic hash for account ${command.accountSequence}`);
|
|
||||||
const verifyResult = await this.blockchainClient.verifyMnemonicHash({
|
|
||||||
mnemonic: command.mnemonic,
|
mnemonic: command.mnemonic,
|
||||||
expectedHash: recoveryMnemonic.mnemonicHash,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!verifyResult.valid) {
|
if (!verifyResult.valid) {
|
||||||
this.logger.warn(`Mnemonic hash mismatch for account ${command.accountSequence}`);
|
this.logger.warn(`Mnemonic verification failed for account ${command.accountSequence}: ${verifyResult.message}`);
|
||||||
throw new ApplicationError('助记词错误');
|
throw new ApplicationError(verifyResult.message || '助记词错误');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`Mnemonic verified successfully for account ${command.accountSequence}`);
|
this.logger.log(`Mnemonic verified successfully for account ${command.accountSequence}`);
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,9 @@ export interface VerifyMnemonicResult {
|
||||||
mismatchedAddresses: string[];
|
mismatchedAddresses: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VerifyMnemonicHashParams {
|
export interface VerifyMnemonicByAccountParams {
|
||||||
|
accountSequence: number;
|
||||||
mnemonic: string;
|
mnemonic: string;
|
||||||
expectedHash: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VerifyMnemonicHashResult {
|
export interface VerifyMnemonicHashResult {
|
||||||
|
|
@ -87,18 +87,18 @@ export class BlockchainClientService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证助记词哈希是否匹配(用于账户恢复)
|
* 通过账户序列号验证助记词(用于账户恢复)
|
||||||
*/
|
*/
|
||||||
async verifyMnemonicHash(params: VerifyMnemonicHashParams): Promise<VerifyMnemonicHashResult> {
|
async verifyMnemonicByAccount(params: VerifyMnemonicByAccountParams): Promise<VerifyMnemonicHashResult> {
|
||||||
this.logger.log(`Verifying mnemonic hash`);
|
this.logger.log(`Verifying mnemonic for account ${params.accountSequence}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await firstValueFrom(
|
const response = await firstValueFrom(
|
||||||
this.httpService.post<VerifyMnemonicHashResult>(
|
this.httpService.post<VerifyMnemonicHashResult>(
|
||||||
`${this.blockchainServiceUrl}/internal/verify-mnemonic-hash`,
|
`${this.blockchainServiceUrl}/internal/verify-mnemonic-hash`,
|
||||||
{
|
{
|
||||||
|
accountSequence: params.accountSequence,
|
||||||
mnemonic: params.mnemonic,
|
mnemonic: params.mnemonic,
|
||||||
expectedHash: params.expectedHash,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|
@ -107,10 +107,10 @@ export class BlockchainClientService {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`Mnemonic hash verification result: valid=${response.data.valid}`);
|
this.logger.log(`Mnemonic verification result: valid=${response.data.valid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Failed to verify mnemonic hash', error);
|
this.logger.error('Failed to verify mnemonic', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue