feat(mnemonic): propagate accountSequence through MPC keygen flow (DDD)
Changes across all three services to properly associate recovery mnemonics with account sequence numbers instead of user IDs, following DDD principles: identity-service: - Add accountSequence to MpcKeygenRequestedEvent payload - Pass accountSequence when publishing keygen request - Remove direct access to recoveryMnemonic table (now in blockchain-service) - Call blockchain-service for mnemonic backup marking - BlockchainWalletHandler no longer saves mnemonic (stored in blockchain-service) mpc-service: - Add accountSequence to KeygenRequestedPayload interface - Pass accountSequence through to blockchain-service when deriving addresses - Include accountSequence in KeygenCompleted event extraPayload blockchain-service: - Add accountSequence to derive-address API and internal interfaces - Add accountSequence to KeygenCompletedPayload extraPayload - Add PUT /internal/mnemonic/backup API for marking mnemonic as backed up - Store recovery mnemonic with accountSequence association 🤖 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
e95dc4ca57
commit
1bfbaa06f1
|
|
@ -1,9 +1,9 @@
|
||||||
import { Controller, Post, Body, Get, Param } from '@nestjs/common';
|
import { Controller, Post, Body, Get, Param, Put } 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 { MnemonicVerificationService } from '@/application/services/mnemonic-verification.service';
|
import { MnemonicVerificationService } from '@/application/services/mnemonic-verification.service';
|
||||||
import { MnemonicDerivationAdapter } from '@/infrastructure/blockchain';
|
import { MnemonicDerivationAdapter } from '@/infrastructure/blockchain';
|
||||||
import { DeriveAddressDto, VerifyMnemonicDto, VerifyMnemonicHashDto } from '../dto/request';
|
import { DeriveAddressDto, VerifyMnemonicDto, VerifyMnemonicHashDto, MarkMnemonicBackupDto } from '../dto/request';
|
||||||
import { DeriveAddressResponseDto } from '../dto/response';
|
import { DeriveAddressResponseDto } from '../dto/response';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -23,10 +23,11 @@ export class InternalController {
|
||||||
@ApiOperation({ summary: '从公钥派生地址' })
|
@ApiOperation({ summary: '从公钥派生地址' })
|
||||||
@ApiResponse({ status: 201, description: '派生成功', type: DeriveAddressResponseDto })
|
@ApiResponse({ status: 201, description: '派生成功', type: DeriveAddressResponseDto })
|
||||||
async deriveAddress(@Body() dto: DeriveAddressDto): Promise<DeriveAddressResponseDto> {
|
async deriveAddress(@Body() dto: DeriveAddressDto): Promise<DeriveAddressResponseDto> {
|
||||||
const result = await this.addressDerivationService.deriveAndRegister(
|
const result = await this.addressDerivationService.deriveAndRegister({
|
||||||
BigInt(dto.userId),
|
userId: BigInt(dto.userId),
|
||||||
dto.publicKey,
|
accountSequence: dto.accountSequence,
|
||||||
);
|
publicKey: dto.publicKey,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userId: result.userId.toString(),
|
userId: result.userId.toString(),
|
||||||
|
|
@ -90,4 +91,15 @@ export class InternalController {
|
||||||
message: result.message,
|
message: result.message,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Put('mnemonic/backup')
|
||||||
|
@ApiOperation({ summary: '标记助记词已备份' })
|
||||||
|
@ApiResponse({ status: 200, description: '标记成功' })
|
||||||
|
async markMnemonicBackedUp(@Body() dto: MarkMnemonicBackupDto) {
|
||||||
|
await this.mnemonicVerification.markAsBackedUp(dto.accountSequence);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Mnemonic marked as backed up',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { IsString, IsNumberString } from 'class-validator';
|
import { IsString, IsNumberString, IsInt } from 'class-validator';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class DeriveAddressDto {
|
export class DeriveAddressDto {
|
||||||
|
|
@ -6,6 +6,10 @@ export class DeriveAddressDto {
|
||||||
@IsNumberString()
|
@IsNumberString()
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '账户序列号 (8位数字)', example: 10000001 })
|
||||||
|
@IsInt()
|
||||||
|
accountSequence: number;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: '压缩公钥 (33 bytes, 0x02/0x03 开头)',
|
description: '压缩公钥 (33 bytes, 0x02/0x03 开头)',
|
||||||
example: '0x02abc123...',
|
example: '0x02abc123...',
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@ export * from './query-balance.dto';
|
||||||
export * from './derive-address.dto';
|
export * from './derive-address.dto';
|
||||||
export * from './verify-mnemonic.dto';
|
export * from './verify-mnemonic.dto';
|
||||||
export * from './verify-mnemonic-hash.dto';
|
export * from './verify-mnemonic-hash.dto';
|
||||||
|
export * from './mark-mnemonic-backup.dto';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { IsInt } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class MarkMnemonicBackupDto {
|
||||||
|
@ApiProperty({ description: '账户序列号 (8位数字)', example: 10000001 })
|
||||||
|
@IsInt()
|
||||||
|
accountSequence: number;
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ export class MpcKeygenCompletedHandler implements OnModuleInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理 MPC 密钥生成完成事件
|
* 处理 MPC 密钥生成完成事件
|
||||||
* 从 mpc-service 的 KeygenCompleted 事件中提取 publicKey 和 userId
|
* 从 mpc-service 的 KeygenCompleted 事件中提取 publicKey、userId 和 accountSequence
|
||||||
*/
|
*/
|
||||||
private async handleKeygenCompleted(payload: KeygenCompletedPayload): Promise<void> {
|
private async handleKeygenCompleted(payload: KeygenCompletedPayload): Promise<void> {
|
||||||
this.logger.log(`[HANDLE] Received KeygenCompleted event`);
|
this.logger.log(`[HANDLE] Received KeygenCompleted event`);
|
||||||
|
|
@ -33,13 +33,20 @@ export class MpcKeygenCompletedHandler implements OnModuleInit {
|
||||||
this.logger.log(`[HANDLE] publicKey: ${payload.publicKey?.substring(0, 30)}...`);
|
this.logger.log(`[HANDLE] publicKey: ${payload.publicKey?.substring(0, 30)}...`);
|
||||||
this.logger.log(`[HANDLE] extraPayload: ${JSON.stringify(payload.extraPayload)}`);
|
this.logger.log(`[HANDLE] extraPayload: ${JSON.stringify(payload.extraPayload)}`);
|
||||||
|
|
||||||
// Extract userId from extraPayload
|
// Extract userId and accountSequence from extraPayload
|
||||||
const userId = payload.extraPayload?.userId;
|
const userId = payload.extraPayload?.userId;
|
||||||
|
const accountSequence = payload.extraPayload?.accountSequence;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
this.logger.error(`[ERROR] Missing userId in extraPayload, cannot derive addresses`);
|
this.logger.error(`[ERROR] Missing userId in extraPayload, cannot derive addresses`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!accountSequence) {
|
||||||
|
this.logger.error(`[ERROR] Missing accountSequence in extraPayload, cannot derive addresses`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const publicKey = payload.publicKey;
|
const publicKey = payload.publicKey;
|
||||||
if (!publicKey) {
|
if (!publicKey) {
|
||||||
this.logger.error(`[ERROR] Missing publicKey in payload, cannot derive addresses`);
|
this.logger.error(`[ERROR] Missing publicKey in payload, cannot derive addresses`);
|
||||||
|
|
@ -47,19 +54,20 @@ export class MpcKeygenCompletedHandler implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.logger.log(`[DERIVE] Starting address derivation for user: ${userId}`);
|
this.logger.log(`[DERIVE] Starting address derivation for user: ${userId}, account: ${accountSequence}`);
|
||||||
|
|
||||||
const result = await this.addressDerivationService.deriveAndRegister(
|
const result = await this.addressDerivationService.deriveAndRegister({
|
||||||
BigInt(userId),
|
userId: BigInt(userId),
|
||||||
|
accountSequence: Number(accountSequence),
|
||||||
publicKey,
|
publicKey,
|
||||||
);
|
});
|
||||||
|
|
||||||
this.logger.log(`[DERIVE] Successfully derived ${result.addresses.length} addresses for user ${userId}`);
|
this.logger.log(`[DERIVE] Successfully derived ${result.addresses.length} addresses for account ${accountSequence}`);
|
||||||
result.addresses.forEach((addr) => {
|
result.addresses.forEach((addr) => {
|
||||||
this.logger.log(`[DERIVE] - ${addr.chainType}: ${addr.address}`);
|
this.logger.log(`[DERIVE] - ${addr.chainType}: ${addr.address}`);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`[ERROR] Failed to derive addresses for user ${userId}:`, error);
|
this.logger.error(`[ERROR] Failed to derive addresses for account ${accountSequence}:`, error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
import { RecoveryMnemonicAdapter } from '@/infrastructure/blockchain/recovery-mnemonic.adapter';
|
import { RecoveryMnemonicAdapter } from '@/infrastructure/blockchain/recovery-mnemonic.adapter';
|
||||||
import { AddressCacheService } from '@/infrastructure/redis/address-cache.service';
|
import { AddressCacheService } from '@/infrastructure/redis/address-cache.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 {
|
import {
|
||||||
MONITORED_ADDRESS_REPOSITORY,
|
MONITORED_ADDRESS_REPOSITORY,
|
||||||
IMonitoredAddressRepository,
|
IMonitoredAddressRepository,
|
||||||
|
|
@ -15,8 +16,15 @@ import { WalletAddressCreatedEvent } from '@/domain/events';
|
||||||
import { ChainType, EvmAddress } from '@/domain/value-objects';
|
import { ChainType, EvmAddress } from '@/domain/value-objects';
|
||||||
import { ChainTypeEnum } from '@/domain/enums';
|
import { ChainTypeEnum } from '@/domain/enums';
|
||||||
|
|
||||||
|
export interface DeriveAddressParams {
|
||||||
|
userId: bigint;
|
||||||
|
accountSequence: number;
|
||||||
|
publicKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DeriveAddressResult {
|
export interface DeriveAddressResult {
|
||||||
userId: bigint;
|
userId: bigint;
|
||||||
|
accountSequence: number;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
addresses: DerivedAddress[];
|
addresses: DerivedAddress[];
|
||||||
}
|
}
|
||||||
|
|
@ -46,6 +54,7 @@ export class AddressDerivationService {
|
||||||
private readonly recoveryMnemonic: RecoveryMnemonicAdapter,
|
private readonly recoveryMnemonic: RecoveryMnemonicAdapter,
|
||||||
private readonly addressCache: AddressCacheService,
|
private readonly addressCache: AddressCacheService,
|
||||||
private readonly eventPublisher: EventPublisherService,
|
private readonly eventPublisher: EventPublisherService,
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
@Inject(MONITORED_ADDRESS_REPOSITORY)
|
@Inject(MONITORED_ADDRESS_REPOSITORY)
|
||||||
private readonly monitoredAddressRepo: IMonitoredAddressRepository,
|
private readonly monitoredAddressRepo: IMonitoredAddressRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
@ -53,8 +62,9 @@ export class AddressDerivationService {
|
||||||
/**
|
/**
|
||||||
* 从公钥派生地址并注册监控
|
* 从公钥派生地址并注册监控
|
||||||
*/
|
*/
|
||||||
async deriveAndRegister(userId: bigint, publicKey: string): Promise<DeriveAddressResult> {
|
async deriveAndRegister(params: DeriveAddressParams): Promise<DeriveAddressResult> {
|
||||||
this.logger.log(`[DERIVE] Starting address derivation for user ${userId}`);
|
const { userId, accountSequence, publicKey } = params;
|
||||||
|
this.logger.log(`[DERIVE] Starting address derivation for user ${userId}, account ${accountSequence}`);
|
||||||
this.logger.log(`[DERIVE] Public key: ${publicKey.substring(0, 30)}...`);
|
this.logger.log(`[DERIVE] Public key: ${publicKey.substring(0, 30)}...`);
|
||||||
|
|
||||||
// 1. 派生所有链的地址 (包括 Cosmos 和 EVM)
|
// 1. 派生所有链的地址 (包括 Cosmos 和 EVM)
|
||||||
|
|
@ -70,35 +80,50 @@ export class AddressDerivationService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 生成恢复助记词 (与钱包公钥关联)
|
// 3. 生成恢复助记词 (与账户序列号关联)
|
||||||
this.logger.log(`[MNEMONIC] Generating recovery mnemonic for user ${userId}`);
|
this.logger.log(`[MNEMONIC] Generating recovery mnemonic for account ${accountSequence}`);
|
||||||
const mnemonicResult = this.recoveryMnemonic.generateMnemonic({
|
const mnemonicResult = this.recoveryMnemonic.generateMnemonic({
|
||||||
userId: userId.toString(),
|
userId: userId.toString(),
|
||||||
publicKey,
|
publicKey,
|
||||||
});
|
});
|
||||||
this.logger.log(`[MNEMONIC] Recovery mnemonic generated, hash: ${mnemonicResult.mnemonicHash.slice(0, 16)}...`);
|
this.logger.log(`[MNEMONIC] Recovery mnemonic generated, hash: ${mnemonicResult.mnemonicHash.slice(0, 16)}...`);
|
||||||
|
|
||||||
// 4. 发布钱包地址创建事件 (包含所有链的地址和助记词)
|
// 4. 存储恢复助记词到 blockchain-service 数据库 (使用 accountSequence 关联)
|
||||||
|
await this.prisma.recoveryMnemonic.create({
|
||||||
|
data: {
|
||||||
|
accountSequence,
|
||||||
|
publicKey,
|
||||||
|
encryptedMnemonic: mnemonicResult.encryptedMnemonic,
|
||||||
|
mnemonicHash: mnemonicResult.mnemonicHash,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
isBackedUp: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.log(`[MNEMONIC] Recovery mnemonic saved for account ${accountSequence}`);
|
||||||
|
|
||||||
|
// 5. 发布钱包地址创建事件 (包含所有链的地址和助记词)
|
||||||
const event = new WalletAddressCreatedEvent({
|
const event = new WalletAddressCreatedEvent({
|
||||||
userId: userId.toString(),
|
userId: userId.toString(),
|
||||||
|
accountSequence,
|
||||||
publicKey,
|
publicKey,
|
||||||
addresses: derivedAddresses.map((a) => ({
|
addresses: derivedAddresses.map((a) => ({
|
||||||
chainType: a.chainType,
|
chainType: a.chainType,
|
||||||
address: a.address,
|
address: a.address,
|
||||||
})),
|
})),
|
||||||
// 恢复助记词
|
// 恢复助记词 (明文仅在事件中传递给客户端首次显示)
|
||||||
mnemonic: mnemonicResult.mnemonic,
|
mnemonic: mnemonicResult.mnemonic,
|
||||||
encryptedMnemonic: mnemonicResult.encryptedMnemonic,
|
encryptedMnemonic: mnemonicResult.encryptedMnemonic,
|
||||||
mnemonicHash: mnemonicResult.mnemonicHash,
|
mnemonicHash: mnemonicResult.mnemonicHash,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`[PUBLISH] Publishing WalletAddressCreated event for user ${userId}`);
|
this.logger.log(`[PUBLISH] Publishing WalletAddressCreated event for account ${accountSequence}`);
|
||||||
this.logger.log(`[PUBLISH] Addresses: ${JSON.stringify(derivedAddresses)}`);
|
this.logger.log(`[PUBLISH] Addresses: ${JSON.stringify(derivedAddresses)}`);
|
||||||
await this.eventPublisher.publish(event);
|
await this.eventPublisher.publish(event);
|
||||||
this.logger.log(`[PUBLISH] WalletAddressCreated event published successfully`);
|
this.logger.log(`[PUBLISH] WalletAddressCreated event published successfully`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userId,
|
userId,
|
||||||
|
accountSequence,
|
||||||
publicKey,
|
publicKey,
|
||||||
addresses: derivedAddresses,
|
addresses: derivedAddresses,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { DomainEvent } from './domain-event.base';
|
||||||
|
|
||||||
export interface WalletAddressCreatedPayload {
|
export interface WalletAddressCreatedPayload {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
accountSequence: number; // 8位账户序列号
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
addresses: {
|
addresses: {
|
||||||
chainType: string;
|
chainType: string;
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export interface KeygenCompletedPayload {
|
||||||
threshold: string;
|
threshold: string;
|
||||||
extraPayload?: {
|
extraPayload?: {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
accountSequence: number; // 8位账户序列号,用于关联恢复助记词
|
||||||
username: string;
|
username: string;
|
||||||
delegateShare?: {
|
delegateShare?: {
|
||||||
partyId: string;
|
partyId: string;
|
||||||
|
|
|
||||||
|
|
@ -103,10 +103,10 @@ export class BlockchainWalletHandler implements OnModuleInit {
|
||||||
await this.userRepository.saveWallets(account.userId, wallets);
|
await this.userRepository.saveWallets(account.userId, wallets);
|
||||||
this.logger.log(`[WALLET] Saved ${wallets.length} wallet addresses for user: ${userId}`);
|
this.logger.log(`[WALLET] Saved ${wallets.length} wallet addresses for user: ${userId}`);
|
||||||
|
|
||||||
// 4. Save recovery mnemonic if provided
|
// 4. Recovery mnemonic is now stored in blockchain-service (DDD: domain separation)
|
||||||
if (mnemonic && encryptedMnemonic && mnemonicHash && publicKey) {
|
// Note: blockchain-service stores mnemonic with accountSequence association
|
||||||
await this.saveRecoveryMnemonic(BigInt(userId), publicKey, encryptedMnemonic, mnemonicHash);
|
if (mnemonic) {
|
||||||
this.logger.log(`[MNEMONIC] Saved recovery mnemonic for user: ${userId}`);
|
this.logger.log(`[MNEMONIC] Recovery mnemonic received for user: ${userId} (stored in blockchain-service)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Update Redis status to completed (include mnemonic for first-time retrieval)
|
// 5. Update Redis status to completed (include mnemonic for first-time retrieval)
|
||||||
|
|
@ -154,38 +154,4 @@ export class BlockchainWalletHandler implements OnModuleInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Save recovery mnemonic to database
|
|
||||||
*/
|
|
||||||
private async saveRecoveryMnemonic(
|
|
||||||
userId: bigint,
|
|
||||||
publicKey: string,
|
|
||||||
encryptedMnemonic: string,
|
|
||||||
mnemonicHash: string,
|
|
||||||
): Promise<void> {
|
|
||||||
// Check if mnemonic already exists for this user
|
|
||||||
const existing = await this.prisma.recoveryMnemonic.findFirst({
|
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
status: 'ACTIVE',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
this.logger.log(`[MNEMONIC] Active mnemonic already exists for user: ${userId}, skipping`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new recovery mnemonic record
|
|
||||||
await this.prisma.recoveryMnemonic.create({
|
|
||||||
data: {
|
|
||||||
userId,
|
|
||||||
publicKey,
|
|
||||||
encryptedMnemonic,
|
|
||||||
mnemonicHash,
|
|
||||||
status: 'ACTIVE',
|
|
||||||
isBackedUp: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ export class UserApplicationService {
|
||||||
await this.eventPublisher.publish(new MpcKeygenRequestedEvent({
|
await this.eventPublisher.publish(new MpcKeygenRequestedEvent({
|
||||||
sessionId,
|
sessionId,
|
||||||
userId: account.userId.toString(),
|
userId: account.userId.toString(),
|
||||||
|
accountSequence: account.accountSequence.value, // 8位账户序列号,用于关联恢复助记词
|
||||||
username: `user_${account.accountSequence.value}`, // 用于 mpc-system 标识
|
username: `user_${account.accountSequence.value}`, // 用于 mpc-system 标识
|
||||||
threshold: 2,
|
threshold: 2,
|
||||||
totalParties: 3,
|
totalParties: 3,
|
||||||
|
|
@ -688,11 +689,11 @@ export class UserApplicationService {
|
||||||
/**
|
/**
|
||||||
* 获取用户的恢复助记词
|
* 获取用户的恢复助记词
|
||||||
*
|
*
|
||||||
* 优先从 Redis 获取(首次生成时临时存储的明文)
|
* 只从 Redis 获取(首次生成时临时存储的明文)
|
||||||
* 如果 Redis 没有,从数据库获取(仅当用户未备份时返回)
|
* DDD: 助记词数据存储在 blockchain-service,identity-service 不直接访问
|
||||||
*/
|
*/
|
||||||
private async getRecoveryMnemonic(userId: bigint): Promise<string | null> {
|
private async getRecoveryMnemonic(userId: bigint): Promise<string | null> {
|
||||||
// 1. 先从 Redis 获取首次生成的助记词
|
// 从 Redis 获取首次生成的助记词
|
||||||
const redisKey = `keygen:status:${userId}`;
|
const redisKey = `keygen:status:${userId}`;
|
||||||
const statusData = await this.redisService.get(redisKey);
|
const statusData = await this.redisService.get(redisKey);
|
||||||
|
|
||||||
|
|
@ -704,27 +705,13 @@ export class UserApplicationService {
|
||||||
return parsed.mnemonic;
|
return parsed.mnemonic;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 解析失败,继续从数据库获取
|
// 解析失败
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 从数据库获取(仅当用户未备份时返回)
|
// Redis 中没有助记词,可能已经备份或过期
|
||||||
const recoveryMnemonic = await this.prisma.recoveryMnemonic.findFirst({
|
// DDD: 不再直接从 identity-service 数据库获取,助记词数据在 blockchain-service
|
||||||
where: {
|
this.logger.log(`[MNEMONIC] No mnemonic in Redis for user: ${userId} (may be backed up or expired)`);
|
||||||
userId,
|
|
||||||
status: 'ACTIVE',
|
|
||||||
isBackedUp: false, // 只有未备份时才返回
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!recoveryMnemonic) {
|
|
||||||
this.logger.log(`[MNEMONIC] No active unbackuped mnemonic for user: ${userId}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回空字符串,因为加密的助记词需要解密才能返回
|
|
||||||
// 实际应用中应该解密后返回,但这里为了安全,只在首次(Redis 中有时)返回
|
|
||||||
this.logger.log(`[MNEMONIC] Found encrypted mnemonic in DB for user: ${userId}, but not returning decrypted value`);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -734,33 +721,30 @@ export class UserApplicationService {
|
||||||
* 标记助记词已备份 (PUT /user/mnemonic/backup)
|
* 标记助记词已备份 (PUT /user/mnemonic/backup)
|
||||||
*
|
*
|
||||||
* 用户确认已备份助记词后调用此接口:
|
* 用户确认已备份助记词后调用此接口:
|
||||||
* 1. 更新数据库 isBackedUp = true
|
* 1. 调用 blockchain-service 更新 isBackedUp = true (DDD: domain separation)
|
||||||
* 2. 清除 Redis 中的明文助记词
|
* 2. 清除 Redis 中的明文助记词
|
||||||
*/
|
*/
|
||||||
async markMnemonicBackedUp(command: MarkMnemonicBackedUpCommand): Promise<void> {
|
async markMnemonicBackedUp(command: MarkMnemonicBackedUpCommand): Promise<void> {
|
||||||
const userId = BigInt(command.userId);
|
const userId = BigInt(command.userId);
|
||||||
this.logger.log(`[BACKUP] Marking mnemonic as backed up for user: ${userId}`);
|
this.logger.log(`[BACKUP] Marking mnemonic as backed up for user: ${userId}`);
|
||||||
|
|
||||||
// 1. 更新数据库
|
// 1. 获取用户的 accountSequence
|
||||||
const result = await this.prisma.recoveryMnemonic.updateMany({
|
const account = await this.userRepository.findById(UserId.create(command.userId));
|
||||||
where: {
|
if (!account) {
|
||||||
userId,
|
this.logger.warn(`[BACKUP] User not found: ${userId}`);
|
||||||
status: 'ACTIVE',
|
return;
|
||||||
isBackedUp: false,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
isBackedUp: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.count === 0) {
|
|
||||||
this.logger.warn(`[BACKUP] No active unbackuped mnemonic found for user: ${userId}`);
|
|
||||||
// 不抛出错误,可能已经备份过了
|
|
||||||
} else {
|
|
||||||
this.logger.log(`[BACKUP] Mnemonic marked as backed up for user: ${userId}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 清除 Redis 中的明文助记词(更新状态,移除 mnemonic 字段)
|
// 2. 调用 blockchain-service 标记助记词已备份 (DDD: domain separation)
|
||||||
|
try {
|
||||||
|
await this.blockchainClient.markMnemonicBackedUp(account.accountSequence.value);
|
||||||
|
this.logger.log(`[BACKUP] Mnemonic marked as backed up in blockchain-service for account: ${account.accountSequence.value}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[BACKUP] Failed to mark mnemonic as backed up in blockchain-service`, error);
|
||||||
|
// 不阻塞,继续清除 Redis
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 清除 Redis 中的明文助记词(更新状态,移除 mnemonic 字段)
|
||||||
const redisKey = `keygen:status:${userId}`;
|
const redisKey = `keygen:status:${userId}`;
|
||||||
const statusData = await this.redisService.get(redisKey);
|
const statusData = await this.redisService.get(redisKey);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,7 @@ export class UserAccountDeactivatedEvent extends DomainEvent {
|
||||||
* payload 格式需要与 mpc-service 的 KeygenRequestedPayload 匹配:
|
* payload 格式需要与 mpc-service 的 KeygenRequestedPayload 匹配:
|
||||||
* - sessionId: 唯一会话ID
|
* - sessionId: 唯一会话ID
|
||||||
* - userId: 用户ID
|
* - userId: 用户ID
|
||||||
|
* - accountSequence: 8位账户序列号 (用于关联恢复助记词)
|
||||||
* - username: 用户名 (用于 mpc-system 标识)
|
* - username: 用户名 (用于 mpc-system 标识)
|
||||||
* - threshold: 签名阈值 (默认 2)
|
* - threshold: 签名阈值 (默认 2)
|
||||||
* - totalParties: 总参与方数 (默认 3)
|
* - totalParties: 总参与方数 (默认 3)
|
||||||
|
|
@ -176,6 +177,7 @@ export class MpcKeygenRequestedEvent extends DomainEvent {
|
||||||
public readonly payload: {
|
public readonly payload: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
accountSequence: number;
|
||||||
username: string;
|
username: string;
|
||||||
threshold: number;
|
threshold: number;
|
||||||
totalParties: number;
|
totalParties: number;
|
||||||
|
|
|
||||||
|
|
@ -140,4 +140,29 @@ export class BlockchainClientService {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记助记词已备份
|
||||||
|
*/
|
||||||
|
async markMnemonicBackedUp(accountSequence: number): Promise<void> {
|
||||||
|
this.logger.log(`Marking mnemonic as backed up for account ${accountSequence}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await firstValueFrom(
|
||||||
|
this.httpService.put(
|
||||||
|
`${this.blockchainServiceUrl}/internal/mnemonic/backup`,
|
||||||
|
{ accountSequence },
|
||||||
|
{
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
timeout: 30000,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log(`Mnemonic marked as backed up for account ${accountSequence}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to mark mnemonic as backed up', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export interface KeygenCompletedPayload {
|
||||||
threshold: string;
|
threshold: string;
|
||||||
extraPayload?: {
|
extraPayload?: {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
accountSequence: number; // 8位账户序列号
|
||||||
username: string;
|
username: string;
|
||||||
delegateShare?: {
|
delegateShare?: {
|
||||||
partyId: string;
|
partyId: string;
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ export class KeygenRequestedHandler implements OnModuleInit {
|
||||||
this.logger.log(`[HANDLE] Payload: ${JSON.stringify(payload)}`);
|
this.logger.log(`[HANDLE] Payload: ${JSON.stringify(payload)}`);
|
||||||
|
|
||||||
const data = payload as unknown as KeygenRequestedPayload;
|
const data = payload as unknown as KeygenRequestedPayload;
|
||||||
const { sessionId, userId, username, threshold, totalParties, requireDelegate } = data;
|
const { sessionId, userId, accountSequence, username, threshold, totalParties, requireDelegate } = data;
|
||||||
|
|
||||||
this.logger.log(`[HANDLE] Parsed request: sessionId=${sessionId}`);
|
this.logger.log(`[HANDLE] Parsed request: sessionId=${sessionId}`);
|
||||||
this.logger.log(`[HANDLE] userId=${userId}, username=${username}`);
|
this.logger.log(`[HANDLE] userId=${userId}, username=${username}`);
|
||||||
|
|
@ -81,6 +81,7 @@ export class KeygenRequestedHandler implements OnModuleInit {
|
||||||
try {
|
try {
|
||||||
const deriveResult = await this.blockchainClient.deriveAddresses({
|
const deriveResult = await this.blockchainClient.deriveAddresses({
|
||||||
userId,
|
userId,
|
||||||
|
accountSequence, // 8位账户序列号,用于关联恢复助记词
|
||||||
publicKey: result.publicKey,
|
publicKey: result.publicKey,
|
||||||
});
|
});
|
||||||
derivedAddresses = deriveResult.addresses;
|
derivedAddresses = deriveResult.addresses;
|
||||||
|
|
@ -129,6 +130,7 @@ export class KeygenRequestedHandler implements OnModuleInit {
|
||||||
// Add extra payload for identity-service
|
// Add extra payload for identity-service
|
||||||
(completedEvent as any).extraPayload = {
|
(completedEvent as any).extraPayload = {
|
||||||
userId,
|
userId,
|
||||||
|
accountSequence, // 8位账户序列号,用于关联恢复助记词
|
||||||
username,
|
username,
|
||||||
delegateShare: result.delegateShare,
|
delegateShare: result.delegateShare,
|
||||||
derivedAddresses, // BSC, KAVA, DST addresses
|
derivedAddresses, // BSC, KAVA, DST addresses
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
export interface DeriveAddressParams {
|
export interface DeriveAddressParams {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
accountSequence: number; // 8位账户序列号,用于关联恢复助记词
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,6 +55,7 @@ export class BlockchainClientService {
|
||||||
`${this.blockchainServiceUrl}/api/v1/internal/derive-address`,
|
`${this.blockchainServiceUrl}/api/v1/internal/derive-address`,
|
||||||
{
|
{
|
||||||
userId: params.userId,
|
userId: params.userId,
|
||||||
|
accountSequence: params.accountSequence,
|
||||||
publicKey: params.publicKey,
|
publicKey: params.publicKey,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export const MPC_CONSUME_TOPICS = {
|
||||||
export interface KeygenRequestedPayload {
|
export interface KeygenRequestedPayload {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
accountSequence: number; // 8位账户序列号,用于关联恢复助记词
|
||||||
username: string;
|
username: string;
|
||||||
threshold: number;
|
threshold: number;
|
||||||
totalParties: number;
|
totalParties: number;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue