268 lines
9.5 KiB
TypeScript
268 lines
9.5 KiB
TypeScript
import { Controller, Get, Post, Body, Param, Req, Logger, BadRequestException, HttpException, HttpStatus } from '@nestjs/common';
|
||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
|
||
import { ConfigService } from '@nestjs/config';
|
||
|
||
// walletName → poolType 映射
|
||
interface PoolMapping {
|
||
blockchainPoolType: string; // blockchain-service 使用的类型 (BURN_POOL / MINING_POOL)
|
||
walletPoolType: string; // wallet-service 使用的类型 (SHARE_POOL_A / SHARE_POOL_B)
|
||
name: string;
|
||
}
|
||
|
||
@ApiTags('Pool Accounts')
|
||
@ApiBearerAuth()
|
||
@Controller('admin/pool-accounts')
|
||
export class PoolAccountController {
|
||
private readonly logger = new Logger(PoolAccountController.name);
|
||
private readonly walletServiceUrl: string;
|
||
private readonly blockchainServiceUrl: string;
|
||
private readonly walletNameMap: Record<string, PoolMapping>;
|
||
|
||
constructor(private readonly configService: ConfigService) {
|
||
this.walletServiceUrl = this.configService.get<string>(
|
||
'MINING_WALLET_SERVICE_URL',
|
||
'http://localhost:3025',
|
||
);
|
||
this.blockchainServiceUrl = this.configService.get<string>(
|
||
'MINING_BLOCKCHAIN_SERVICE_URL',
|
||
'http://localhost:3020',
|
||
);
|
||
|
||
// 从环境变量构建 walletName → poolType 映射
|
||
const burnPoolUsername = this.configService.get<string>('BURN_POOL_WALLET_USERNAME', '');
|
||
const miningPoolUsername = this.configService.get<string>('MINING_POOL_WALLET_USERNAME', '');
|
||
|
||
this.walletNameMap = {};
|
||
if (burnPoolUsername) {
|
||
this.walletNameMap[burnPoolUsername] = {
|
||
blockchainPoolType: 'BURN_POOL',
|
||
walletPoolType: 'SHARE_POOL_A',
|
||
name: '100亿销毁池',
|
||
};
|
||
}
|
||
if (miningPoolUsername) {
|
||
this.walletNameMap[miningPoolUsername] = {
|
||
blockchainPoolType: 'MINING_POOL',
|
||
walletPoolType: 'SHARE_POOL_B',
|
||
name: '200万挖矿池',
|
||
};
|
||
}
|
||
|
||
this.logger.log(
|
||
`Pool account proxy initialized: walletService=${this.walletServiceUrl}, blockchainService=${this.blockchainServiceUrl}, pools=${Object.keys(this.walletNameMap).join(',')}`,
|
||
);
|
||
}
|
||
|
||
@Get()
|
||
@ApiOperation({ summary: '获取已配置的池账户列表' })
|
||
async listPoolAccounts() {
|
||
return Object.entries(this.walletNameMap).map(([walletName, info]) => ({
|
||
walletName,
|
||
name: info.name,
|
||
walletPoolType: info.walletPoolType,
|
||
blockchainPoolType: info.blockchainPoolType,
|
||
}));
|
||
}
|
||
|
||
@Get(':walletName/balance')
|
||
@ApiOperation({ summary: '获取池账户余额(代理到 wallet-service + blockchain-service)' })
|
||
@ApiParam({ name: 'walletName', description: '池钱包名称(MPC用户名)' })
|
||
async getPoolAccountBalance(@Param('walletName') walletName: string) {
|
||
const poolInfo = this.walletNameMap[walletName];
|
||
if (!poolInfo) {
|
||
throw new BadRequestException(`Unknown wallet name: ${walletName}`);
|
||
}
|
||
|
||
// 并行调用 wallet-service 和 blockchain-service
|
||
const [walletResponse, blockchainResponse] = await Promise.all([
|
||
fetch(`${this.walletServiceUrl}/api/v2/pool-accounts/${poolInfo.walletPoolType}`).catch((err) => {
|
||
this.logger.error(`Failed to fetch wallet balance: ${err.message}`);
|
||
return null;
|
||
}),
|
||
fetch(`${this.blockchainServiceUrl}/api/v1/transfer/pool-accounts/${poolInfo.blockchainPoolType}/wallet-info`).catch((err) => {
|
||
this.logger.error(`Failed to fetch blockchain wallet info: ${err.message}`);
|
||
return null;
|
||
}),
|
||
]);
|
||
|
||
// 从 wallet-service 获取链下余额
|
||
let balance = '0';
|
||
let lastUpdated: string | undefined;
|
||
if (walletResponse && walletResponse.ok) {
|
||
const walletResult = await walletResponse.json();
|
||
const data = walletResult.data || walletResult;
|
||
balance = data.balance?.toString() || '0';
|
||
lastUpdated = data.updatedAt;
|
||
}
|
||
|
||
// 从 blockchain-service 获取钱包地址和链上余额
|
||
let walletAddress = '';
|
||
let onChainBalance = '0';
|
||
if (blockchainResponse && blockchainResponse.ok) {
|
||
const blockchainResult = await blockchainResponse.json();
|
||
const data = blockchainResult.data || blockchainResult;
|
||
walletAddress = data.address || '';
|
||
onChainBalance = data.balance || '0';
|
||
}
|
||
|
||
return {
|
||
walletName,
|
||
walletAddress,
|
||
balance,
|
||
availableBalance: balance,
|
||
frozenBalance: '0',
|
||
onChainBalance,
|
||
threshold: '0',
|
||
lastUpdated,
|
||
};
|
||
}
|
||
|
||
@Post(':walletName/blockchain-withdraw')
|
||
@ApiOperation({ summary: '池账户区块链提现(代理到 blockchain-service + wallet-service)' })
|
||
@ApiParam({ name: 'walletName', description: '池钱包名称(MPC用户名)' })
|
||
async poolAccountBlockchainWithdraw(
|
||
@Param('walletName') walletName: string,
|
||
@Body() body: { toAddress: string; amount: string },
|
||
) {
|
||
const poolInfo = this.walletNameMap[walletName];
|
||
if (!poolInfo) {
|
||
throw new BadRequestException(`Unknown wallet name: ${walletName}`);
|
||
}
|
||
|
||
if (!body.toAddress || !body.amount) {
|
||
throw new BadRequestException('toAddress and amount are required');
|
||
}
|
||
|
||
this.logger.log(
|
||
`[withdraw] ${poolInfo.name}: ${body.amount} fUSDT to ${body.toAddress}`,
|
||
);
|
||
|
||
// Step 1: 通过 blockchain-service 执行区块链转账
|
||
let blockchainData: any;
|
||
try {
|
||
const blockchainResponse = await fetch(
|
||
`${this.blockchainServiceUrl}/api/v1/transfer/pool-account`,
|
||
{
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
poolType: poolInfo.blockchainPoolType,
|
||
toAddress: body.toAddress,
|
||
amount: body.amount,
|
||
}),
|
||
},
|
||
);
|
||
|
||
const blockchainResult = await blockchainResponse.json();
|
||
blockchainData = blockchainResult.data || blockchainResult;
|
||
|
||
if (!blockchainResponse.ok || !blockchainData.success) {
|
||
this.logger.error(`[withdraw] Blockchain transfer failed: ${blockchainData.error}`);
|
||
throw new HttpException(
|
||
blockchainData.error || '区块链转账失败',
|
||
blockchainResponse.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
}
|
||
} catch (error) {
|
||
if (error instanceof HttpException) throw error;
|
||
this.logger.error(`[withdraw] Failed to call blockchain service: ${error}`);
|
||
throw new HttpException(
|
||
`区块链服务调用失败: ${error instanceof Error ? error.message : error}`,
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
}
|
||
|
||
// Step 2: 在 wallet-service 记录提现(扣减余额 + 分类账)
|
||
try {
|
||
const walletResponse = await fetch(
|
||
`${this.walletServiceUrl}/api/v2/pool-accounts/blockchain-withdraw`,
|
||
{
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
poolType: poolInfo.walletPoolType,
|
||
amount: body.amount,
|
||
txHash: blockchainData.txHash,
|
||
toAddress: body.toAddress,
|
||
blockNumber: blockchainData.blockNumber?.toString(),
|
||
}),
|
||
},
|
||
);
|
||
|
||
if (!walletResponse.ok) {
|
||
const errResult = await walletResponse.json().catch(() => ({}));
|
||
this.logger.error(
|
||
`[withdraw] Failed to record withdrawal in wallet-service: ${JSON.stringify(errResult)}`,
|
||
);
|
||
}
|
||
} catch (error) {
|
||
// 区块链转账已成功,wallet-service 记录失败不影响最终结果
|
||
this.logger.error(`[withdraw] Failed to record withdrawal in wallet-service: ${error}`);
|
||
}
|
||
|
||
this.logger.log(
|
||
`[withdraw] Success: ${poolInfo.name}, txHash=${blockchainData.txHash}`,
|
||
);
|
||
|
||
return {
|
||
success: true,
|
||
message: '提现成功',
|
||
txHash: blockchainData.txHash,
|
||
blockNumber: blockchainData.blockNumber,
|
||
};
|
||
}
|
||
|
||
@Post(':walletName/centralized-deposit')
|
||
@ApiOperation({ summary: '池账户中心化充值(管理员手动调整余额)' })
|
||
@ApiParam({ name: 'walletName', description: '池钱包名称(MPC用户名)' })
|
||
async centralizedDeposit(
|
||
@Param('walletName') walletName: string,
|
||
@Body() body: { amount: string },
|
||
@Req() req: any,
|
||
) {
|
||
const poolInfo = this.walletNameMap[walletName];
|
||
if (!poolInfo) {
|
||
throw new BadRequestException(`Unknown wallet name: ${walletName}`);
|
||
}
|
||
|
||
if (!body.amount || parseFloat(body.amount) <= 0) {
|
||
throw new BadRequestException('amount must be greater than 0');
|
||
}
|
||
|
||
const adminId = req.admin?.id || 'admin';
|
||
this.logger.log(`[centralized-deposit] ${poolInfo.name}: ${body.amount} by admin ${adminId}`);
|
||
|
||
try {
|
||
const walletResponse = await fetch(
|
||
`${this.walletServiceUrl}/api/v2/pool-accounts/centralized-deposit`,
|
||
{
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
poolType: poolInfo.walletPoolType,
|
||
amount: body.amount,
|
||
adminId: String(adminId),
|
||
}),
|
||
},
|
||
);
|
||
|
||
if (!walletResponse.ok) {
|
||
const errResult = await walletResponse.json().catch(() => ({}));
|
||
throw new HttpException(
|
||
errResult.message || '中心化充值失败',
|
||
walletResponse.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
}
|
||
|
||
return { success: true, message: `${poolInfo.name}充值成功` };
|
||
} catch (error) {
|
||
if (error instanceof HttpException) throw error;
|
||
this.logger.error(`[centralized-deposit] Failed: ${error}`);
|
||
throw new HttpException(
|
||
`中心化充值失败: ${error instanceof Error ? error.message : error}`,
|
||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
);
|
||
}
|
||
}
|
||
}
|