384 lines
12 KiB
TypeScript
384 lines
12 KiB
TypeScript
/**
|
||
* 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));
|
||
}
|
||
}
|