fix(mnemonic): use hash verification instead of address derivation for account recovery
The mnemonic recovery now uses stored hash comparison via blockchain-service instead of trying to derive addresses from mnemonic (which never matched because MPC wallet addresses are not derived from mnemonics). 🤖 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
1a5bafec1a
commit
0311ecf498
|
|
@ -1,8 +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 } from '@/infrastructure/blockchain';
|
import { MnemonicDerivationAdapter, RecoveryMnemonicAdapter } from '@/infrastructure/blockchain';
|
||||||
import { DeriveAddressDto, VerifyMnemonicDto } from '../dto/request';
|
import { DeriveAddressDto, VerifyMnemonicDto, VerifyMnemonicHashDto } from '../dto/request';
|
||||||
import { DeriveAddressResponseDto } from '../dto/response';
|
import { DeriveAddressResponseDto } from '../dto/response';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -15,6 +15,7 @@ export class InternalController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly addressDerivationService: AddressDerivationService,
|
private readonly addressDerivationService: AddressDerivationService,
|
||||||
private readonly mnemonicDerivation: MnemonicDerivationAdapter,
|
private readonly mnemonicDerivation: MnemonicDerivationAdapter,
|
||||||
|
private readonly recoveryMnemonic: RecoveryMnemonicAdapter,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('derive-address')
|
@Post('derive-address')
|
||||||
|
|
@ -74,4 +75,15 @@ export class InternalController {
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('verify-mnemonic-hash')
|
||||||
|
@ApiOperation({ summary: '验证助记词哈希是否匹配' })
|
||||||
|
@ApiResponse({ status: 200, description: '验证结果' })
|
||||||
|
async verifyMnemonicHash(@Body() dto: VerifyMnemonicHashDto) {
|
||||||
|
const result = this.recoveryMnemonic.verifyMnemonic(dto.mnemonic, dto.expectedHash);
|
||||||
|
return {
|
||||||
|
valid: result.valid,
|
||||||
|
message: result.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './query-balance.dto';
|
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';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { IsString } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class VerifyMnemonicHashDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '助记词 (12个单词,空格分隔)',
|
||||||
|
example: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
mnemonic: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '期望的助记词哈希',
|
||||||
|
example: 'a1b2c3d4e5f6...',
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
expectedHash: string;
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||||
import { RecoverByMnemonicCommand } from './recover-by-mnemonic.command';
|
import { RecoverByMnemonicCommand } from './recover-by-mnemonic.command';
|
||||||
import { UserAccountRepository, USER_ACCOUNT_REPOSITORY } from '@/domain/repositories/user-account.repository.interface';
|
import { UserAccountRepository, USER_ACCOUNT_REPOSITORY } from '@/domain/repositories/user-account.repository.interface';
|
||||||
import { AccountSequence, ChainType } 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';
|
||||||
|
|
@ -15,9 +16,10 @@ export class RecoverByMnemonicHandler {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(USER_ACCOUNT_REPOSITORY)
|
@Inject(USER_ACCOUNT_REPOSITORY)
|
||||||
private readonly userRepository: UserAccountRepository,
|
private readonly userRepository: UserAccountRepository,
|
||||||
private readonly blockchainClient: BlockchainClientService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly tokenService: TokenService,
|
private readonly tokenService: TokenService,
|
||||||
private readonly eventPublisher: EventPublisherService,
|
private readonly eventPublisher: EventPublisherService,
|
||||||
|
private readonly blockchainClient: BlockchainClientService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(command: RecoverByMnemonicCommand): Promise<RecoverAccountResult> {
|
async execute(command: RecoverByMnemonicCommand): Promise<RecoverAccountResult> {
|
||||||
|
|
@ -26,26 +28,28 @@ 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 表获取存储的助记词哈希
|
||||||
const expectedAddresses: Array<{ chainType: string; address: string }> = [];
|
const recoveryMnemonic = await this.prisma.recoveryMnemonic.findFirst({
|
||||||
const kavaWallet = account.getWalletAddress(ChainType.KAVA);
|
where: {
|
||||||
if (kavaWallet) {
|
userId: account.userId.value,
|
||||||
expectedAddresses.push({ chainType: 'KAVA', address: kavaWallet.address });
|
status: 'ACTIVE',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!recoveryMnemonic) {
|
||||||
|
this.logger.error(`No recovery mnemonic found for account ${command.accountSequence}`);
|
||||||
|
throw new ApplicationError('账户未设置恢复助记词');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expectedAddresses.length === 0) {
|
// 调用 blockchain-service 验证助记词哈希
|
||||||
throw new ApplicationError('账户没有关联的钱包地址');
|
this.logger.log(`Verifying mnemonic hash for account ${command.accountSequence}`);
|
||||||
}
|
const verifyResult = await this.blockchainClient.verifyMnemonicHash({
|
||||||
|
|
||||||
// 调用 blockchain-service 验证助记词
|
|
||||||
this.logger.log(`Verifying mnemonic for account ${command.accountSequence}`);
|
|
||||||
const verifyResult = await this.blockchainClient.verifyMnemonic({
|
|
||||||
mnemonic: command.mnemonic,
|
mnemonic: command.mnemonic,
|
||||||
expectedAddresses,
|
expectedHash: recoveryMnemonic.mnemonicHash,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!verifyResult.valid) {
|
if (!verifyResult.valid) {
|
||||||
this.logger.warn(`Mnemonic verification failed for account ${command.accountSequence}`);
|
this.logger.warn(`Mnemonic hash mismatch for account ${command.accountSequence}`);
|
||||||
throw new ApplicationError('助记词错误');
|
throw new ApplicationError('助记词错误');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,16 @@ export interface VerifyMnemonicResult {
|
||||||
mismatchedAddresses: string[];
|
mismatchedAddresses: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VerifyMnemonicHashParams {
|
||||||
|
mnemonic: string;
|
||||||
|
expectedHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifyMnemonicHashResult {
|
||||||
|
valid: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DerivedAddress {
|
export interface DerivedAddress {
|
||||||
chainType: string;
|
chainType: string;
|
||||||
address: string;
|
address: string;
|
||||||
|
|
@ -76,6 +86,35 @@ export class BlockchainClientService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证助记词哈希是否匹配(用于账户恢复)
|
||||||
|
*/
|
||||||
|
async verifyMnemonicHash(params: VerifyMnemonicHashParams): Promise<VerifyMnemonicHashResult> {
|
||||||
|
this.logger.log(`Verifying mnemonic hash`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.post<VerifyMnemonicHashResult>(
|
||||||
|
`${this.blockchainServiceUrl}/internal/verify-mnemonic-hash`,
|
||||||
|
{
|
||||||
|
mnemonic: params.mnemonic,
|
||||||
|
expectedHash: params.expectedHash,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
timeout: 30000,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log(`Mnemonic hash verification result: valid=${response.data.valid}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to verify mnemonic hash', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从助记词派生所有链的钱包地址
|
* 从助记词派生所有链的钱包地址
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue