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 { 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 { DeriveAddressDto } from '../dto/request';
|
import { MnemonicDerivationAdapter } from '@/infrastructure/blockchain';
|
||||||
|
import { DeriveAddressDto, VerifyMnemonicDto } from '../dto/request';
|
||||||
import { DeriveAddressResponseDto } from '../dto/response';
|
import { DeriveAddressResponseDto } from '../dto/response';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -11,7 +12,10 @@ import { DeriveAddressResponseDto } from '../dto/response';
|
||||||
@ApiTags('Internal')
|
@ApiTags('Internal')
|
||||||
@Controller('internal')
|
@Controller('internal')
|
||||||
export class InternalController {
|
export class InternalController {
|
||||||
constructor(private readonly addressDerivationService: AddressDerivationService) {}
|
constructor(
|
||||||
|
private readonly addressDerivationService: AddressDerivationService,
|
||||||
|
private readonly mnemonicDerivation: MnemonicDerivationAdapter,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post('derive-address')
|
@Post('derive-address')
|
||||||
@ApiOperation({ summary: '从公钥派生地址' })
|
@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 './query-balance.dto';
|
||||||
export * from './derive-address.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 './evm-provider.adapter';
|
||||||
export * from './address-derivation.adapter';
|
export * from './address-derivation.adapter';
|
||||||
|
export * from './mnemonic-derivation.adapter';
|
||||||
export * from './block-scanner.service';
|
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 { PrismaService } from './persistence/prisma/prisma.service';
|
||||||
import { RedisService, AddressCacheService } from './redis';
|
import { RedisService, AddressCacheService } from './redis';
|
||||||
import { EventPublisherService, MpcEventConsumerService } from './kafka';
|
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 { DomainModule } from '@/domain/domain.module';
|
||||||
import {
|
import {
|
||||||
DEPOSIT_TRANSACTION_REPOSITORY,
|
DEPOSIT_TRANSACTION_REPOSITORY,
|
||||||
|
|
@ -30,6 +30,7 @@ import {
|
||||||
// 区块链适配器
|
// 区块链适配器
|
||||||
EvmProviderAdapter,
|
EvmProviderAdapter,
|
||||||
AddressDerivationAdapter,
|
AddressDerivationAdapter,
|
||||||
|
MnemonicDerivationAdapter,
|
||||||
BlockScannerService,
|
BlockScannerService,
|
||||||
|
|
||||||
// 缓存服务
|
// 缓存服务
|
||||||
|
|
@ -60,6 +61,7 @@ import {
|
||||||
MpcEventConsumerService,
|
MpcEventConsumerService,
|
||||||
EvmProviderAdapter,
|
EvmProviderAdapter,
|
||||||
AddressDerivationAdapter,
|
AddressDerivationAdapter,
|
||||||
|
MnemonicDerivationAdapter,
|
||||||
BlockScannerService,
|
BlockScannerService,
|
||||||
AddressCacheService,
|
AddressCacheService,
|
||||||
DEPOSIT_TRANSACTION_REPOSITORY,
|
DEPOSIT_TRANSACTION_REPOSITORY,
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,10 @@
|
||||||
"Bash(powershell -Command:*)",
|
"Bash(powershell -Command:*)",
|
||||||
"Bash(git pull:*)",
|
"Bash(git pull:*)",
|
||||||
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" add -A)",
|
"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": [],
|
"deny": [],
|
||||||
"ask": []
|
"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 { 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 { WalletGeneratorService } from '@/domain/services';
|
import { AccountSequence, ChainType } from '@/domain/value-objects';
|
||||||
import { AccountSequence, ChainType, Mnemonic } 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 { 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';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RecoverByMnemonicHandler {
|
export class RecoverByMnemonicHandler {
|
||||||
|
private readonly logger = new Logger(RecoverByMnemonicHandler.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(USER_ACCOUNT_REPOSITORY)
|
@Inject(USER_ACCOUNT_REPOSITORY)
|
||||||
private readonly userRepository: UserAccountRepository,
|
private readonly userRepository: UserAccountRepository,
|
||||||
private readonly walletGenerator: WalletGeneratorService,
|
private readonly blockchainClient: BlockchainClientService,
|
||||||
private readonly tokenService: TokenService,
|
private readonly tokenService: TokenService,
|
||||||
private readonly eventPublisher: EventPublisherService,
|
private readonly eventPublisher: EventPublisherService,
|
||||||
) {}
|
) {}
|
||||||
|
|
@ -24,18 +26,31 @@ 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('账户已冻结或注销');
|
||||||
|
|
||||||
const mnemonic = Mnemonic.create(command.mnemonic);
|
// 获取账户的钱包地址用于验证
|
||||||
const wallets = this.walletGenerator.recoverWalletSystem({
|
const expectedAddresses: Array<{ chainType: string; address: string }> = [];
|
||||||
userId: account.userId,
|
const kavaWallet = account.getWalletAddress(ChainType.KAVA);
|
||||||
mnemonic,
|
if (kavaWallet) {
|
||||||
deviceId: command.newDeviceId,
|
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 (!verifyResult.valid) {
|
||||||
if (!kavaWallet || kavaWallet.address !== wallets.get(ChainType.KAVA)!.address) {
|
this.logger.warn(`Mnemonic verification failed for account ${command.accountSequence}`);
|
||||||
throw new ApplicationError('助记词错误');
|
throw new ApplicationError('助记词错误');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Mnemonic verified successfully for account ${command.accountSequence}`);
|
||||||
|
|
||||||
account.addDevice(command.newDeviceId, command.deviceName);
|
account.addDevice(command.newDeviceId, command.deviceName);
|
||||||
account.recordLogin();
|
account.recordLogin();
|
||||||
await this.userRepository.save(account);
|
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 { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate';
|
||||||
import { WalletAddress } from '@/domain/entities/wallet-address.entity';
|
import { WalletAddress } from '@/domain/entities/wallet-address.entity';
|
||||||
import {
|
import {
|
||||||
AccountSequenceGeneratorService, UserValidatorService, WalletGeneratorService,
|
AccountSequenceGeneratorService, UserValidatorService,
|
||||||
} from '@/domain/services';
|
} from '@/domain/services';
|
||||||
import {
|
import {
|
||||||
UserId, PhoneNumber, ReferralCode, AccountSequence, ProvinceCode, CityCode,
|
UserId, PhoneNumber, ReferralCode, AccountSequence, ProvinceCode, CityCode,
|
||||||
ChainType, Mnemonic, KYCInfo, HardwareInfo,
|
ChainType, KYCInfo, HardwareInfo,
|
||||||
} from '@/domain/value-objects';
|
} from '@/domain/value-objects';
|
||||||
import { TokenService } from './token.service';
|
import { TokenService } from './token.service';
|
||||||
import { RedisService } from '@/infrastructure/redis/redis.service';
|
import { RedisService } from '@/infrastructure/redis/redis.service';
|
||||||
import { SmsService } from '@/infrastructure/external/sms/sms.service';
|
import { SmsService } from '@/infrastructure/external/sms/sms.service';
|
||||||
import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.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 { MpcWalletService } from '@/infrastructure/external/mpc';
|
||||||
import { ApplicationError } from '@/shared/exceptions/domain.exception';
|
import { ApplicationError } from '@/shared/exceptions/domain.exception';
|
||||||
import { generateRandomIdentity } from '@/shared/utils';
|
import { generateRandomIdentity } from '@/shared/utils';
|
||||||
|
|
@ -40,7 +41,7 @@ export class UserApplicationService {
|
||||||
private readonly mpcKeyShareRepository: MpcKeyShareRepository,
|
private readonly mpcKeyShareRepository: MpcKeyShareRepository,
|
||||||
private readonly sequenceGenerator: AccountSequenceGeneratorService,
|
private readonly sequenceGenerator: AccountSequenceGeneratorService,
|
||||||
private readonly validatorService: UserValidatorService,
|
private readonly validatorService: UserValidatorService,
|
||||||
private readonly walletGenerator: WalletGeneratorService,
|
private readonly blockchainClient: BlockchainClientService,
|
||||||
private readonly mpcWalletService: MpcWalletService,
|
private readonly mpcWalletService: MpcWalletService,
|
||||||
private readonly tokenService: TokenService,
|
private readonly tokenService: TokenService,
|
||||||
private readonly redisService: RedisService,
|
private readonly redisService: RedisService,
|
||||||
|
|
@ -155,15 +156,24 @@ export class UserApplicationService {
|
||||||
if (!account) throw new ApplicationError('账户序列号不存在');
|
if (!account) throw new ApplicationError('账户序列号不存在');
|
||||||
if (!account.isActive) throw new ApplicationError('账户已冻结或注销');
|
if (!account.isActive) throw new ApplicationError('账户已冻结或注销');
|
||||||
|
|
||||||
const mnemonic = Mnemonic.create(command.mnemonic);
|
// 获取账户的钱包地址用于验证
|
||||||
const wallets = this.walletGenerator.recoverWalletSystem({
|
const expectedAddresses: Array<{ chainType: string; address: string }> = [];
|
||||||
userId: account.userId,
|
const kavaWallet = account.getWalletAddress(ChainType.KAVA);
|
||||||
mnemonic,
|
if (kavaWallet) {
|
||||||
deviceId: command.newDeviceId,
|
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 (!verifyResult.valid) {
|
||||||
if (!kavaWallet || kavaWallet.address !== wallets.get(ChainType.KAVA)!.address) {
|
|
||||||
throw new ApplicationError('助记词错误');
|
throw new ApplicationError('助记词错误');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,12 @@ import { InfrastructureModule } from '@/infrastructure/infrastructure.module';
|
||||||
{ provide: USER_ACCOUNT_REPOSITORY, useClass: UserAccountRepositoryImpl },
|
{ provide: USER_ACCOUNT_REPOSITORY, useClass: UserAccountRepositoryImpl },
|
||||||
AccountSequenceGeneratorService,
|
AccountSequenceGeneratorService,
|
||||||
UserValidatorService,
|
UserValidatorService,
|
||||||
// WalletGeneratorService 由 InfrastructureModule 提供
|
|
||||||
UserAccountFactory,
|
UserAccountFactory,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
USER_ACCOUNT_REPOSITORY,
|
USER_ACCOUNT_REPOSITORY,
|
||||||
AccountSequenceGeneratorService,
|
AccountSequenceGeneratorService,
|
||||||
UserValidatorService,
|
UserValidatorService,
|
||||||
// WalletGeneratorService 由 InfrastructureModule 导出
|
|
||||||
UserAccountFactory,
|
UserAccountFactory,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,6 @@ import { Injectable, Inject } from '@nestjs/common';
|
||||||
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, PhoneNumber, ReferralCode, ChainType } from '@/domain/value-objects';
|
import { AccountSequence, PhoneNumber, ReferralCode, ChainType } from '@/domain/value-objects';
|
||||||
|
|
||||||
// 导出 WalletGeneratorService 和相关类型
|
|
||||||
export {
|
|
||||||
WalletGeneratorService,
|
|
||||||
MpcWalletGenerationParams,
|
|
||||||
MpcWalletGenerationResult,
|
|
||||||
ChainWalletInfo,
|
|
||||||
} from './wallet-generator.service';
|
|
||||||
|
|
||||||
// ============ ValidationResult ============
|
// ============ ValidationResult ============
|
||||||
export class ValidationResult {
|
export class ValidationResult {
|
||||||
private constructor(
|
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 { Module, Global } from '@nestjs/common';
|
||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { PrismaService } from './persistence/prisma/prisma.service';
|
import { PrismaService } from './persistence/prisma/prisma.service';
|
||||||
import { UserAccountRepositoryImpl } from './persistence/repositories/user-account.repository.impl';
|
import { UserAccountRepositoryImpl } from './persistence/repositories/user-account.repository.impl';
|
||||||
import { MpcKeyShareRepositoryImpl } from './persistence/repositories/mpc-key-share.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 { EventPublisherService } from './kafka/event-publisher.service';
|
||||||
import { MpcEventConsumerService } from './kafka/mpc-event-consumer.service';
|
import { MpcEventConsumerService } from './kafka/mpc-event-consumer.service';
|
||||||
import { SmsService } from './external/sms/sms.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 { MpcClientService, MpcWalletService } from './external/mpc';
|
||||||
import { MPC_KEY_SHARE_REPOSITORY } from '@/domain/repositories/mpc-key-share.repository.interface';
|
import { MPC_KEY_SHARE_REPOSITORY } from '@/domain/repositories/mpc-key-share.repository.interface';
|
||||||
import { WalletGeneratorService } from '@/domain/services/wallet-generator.service';
|
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
ConfigModule,
|
||||||
HttpModule.register({
|
HttpModule.register({
|
||||||
timeout: 300000,
|
timeout: 300000,
|
||||||
maxRedirects: 5,
|
maxRedirects: 5,
|
||||||
|
|
@ -33,12 +34,8 @@ import { WalletGeneratorService } from '@/domain/services/wallet-generator.servi
|
||||||
EventPublisherService,
|
EventPublisherService,
|
||||||
MpcEventConsumerService,
|
MpcEventConsumerService,
|
||||||
SmsService,
|
SmsService,
|
||||||
// WalletGeneratorService 抽象类由 WalletGeneratorServiceImpl 实现
|
// BlockchainClientService 调用 blockchain-service API
|
||||||
WalletGeneratorServiceImpl,
|
BlockchainClientService,
|
||||||
{
|
|
||||||
provide: WalletGeneratorService,
|
|
||||||
useExisting: WalletGeneratorServiceImpl,
|
|
||||||
},
|
|
||||||
MpcClientService,
|
MpcClientService,
|
||||||
MpcWalletService,
|
MpcWalletService,
|
||||||
],
|
],
|
||||||
|
|
@ -54,8 +51,7 @@ import { WalletGeneratorService } from '@/domain/services/wallet-generator.servi
|
||||||
EventPublisherService,
|
EventPublisherService,
|
||||||
MpcEventConsumerService,
|
MpcEventConsumerService,
|
||||||
SmsService,
|
SmsService,
|
||||||
WalletGeneratorServiceImpl,
|
BlockchainClientService,
|
||||||
WalletGeneratorService,
|
|
||||||
MpcClientService,
|
MpcClientService,
|
||||||
MpcWalletService,
|
MpcWalletService,
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue