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 { ethers, JsonRpcProvider, Contract } from 'ethers';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';
/**
* USDT
@ -13,75 +15,49 @@ export interface UsdtBalance {
}
/**
*
* blockchain-service
*/
interface ChainConfig {
rpcUrl: string;
usdtContract: string;
decimals: number;
name: string;
interface BlockchainBalanceResponse {
chainType: string;
address: string;
usdtBalance: string;
nativeBalance: string;
nativeSymbol: string;
}
/**
* ERC20 ABI ( balanceOf)
* blockchain-service
*/
const ERC20_ABI = [
'function balanceOf(address owner) view returns (uint256)',
'function decimals() view returns (uint8)',
];
interface BlockchainMultiChainResponse {
address: string;
balances: BlockchainBalanceResponse[];
}
/**
*
*
* KAVA EVM BSC USDT
* HTTP blockchain-service
*/
@Injectable()
export class BlockchainQueryService {
private readonly logger = new Logger(BlockchainQueryService.name);
private readonly blockchainServiceUrl: string;
/**
*
*/
private readonly chainConfigs: Record<string, ChainConfig> = {
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',
},
// 链的 decimals 配置
private readonly chainDecimals: Record<string, number> = {
KAVA: 6,
BSC: 18,
};
/**
* Provider
*/
private providers: Map<string, JsonRpcProvider> = new Map();
/**
* Provider
*/
private getProvider(chainType: string): JsonRpcProvider {
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;
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(`BlockchainQueryService initialized, URL: ${this.blockchainServiceUrl}`);
}
/**
@ -91,36 +67,33 @@ export class BlockchainQueryService {
* @param address EVM
*/
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 {
this.logger.debug(`查询 ${config.name} USDT 余额: ${address}`);
this.logger.debug(`查询 ${chainType} USDT 余额: ${address}`);
const provider = this.getProvider(chainType);
const contract = new Contract(config.usdtContract, ERC20_ABI, provider);
const response = await firstValueFrom(
this.httpService.get<BlockchainBalanceResponse>(
`${this.blockchainServiceUrl}/balance`,
{
params: {
chainType,
address,
},
},
),
);
const rawBalance = await contract.balanceOf(address);
const balance = ethers.formatUnits(rawBalance, config.decimals);
this.logger.debug(`${config.name} USDT 余额: ${balance}`);
const data = response.data;
this.logger.debug(`${chainType} USDT 余额: ${data.usdtBalance}`);
return {
chainType,
address,
balance,
rawBalance: rawBalance.toString(),
decimals: config.decimals,
chainType: data.chainType,
address: data.address,
balance: data.usdtBalance,
rawBalance: this.toRawBalance(data.usdtBalance, chainType),
decimals: this.chainDecimals[chainType] || 6,
};
} catch (error) {
this.logger.error(`查询 ${config.name} USDT 余额失败: ${error.message}`);
this.logger.error(`查询 ${chainType} USDT 余额失败: ${error.message}`);
throw new Error(`查询余额失败: ${error.message}`);
}
}
@ -148,7 +121,7 @@ export class BlockchainQueryService {
address: addresses[index].address,
balance: '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)
*/
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 {
const provider = this.getProvider(chainType);
const rawBalance = await provider.getBalance(address);
return ethers.formatEther(rawBalance);
const response = await firstValueFrom(
this.httpService.get<BlockchainBalanceResponse>(
`${this.blockchainServiceUrl}/balance`,
{
params: {
chainType,
address,
},
},
),
);
return response.data.nativeBalance;
} catch (error) {
this.logger.error(`查询 ${config.name} 原生代币余额失败: ${error.message}`);
this.logger.error(`查询 ${chainType} 原生代币余额失败: ${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 { HttpModule } from '@nestjs/axios';
import { ConfigModule } from '@nestjs/config';
import { PrismaService } from './persistence/prisma/prisma.service';
import { UserAccountRepositoryImpl } from './persistence/repositories/user-account.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()
@Module({
imports: [
ConfigModule,
HttpModule.register({
timeout: 300000,
maxRedirects: 5,