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:
parent
383a9540a0
commit
9ae26d0f1f
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue