refactor(identity-service): replace direct RPC with blockchain-service API calls

- Remove ethers.js direct RPC connection to blockchain
- Add HTTP client to call blockchain-service /balance API
- Add ConfigService for BLOCKCHAIN_SERVICE_URL configuration
- Enforce proper microservice boundaries

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-06 23:07:46 -08:00
parent 383a9540a0
commit 9ae26d0f1f
2 changed files with 82 additions and 91 deletions

View File

@ -1,5 +1,7 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { ethers, JsonRpcProvider, Contract } from 'ethers'; import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';
/** /**
* USDT * USDT
@ -13,75 +15,49 @@ export interface UsdtBalance {
} }
/** /**
* * blockchain-service
*/ */
interface ChainConfig { interface BlockchainBalanceResponse {
rpcUrl: string; chainType: string;
usdtContract: string; address: string;
decimals: number; usdtBalance: string;
name: string; nativeBalance: string;
nativeSymbol: string;
} }
/** /**
* ERC20 ABI ( balanceOf) * blockchain-service
*/ */
const ERC20_ABI = [ interface BlockchainMultiChainResponse {
'function balanceOf(address owner) view returns (uint256)', address: string;
'function decimals() view returns (uint8)', balances: BlockchainBalanceResponse[];
]; }
/** /**
* *
* *
* KAVA EVM BSC USDT * HTTP blockchain-service
*/ */
@Injectable() @Injectable()
export class BlockchainQueryService { export class BlockchainQueryService {
private readonly logger = new Logger(BlockchainQueryService.name); private readonly logger = new Logger(BlockchainQueryService.name);
private readonly blockchainServiceUrl: string;
/** // 链的 decimals 配置
* private readonly chainDecimals: Record<string, number> = {
*/ KAVA: 6,
private readonly chainConfigs: Record<string, ChainConfig> = { BSC: 18,
KAVA: {
rpcUrl: process.env.KAVA_RPC_URL || 'https://evm.kava.io',
// KAVA EVM 原生 USDT 合约地址 (Tether 官方发行)
usdtContract: '0x919C1c267BC06a7039e03fcc2eF738525769109c',
decimals: 6,
name: 'KAVA EVM',
},
BSC: {
rpcUrl: process.env.BSC_RPC_URL || 'https://bsc-dataseed.binance.org',
// BSC USDT 合约地址 (Binance-Peg)
usdtContract: '0x55d398326f99059fF775485246999027B3197955',
decimals: 18,
name: 'BSC',
},
}; };
/** constructor(
* Provider private readonly httpService: HttpService,
*/ private readonly configService: ConfigService,
private providers: Map<string, JsonRpcProvider> = new Map(); ) {
this.blockchainServiceUrl = this.configService.get<string>(
/** 'BLOCKCHAIN_SERVICE_URL',
* Provider 'http://blockchain-service:3000',
*/ );
private getProvider(chainType: string): JsonRpcProvider { this.logger.log(`BlockchainQueryService initialized, URL: ${this.blockchainServiceUrl}`);
if (this.providers.has(chainType)) {
return this.providers.get(chainType)!;
}
const config = this.chainConfigs[chainType];
if (!config) {
throw new Error(`不支持的链类型: ${chainType}`);
}
const provider = new JsonRpcProvider(config.rpcUrl, undefined, {
staticNetwork: true,
});
this.providers.set(chainType, provider);
return provider;
} }
/** /**
@ -91,36 +67,33 @@ export class BlockchainQueryService {
* @param address EVM * @param address EVM
*/ */
async getUsdtBalance(chainType: string, address: string): Promise<UsdtBalance> { async getUsdtBalance(chainType: string, address: string): Promise<UsdtBalance> {
const config = this.chainConfigs[chainType];
if (!config) {
throw new Error(`不支持的链类型: ${chainType}`);
}
// 验证地址格式
if (!ethers.isAddress(address)) {
throw new Error(`无效的 EVM 地址: ${address}`);
}
try { try {
this.logger.debug(`查询 ${config.name} USDT 余额: ${address}`); this.logger.debug(`查询 ${chainType} USDT 余额: ${address}`);
const provider = this.getProvider(chainType); const response = await firstValueFrom(
const contract = new Contract(config.usdtContract, ERC20_ABI, provider); this.httpService.get<BlockchainBalanceResponse>(
`${this.blockchainServiceUrl}/balance`,
{
params: {
chainType,
address,
},
},
),
);
const rawBalance = await contract.balanceOf(address); const data = response.data;
const balance = ethers.formatUnits(rawBalance, config.decimals); this.logger.debug(`${chainType} USDT 余额: ${data.usdtBalance}`);
this.logger.debug(`${config.name} USDT 余额: ${balance}`);
return { return {
chainType, chainType: data.chainType,
address, address: data.address,
balance, balance: data.usdtBalance,
rawBalance: rawBalance.toString(), rawBalance: this.toRawBalance(data.usdtBalance, chainType),
decimals: config.decimals, decimals: this.chainDecimals[chainType] || 6,
}; };
} catch (error) { } catch (error) {
this.logger.error(`查询 ${config.name} USDT 余额失败: ${error.message}`); this.logger.error(`查询 ${chainType} USDT 余额失败: ${error.message}`);
throw new Error(`查询余额失败: ${error.message}`); throw new Error(`查询余额失败: ${error.message}`);
} }
} }
@ -148,7 +121,7 @@ export class BlockchainQueryService {
address: addresses[index].address, address: addresses[index].address,
balance: '0', balance: '0',
rawBalance: '0', rawBalance: '0',
decimals: this.chainConfigs[addresses[index].chainType]?.decimals || 6, decimals: this.chainDecimals[addresses[index].chainType] || 6,
}; };
}); });
} }
@ -157,22 +130,38 @@ export class BlockchainQueryService {
* (KAVA / BNB) * (KAVA / BNB)
*/ */
async getNativeBalance(chainType: string, address: string): Promise<string> { async getNativeBalance(chainType: string, address: string): Promise<string> {
const config = this.chainConfigs[chainType];
if (!config) {
throw new Error(`不支持的链类型: ${chainType}`);
}
if (!ethers.isAddress(address)) {
throw new Error(`无效的 EVM 地址: ${address}`);
}
try { try {
const provider = this.getProvider(chainType); const response = await firstValueFrom(
const rawBalance = await provider.getBalance(address); this.httpService.get<BlockchainBalanceResponse>(
return ethers.formatEther(rawBalance); `${this.blockchainServiceUrl}/balance`,
{
params: {
chainType,
address,
},
},
),
);
return response.data.nativeBalance;
} catch (error) { } catch (error) {
this.logger.error(`查询 ${config.name} 原生代币余额失败: ${error.message}`); this.logger.error(`查询 ${chainType} 原生代币余额失败: ${error.message}`);
throw new Error(`查询余额失败: ${error.message}`); throw new Error(`查询余额失败: ${error.message}`);
} }
} }
/**
* (wei)
*/
private toRawBalance(formattedBalance: string, chainType: string): string {
try {
const decimals = this.chainDecimals[chainType] || 6;
const [intPart, decPart = ''] = formattedBalance.split('.');
const paddedDecPart = decPart.padEnd(decimals, '0').slice(0, decimals);
const rawValue = BigInt(intPart + paddedDecPart);
return rawValue.toString();
} catch {
return '0';
}
}
} }

View File

@ -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';
@ -17,6 +18,7 @@ import { WalletGeneratorService } from '@/domain/services/wallet-generator.servi
@Global() @Global()
@Module({ @Module({
imports: [ imports: [
ConfigModule,
HttpModule.register({ HttpModule.register({
timeout: 300000, timeout: 300000,
maxRedirects: 5, maxRedirects: 5,