rwadurian/backend/services/mining-blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts

384 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* MPC Signing Client
*
* 直接调用 mpc-system 的 account-service (port 4000) 进行 MPC 签名
* 用于热钱包和做市商钱包的 ERC20 转账签名
*
* 签名流程:
* 1. POST /api/v1/mpc/sign → 创建签名会话
* 2. GET /api/v1/mpc/sessions/{session_id} → 轮询签名结果
*/
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { JwtService } from '@nestjs/jwt';
import { randomUUID } from 'crypto';
import { firstValueFrom } from 'rxjs';
export interface CreateSigningInput {
username: string;
messageHash: string;
}
export interface SigningResult {
sessionId: string;
status: string;
signature?: string;
}
// MPC 签名请求 Topic (保留导出以避免外部引用报错)
export const MPC_SIGNING_TOPIC = 'mining_mpc.SigningRequested';
@Injectable()
export class MpcSigningClient {
private readonly logger = new Logger(MpcSigningClient.name);
// C2C Bot 热钱包
private readonly hotWalletUsername: string;
private readonly hotWalletAddress: string;
// eUSDT (积分股) 做市商钱包
private readonly eusdtMarketMakerUsername: string;
private readonly eusdtMarketMakerAddress: string;
// fUSDT (积分值) 做市商钱包
private readonly fusdtMarketMakerUsername: string;
private readonly fusdtMarketMakerAddress: string;
// 100亿销毁池钱包
private readonly burnPoolUsername: string;
private readonly burnPoolAddress: string;
// 200万挖矿池钱包
private readonly miningPoolUsername: string;
private readonly miningPoolAddress: string;
// MPC system 配置
private readonly mpcAccountServiceUrl: string;
private readonly mpcJwtSecret: string;
private readonly signingTimeoutMs: number = 300000; // 5 minutes
private readonly pollingIntervalMs: number = 2000; // 2 seconds
constructor(
private readonly configService: ConfigService,
private readonly httpService: HttpService,
private readonly jwtService: JwtService,
) {
// C2C Bot 热钱包配置
this.hotWalletUsername = this.configService.get<string>('C2C_BOT_WALLET_USERNAME', '');
this.hotWalletAddress = this.configService.get<string>('C2C_BOT_WALLET_ADDRESS', '');
// eUSDT (积分股) 做市商钱包配置
this.eusdtMarketMakerUsername = this.configService.get<string>('EUSDT_MARKET_MAKER_USERNAME', '');
this.eusdtMarketMakerAddress = this.configService.get<string>('EUSDT_MARKET_MAKER_ADDRESS', '');
// fUSDT (积分值) 做市商钱包配置
this.fusdtMarketMakerUsername = this.configService.get<string>('FUSDT_MARKET_MAKER_USERNAME', '');
this.fusdtMarketMakerAddress = this.configService.get<string>('FUSDT_MARKET_MAKER_ADDRESS', '');
// 100亿销毁池钱包配置
this.burnPoolUsername = this.configService.get<string>('BURN_POOL_WALLET_USERNAME', '');
this.burnPoolAddress = this.configService.get<string>('BURN_POOL_WALLET_ADDRESS', '');
// 200万挖矿池钱包配置
this.miningPoolUsername = this.configService.get<string>('MINING_POOL_WALLET_USERNAME', '');
this.miningPoolAddress = this.configService.get<string>('MINING_POOL_WALLET_ADDRESS', '');
// MPC system 配置
this.mpcAccountServiceUrl = this.configService.get<string>('MPC_ACCOUNT_SERVICE_URL', 'http://localhost:4000');
this.mpcJwtSecret = this.configService.get<string>('MPC_JWT_SECRET', '');
if (!this.hotWalletUsername) {
this.logger.warn('[INIT] C2C_BOT_WALLET_USERNAME not configured (C2C Bot disabled)');
}
if (!this.hotWalletAddress) {
this.logger.warn('[INIT] C2C_BOT_WALLET_ADDRESS not configured (C2C Bot disabled)');
}
if (!this.eusdtMarketMakerUsername || !this.eusdtMarketMakerAddress) {
this.logger.warn('[INIT] eUSDT Market Maker not configured');
}
if (!this.fusdtMarketMakerUsername || !this.fusdtMarketMakerAddress) {
this.logger.warn('[INIT] fUSDT Market Maker not configured');
}
if (!this.burnPoolUsername || !this.burnPoolAddress) {
this.logger.warn('[INIT] Burn Pool wallet not configured');
}
if (!this.miningPoolUsername || !this.miningPoolAddress) {
this.logger.warn('[INIT] Mining Pool wallet not configured');
}
if (!this.mpcJwtSecret) {
this.logger.warn('[INIT] MPC_JWT_SECRET not configured - signing will fail');
}
this.logger.log(`[INIT] C2C Bot Wallet: ${this.hotWalletAddress || '(not configured)'}`);
this.logger.log(`[INIT] eUSDT Market Maker: ${this.eusdtMarketMakerAddress || '(not configured)'}`);
this.logger.log(`[INIT] fUSDT Market Maker: ${this.fusdtMarketMakerAddress || '(not configured)'}`);
this.logger.log(`[INIT] Burn Pool: ${this.burnPoolAddress || '(not configured)'}`);
this.logger.log(`[INIT] Mining Pool: ${this.miningPoolAddress || '(not configured)'}`);
this.logger.log(`[INIT] MPC Account Service: ${this.mpcAccountServiceUrl}`);
this.logger.log(`[INIT] Using HTTP direct call to mpc-system`);
}
/**
* 检查 C2C Bot 热钱包是否已配置
*/
isConfigured(): boolean {
return !!this.hotWalletUsername && !!this.hotWalletAddress;
}
/**
* 检查 eUSDT 做市商钱包是否已配置
*/
isEusdtMarketMakerConfigured(): boolean {
return !!this.eusdtMarketMakerUsername && !!this.eusdtMarketMakerAddress;
}
/**
* 检查 fUSDT 做市商钱包是否已配置
*/
isFusdtMarketMakerConfigured(): boolean {
return !!this.fusdtMarketMakerUsername && !!this.fusdtMarketMakerAddress;
}
/**
* 获取 C2C Bot 热钱包地址
*/
getHotWalletAddress(): string {
return this.hotWalletAddress;
}
/**
* 获取 C2C Bot 热钱包用户名
*/
getHotWalletUsername(): string {
return this.hotWalletUsername;
}
/**
* 获取 eUSDT 做市商钱包地址
*/
getEusdtMarketMakerAddress(): string {
return this.eusdtMarketMakerAddress;
}
/**
* 获取 eUSDT 做市商 MPC 用户名
*/
getEusdtMarketMakerUsername(): string {
return this.eusdtMarketMakerUsername;
}
/**
* 获取 fUSDT 做市商钱包地址
*/
getFusdtMarketMakerAddress(): string {
return this.fusdtMarketMakerAddress;
}
/**
* 获取 fUSDT 做市商 MPC 用户名
*/
getFusdtMarketMakerUsername(): string {
return this.fusdtMarketMakerUsername;
}
// ============ 100亿销毁池钱包 ============
isBurnPoolConfigured(): boolean {
return !!this.burnPoolUsername && !!this.burnPoolAddress;
}
getBurnPoolAddress(): string {
return this.burnPoolAddress;
}
getBurnPoolUsername(): string {
return this.burnPoolUsername;
}
async signMessageAsBurnPool(messageHash: string): Promise<string> {
if (!this.burnPoolUsername) {
throw new Error('Burn Pool MPC username not configured');
}
return this.signMessageWithUsername(this.burnPoolUsername, messageHash);
}
// ============ 200万挖矿池钱包 ============
isMiningPoolConfigured(): boolean {
return !!this.miningPoolUsername && !!this.miningPoolAddress;
}
getMiningPoolAddress(): string {
return this.miningPoolAddress;
}
getMiningPoolUsername(): string {
return this.miningPoolUsername;
}
async signMessageAsMiningPool(messageHash: string): Promise<string> {
if (!this.miningPoolUsername) {
throw new Error('Mining Pool MPC username not configured');
}
return this.signMessageWithUsername(this.miningPoolUsername, messageHash);
}
/**
* 签名消息(使用 C2C Bot 热钱包)
*
* @param messageHash 要签名的消息哈希 (hex string with 0x prefix)
* @returns 签名结果 (hex string)
*/
async signMessage(messageHash: string): Promise<string> {
if (!this.hotWalletUsername) {
throw new Error('Hot wallet username not configured');
}
return this.signMessageWithUsername(this.hotWalletUsername, messageHash);
}
/**
* 使用 eUSDT 做市商钱包签名消息
*
* @param messageHash 要签名的消息哈希 (hex string with 0x prefix)
* @returns 签名结果 (hex string)
*/
async signMessageAsEusdtMarketMaker(messageHash: string): Promise<string> {
if (!this.eusdtMarketMakerUsername) {
throw new Error('eUSDT Market Maker MPC username not configured');
}
return this.signMessageWithUsername(this.eusdtMarketMakerUsername, messageHash);
}
/**
* 使用 fUSDT 做市商钱包签名消息
*
* @param messageHash 要签名的消息哈希 (hex string with 0x prefix)
* @returns 签名结果 (hex string)
*/
async signMessageAsFusdtMarketMaker(messageHash: string): Promise<string> {
if (!this.fusdtMarketMakerUsername) {
throw new Error('fUSDT Market Maker MPC username not configured');
}
return this.signMessageWithUsername(this.fusdtMarketMakerUsername, messageHash);
}
/**
* 使用指定用户名签名消息HTTP 直调 mpc-system
*
* @param username MPC 用户名
* @param messageHash 要签名的消息哈希 (hex string with 0x prefix)
* @returns 签名结果 (hex string)
*/
async signMessageWithUsername(username: string, messageHash: string): Promise<string> {
this.logger.log(`[SIGN] Starting MPC signing for: ${messageHash.slice(0, 16)}... (username: ${username})`);
if (!username) {
throw new Error('MPC username not provided');
}
if (!this.mpcJwtSecret) {
throw new Error('MPC_JWT_SECRET not configured');
}
// Step 1: 创建签名会话
const createUrl = `${this.mpcAccountServiceUrl}/api/v1/mpc/sign`;
const headers = this.getMpcAuthHeaders();
this.logger.log(`[SIGN] POST ${createUrl}`);
const createResponse = await firstValueFrom(
this.httpService.post<{
session_id: string;
status: string;
session_type?: string;
username?: string;
message_hash?: string;
}>(
createUrl,
{ username, message_hash: messageHash.startsWith('0x') ? messageHash.slice(2) : messageHash },
{ headers, timeout: 30000 },
),
);
const sessionId = createResponse.data.session_id;
this.logger.log(`[SIGN] Session created: ${sessionId}, status: ${createResponse.data.status}`);
// Step 2: 轮询签名结果
const signature = await this.pollSigningStatus(sessionId);
this.logger.log(`[SIGN] Signature obtained: ${signature.slice(0, 20)}...`);
return signature;
}
/**
* 轮询签名会话状态直到完成或超时
*/
private async pollSigningStatus(sessionId: string): Promise<string> {
const statusUrl = `${this.mpcAccountServiceUrl}/api/v1/mpc/sessions/${sessionId}`;
const maxAttempts = Math.ceil(this.signingTimeoutMs / this.pollingIntervalMs);
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
await this.delay(this.pollingIntervalMs);
const response = await firstValueFrom(
this.httpService.get<{
session_id: string;
status: string;
session_type?: string;
completed_parties?: number;
total_parties?: number;
signature?: string;
}>(statusUrl, {
headers: this.getMpcAuthHeaders(),
timeout: 10000,
}),
);
const { status, signature } = response.data;
if (attempt % 5 === 0 || status !== 'pending') {
this.logger.log(`[POLL] Attempt ${attempt}/${maxAttempts}: status=${status}`);
}
if (status === 'completed') {
if (!signature) {
throw new Error('Signing completed but no signature returned');
}
return signature;
}
if (status === 'failed' || status === 'expired') {
throw new Error(`MPC signing ${status}: sessionId=${sessionId}`);
}
}
throw new Error(`MPC signing timeout after ${this.signingTimeoutMs}ms`);
}
/**
* 生成 mpc-system 认证 JWT token
*/
private generateMpcAccessToken(): string {
const now = Math.floor(Date.now() / 1000);
const payload = {
jti: randomUUID(),
iss: 'mining-blockchain-service',
sub: 'system',
username: 'mining-blockchain-service',
token_type: 'access',
iat: now,
nbf: now,
exp: now + 24 * 60 * 60,
};
return this.jwtService.sign(payload, {
secret: this.mpcJwtSecret,
algorithm: 'HS256' as const,
});
}
/**
* 获取 mpc-system 认证请求头
*/
private getMpcAuthHeaders(): Record<string, string> {
const token = this.generateMpcAccessToken();
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
};
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}