feat(blockchain): 将提现转账从私钥签名改为 MPC 签名

背景:
- 原实现使用 HOT_WALLET_PRIVATE_KEY 进行热钱包签名
- 私钥直接存储存在安全风险
- 系统已有 MPC 基础设施,应该复用

改动内容:

1. 新增 MPC 签名客户端
   - infrastructure/mpc/mpc-signing.client.ts: 调用 mpc-service 的签名 API
   - 支持创建签名会话、轮询等待、获取签名结果

2. 重构 ERC20 转账服务
   - domain/services/erc20-transfer.service.ts: 从私钥签名改为 MPC 签名
   - 移除 Wallet 依赖,改用 Transaction 手动构建交易
   - 使用 MPC 签名后广播已签名交易

3. 新增初始化服务
   - mpc-transfer-initializer.service.ts: 启动时注入 MPC 客户端
   - 解决 Domain 层和 Infrastructure 层的循环依赖

4. 新增热钱包初始化脚本
   - scripts/init-hot-wallet.sh: 便捷创建系统热钱包的 MPC 密钥
   - 支持配置门限值、用户名等参数

5. 更新配置
   - 移除 HOT_WALLET_PRIVATE_KEY 依赖
   - 新增 MPC_SERVICE_URL, HOT_WALLET_USERNAME, HOT_WALLET_ADDRESS
   - 更新 docker-compose.yml 和 .env.example

部署前需要:
1. 运行 init-hot-wallet.sh 初始化热钱包
2. 配置 HOT_WALLET_USERNAME 和 HOT_WALLET_ADDRESS
3. 向热钱包充值 USDT 和原生币(gas)

🤖 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-15 08:04:17 -08:00
parent 28c44d7219
commit 9cac91b5f0
12 changed files with 572 additions and 42 deletions

View File

@ -0,0 +1,187 @@
#!/bin/bash
# =============================================================================
# 热钱包 MPC 初始化脚本
# =============================================================================
#
# 用途: 创建系统热钱包的 MPC 密钥,用于提现转账
#
# 前提条件:
# 1. mpc-service 正在运行
# 2. mpc-system 正在运行且所有 party 已启动
#
# 使用方法:
# ./init-hot-wallet.sh [options]
#
# 选项:
# -u, --username 热钱包用户名 (默认: system-hot-wallet)
# -n, --threshold-n 总 party 数量 (默认: 3)
# -t, --threshold-t 签名门限值 (默认: 2)
# -h, --host mpc-service 地址 (默认: http://localhost:3013)
# --help 显示帮助
#
# =============================================================================
set -e
# 默认配置
USERNAME="system-hot-wallet"
THRESHOLD_N=3
THRESHOLD_T=2
MPC_HOST="http://localhost:3013"
# 解析参数
while [[ $# -gt 0 ]]; do
case $1 in
-u|--username)
USERNAME="$2"
shift 2
;;
-n|--threshold-n)
THRESHOLD_N="$2"
shift 2
;;
-t|--threshold-t)
THRESHOLD_T="$2"
shift 2
;;
-h|--host)
MPC_HOST="$2"
shift 2
;;
--help)
head -30 "$0" | tail -25
exit 0
;;
*)
echo "未知参数: $1"
exit 1
;;
esac
done
echo "=============================================="
echo "热钱包 MPC 初始化"
echo "=============================================="
echo "用户名: $USERNAME"
echo "门限: $THRESHOLD_T-of-$THRESHOLD_N"
echo "MPC 服务: $MPC_HOST"
echo "=============================================="
echo ""
# Step 1: 创建 Keygen 会话
echo "[1/4] 创建 Keygen 会话..."
KEYGEN_RESPONSE=$(curl -s -X POST "$MPC_HOST/mpc/keygen" \
-H "Content-Type: application/json" \
-d "{
\"username\": \"$USERNAME\",
\"thresholdN\": $THRESHOLD_N,
\"thresholdT\": $THRESHOLD_T,
\"requireDelegate\": true
}")
SESSION_ID=$(echo "$KEYGEN_RESPONSE" | jq -r '.sessionId')
STATUS=$(echo "$KEYGEN_RESPONSE" | jq -r '.status')
if [ "$SESSION_ID" == "null" ] || [ -z "$SESSION_ID" ]; then
echo "错误: 创建 Keygen 会话失败"
echo "响应: $KEYGEN_RESPONSE"
exit 1
fi
echo "会话 ID: $SESSION_ID"
echo "状态: $STATUS"
echo ""
# Step 2: 轮询等待 Keygen 完成
echo "[2/4] 等待 Keygen 完成..."
MAX_ATTEMPTS=180 # 最多等待 6 分钟
ATTEMPT=0
PUBLIC_KEY=""
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
STATUS_RESPONSE=$(curl -s -X GET "$MPC_HOST/mpc/keygen/$SESSION_ID/status")
STATUS=$(echo "$STATUS_RESPONSE" | jq -r '.status')
PUBLIC_KEY=$(echo "$STATUS_RESPONSE" | jq -r '.publicKey // empty')
echo " 轮询 #$((ATTEMPT + 1)): 状态=$STATUS"
if [ "$STATUS" == "completed" ]; then
echo ""
echo "Keygen 完成!"
break
fi
if [ "$STATUS" == "failed" ] || [ "$STATUS" == "expired" ]; then
echo ""
echo "错误: Keygen 失败,状态=$STATUS"
exit 1
fi
ATTEMPT=$((ATTEMPT + 1))
sleep 2
done
if [ "$STATUS" != "completed" ]; then
echo "错误: Keygen 超时"
exit 1
fi
# Step 3: 显示公钥
echo ""
echo "[3/4] 获取公钥..."
if [ -z "$PUBLIC_KEY" ]; then
# 再次获取状态以确保拿到公钥
STATUS_RESPONSE=$(curl -s -X GET "$MPC_HOST/mpc/keygen/$SESSION_ID/status")
PUBLIC_KEY=$(echo "$STATUS_RESPONSE" | jq -r '.publicKey')
fi
if [ -z "$PUBLIC_KEY" ] || [ "$PUBLIC_KEY" == "null" ]; then
echo "错误: 无法获取公钥"
exit 1
fi
echo "公钥: $PUBLIC_KEY"
# Step 4: 从公钥派生 EVM 地址
echo ""
echo "[4/4] 派生 EVM 地址..."
# 使用 node 计算地址(如果安装了 ethers
if command -v node &> /dev/null; then
ADDRESS=$(node -e "
const { computeAddress } = require('ethers');
try {
const address = computeAddress('0x$PUBLIC_KEY');
console.log(address);
} catch (e) {
console.error('Error:', e.message);
process.exit(1);
}
" 2>/dev/null || echo "")
if [ -n "$ADDRESS" ] && [ "$ADDRESS" != "Error:"* ]; then
echo "EVM 地址: $ADDRESS"
else
echo "提示: 无法自动计算地址,请手动计算"
echo "公钥 (hex): $PUBLIC_KEY"
fi
else
echo "提示: 未安装 Node.js请手动从公钥计算 EVM 地址"
echo "公钥 (hex): $PUBLIC_KEY"
fi
echo ""
echo "=============================================="
echo "初始化完成!"
echo "=============================================="
echo ""
echo "请将以下配置添加到 blockchain-service 的环境变量:"
echo ""
echo " HOT_WALLET_USERNAME=$USERNAME"
if [ -n "$ADDRESS" ] && [ "$ADDRESS" != "Error:"* ]; then
echo " HOT_WALLET_ADDRESS=$ADDRESS"
else
echo " HOT_WALLET_ADDRESS=<从公钥计算的地址>"
fi
echo ""
echo "=============================================="

View File

@ -117,3 +117,14 @@ MINIO_BUCKET_AVATARS=avatars
# Public URL for accessing files
MINIO_PUBLIC_URL=https://minio.szaiai.com
# =============================================================================
# MPC Hot Wallet (用于提现转账)
# =============================================================================
# 热钱包用户名MPC 系统中的标识,需要预先通过 keygen 创建)
# 使用 scripts/init-hot-wallet.sh 脚本初始化
HOT_WALLET_USERNAME=system-hot-wallet
# 热钱包地址(从 MPC 公钥派生的 EVM 地址)
# 在 MPC keygen 完成后,从公钥计算得出
HOT_WALLET_ADDRESS=

View File

@ -87,6 +87,22 @@ BLOCK_CONFIRMATIONS_REQUIRED=12
# Maximum blocks to process in one batch
BLOCK_SCAN_BATCH_SIZE=100
# =============================================================================
# MPC Hot Wallet (用于提现转账)
# =============================================================================
# MPC 服务地址
# Docker Compose: http://mpc-service:3013 / Direct: http://192.168.1.111:3013
MPC_SERVICE_URL=http://192.168.1.111:3013
# 热钱包用户名MPC 系统中的标识,需要预先通过 keygen 创建)
# 示例: system-hot-wallet
HOT_WALLET_USERNAME=system-hot-wallet
# 热钱包地址(从 MPC 公钥派生的 EVM 地址)
# 在 MPC keygen 完成后,从公钥计算得出
# 示例: 0x1234567890abcdef1234567890abcdef12345678
HOT_WALLET_ADDRESS=
# =============================================================================
# Logging
# =============================================================================

View File

@ -34,6 +34,10 @@ services:
# Blockchain RPC
KAVA_RPC_URL: https://evm.kava.io
BSC_RPC_URL: https://bsc-dataseed.binance.org
# MPC Hot Wallet (用于提现转账)
MPC_SERVICE_URL: http://rwa-mpc-service:3013
HOT_WALLET_USERNAME: ${HOT_WALLET_USERNAME:-}
HOT_WALLET_ADDRESS: ${HOT_WALLET_ADDRESS:-}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3012/api/v1/health"]
interval: 30s

View File

@ -8,6 +8,7 @@ import {
MnemonicVerificationService,
OutboxPublisherService,
DepositRepairService,
MpcTransferInitializerService,
} from './services';
import { MpcKeygenCompletedHandler, WithdrawalRequestedHandler } from './event-handlers';
import { DepositAckConsumerService } from '@/infrastructure/kafka/deposit-ack-consumer.service';
@ -22,6 +23,7 @@ import { DepositAckConsumerService } from '@/infrastructure/kafka/deposit-ack-co
MnemonicVerificationService,
OutboxPublisherService,
DepositRepairService,
MpcTransferInitializerService,
// 事件消费者(依赖 OutboxPublisherService需要在这里注册
DepositAckConsumerService,

View File

@ -4,3 +4,4 @@ export * from './balance-query.service';
export * from './mnemonic-verification.service';
export * from './outbox-publisher.service';
export * from './deposit-repair.service';
export * from './mpc-transfer-initializer.service';

View File

@ -0,0 +1,26 @@
/**
* MPC Transfer Initializer Service
*
* MPC ERC20
* Domain Infrastructure
*/
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { Erc20TransferService } from '@/domain/services/erc20-transfer.service';
import { MpcSigningClient } from '@/infrastructure/mpc';
@Injectable()
export class MpcTransferInitializerService implements OnModuleInit {
private readonly logger = new Logger(MpcTransferInitializerService.name);
constructor(
private readonly erc20TransferService: Erc20TransferService,
private readonly mpcSigningClient: MpcSigningClient,
) {}
onModuleInit() {
this.logger.log('[INIT] Injecting MPC Signing Client into ERC20 Transfer Service');
this.erc20TransferService.setMpcSigningClient(this.mpcSigningClient);
this.logger.log('[INIT] MPC Signing Client injection complete');
}
}

View File

@ -1,6 +1,13 @@
import { Injectable, Logger } from '@nestjs/common';
import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JsonRpcProvider, Wallet, Contract, parseUnits, formatUnits } from 'ethers';
import {
JsonRpcProvider,
Contract,
parseUnits,
formatUnits,
Transaction,
Signature,
} from 'ethers';
import { ChainConfigService } from './chain-config.service';
import { ChainType } from '@/domain/value-objects';
import { ChainTypeEnum } from '@/domain/enums';
@ -21,75 +28,97 @@ export interface TransferResult {
blockNumber?: number;
}
// MPC 签名客户端接口(避免循环依赖)
export interface IMpcSigningClient {
isConfigured(): boolean;
getHotWalletAddress(): string;
signMessage(messageHash: string): Promise<string>;
}
export const MPC_SIGNING_CLIENT = Symbol('MPC_SIGNING_CLIENT');
/**
* ERC20
*
* ERC20
* 使 MPC
* 使 MPC
*/
@Injectable()
export class Erc20TransferService {
private readonly logger = new Logger(Erc20TransferService.name);
private readonly hotWallets: Map<ChainTypeEnum, Wallet> = new Map();
private readonly providers: Map<ChainTypeEnum, JsonRpcProvider> = new Map();
private readonly hotWalletAddress: string;
private mpcSigningClient: IMpcSigningClient | null = null;
constructor(
private readonly configService: ConfigService,
private readonly chainConfig: ChainConfigService,
) {
this.initializeHotWallets();
this.hotWalletAddress = this.configService.get<string>('HOT_WALLET_ADDRESS', '');
this.initializeProviders();
}
private initializeHotWallets(): void {
// 从环境变量获取热钱包私钥
const hotWalletPrivateKey = this.configService.get<string>('HOT_WALLET_PRIVATE_KEY');
/**
* MPC
*/
setMpcSigningClient(client: IMpcSigningClient): void {
this.mpcSigningClient = client;
this.logger.log(`[INIT] MPC Signing Client injected`);
}
if (!hotWalletPrivateKey) {
this.logger.warn('[INIT] HOT_WALLET_PRIVATE_KEY not configured, transfers will fail');
return;
}
// 为每条支持的链创建钱包
private initializeProviders(): void {
// 为每条支持的链创建 Provider
for (const chainType of this.chainConfig.getSupportedChains()) {
try {
const config = this.chainConfig.getConfig(ChainType.fromEnum(chainType));
const provider = new JsonRpcProvider(config.rpcUrl, config.chainId);
const wallet = new Wallet(hotWalletPrivateKey, provider);
this.hotWallets.set(chainType, wallet);
this.logger.log(`[INIT] Hot wallet initialized for ${chainType}: ${wallet.address}`);
this.providers.set(chainType, provider);
this.logger.log(`[INIT] Provider initialized for ${chainType}: ${config.rpcUrl}`);
} catch (error) {
this.logger.error(`[INIT] Failed to initialize wallet for ${chainType}`, error);
this.logger.error(`[INIT] Failed to initialize provider for ${chainType}`, error);
}
}
// 检查热钱包地址配置
if (this.hotWalletAddress) {
this.logger.log(`[INIT] Hot wallet address configured: ${this.hotWalletAddress}`);
} else {
this.logger.warn('[INIT] HOT_WALLET_ADDRESS not configured, transfers will fail');
}
}
/**
*
*/
getHotWalletAddress(chainType: ChainTypeEnum): string | null {
const wallet = this.hotWallets.get(chainType);
return wallet?.address ?? null;
// MPC 钱包地址在所有 EVM 链上相同
return this.hotWalletAddress || null;
}
/**
* USDT
*/
async getHotWalletBalance(chainType: ChainTypeEnum): Promise<string> {
const wallet = this.hotWallets.get(chainType);
if (!wallet) {
throw new Error(`Hot wallet not configured for chain: ${chainType}`);
const provider = this.providers.get(chainType);
if (!provider) {
throw new Error(`Provider not configured for chain: ${chainType}`);
}
if (!this.hotWalletAddress) {
throw new Error('Hot wallet address not configured');
}
const config = this.chainConfig.getConfig(ChainType.fromEnum(chainType));
const contract = new Contract(config.usdtContract, ERC20_TRANSFER_ABI, wallet.provider);
const contract = new Contract(config.usdtContract, ERC20_TRANSFER_ABI, provider);
const balance = await contract.balanceOf(wallet.address);
const balance = await contract.balanceOf(this.hotWalletAddress);
const decimals = await contract.decimals();
return formatUnits(balance, decimals);
}
/**
* ERC20
* ERC20 使 MPC
*
* @param chainType (KAVA, BSC)
* @param toAddress
@ -101,21 +130,33 @@ export class Erc20TransferService {
toAddress: string,
amount: string,
): Promise<TransferResult> {
this.logger.log(`[TRANSFER] Starting USDT transfer`);
this.logger.log(`[TRANSFER] Starting USDT transfer with MPC signing`);
this.logger.log(`[TRANSFER] Chain: ${chainType}`);
this.logger.log(`[TRANSFER] To: ${toAddress}`);
this.logger.log(`[TRANSFER] Amount: ${amount} USDT`);
const wallet = this.hotWallets.get(chainType);
if (!wallet) {
const error = `Hot wallet not configured for chain: ${chainType}`;
const provider = this.providers.get(chainType);
if (!provider) {
const error = `Provider not configured for chain: ${chainType}`;
this.logger.error(`[TRANSFER] ${error}`);
return { success: false, error };
}
if (!this.mpcSigningClient || !this.mpcSigningClient.isConfigured()) {
const error = 'MPC signing client not configured';
this.logger.error(`[TRANSFER] ${error}`);
return { success: false, error };
}
if (!this.hotWalletAddress) {
const error = 'Hot wallet address not configured';
this.logger.error(`[TRANSFER] ${error}`);
return { success: false, error };
}
try {
const config = this.chainConfig.getConfig(ChainType.fromEnum(chainType));
const contract = new Contract(config.usdtContract, ERC20_TRANSFER_ABI, wallet);
const contract = new Contract(config.usdtContract, ERC20_TRANSFER_ABI, provider);
// 获取代币精度
const decimals = await contract.decimals();
@ -126,7 +167,7 @@ export class Erc20TransferService {
this.logger.log(`[TRANSFER] Amount in wei: ${amountInWei.toString()}`);
// 检查余额
const balance = await contract.balanceOf(wallet.address);
const balance = await contract.balanceOf(this.hotWalletAddress);
this.logger.log(`[TRANSFER] Hot wallet balance: ${formatUnits(balance, decimals)} USDT`);
if (balance < amountInWei) {
@ -135,30 +176,74 @@ export class Erc20TransferService {
return { success: false, error };
}
// 执行转账
this.logger.log(`[TRANSFER] Sending transaction...`);
const tx = await contract.transfer(toAddress, amountInWei);
this.logger.log(`[TRANSFER] Transaction sent: ${tx.hash}`);
// 构建交易
this.logger.log(`[TRANSFER] Building transaction...`);
const nonce = await provider.getTransactionCount(this.hotWalletAddress);
const feeData = await provider.getFeeData();
// ERC20 transfer 的 calldata
const transferData = contract.interface.encodeFunctionData('transfer', [toAddress, amountInWei]);
// 估算 gas
const gasEstimate = await provider.estimateGas({
from: this.hotWalletAddress,
to: config.usdtContract,
data: transferData,
});
const tx = Transaction.from({
type: 2, // EIP-1559
chainId: config.chainId,
nonce,
to: config.usdtContract,
data: transferData,
gasLimit: gasEstimate * BigInt(120) / BigInt(100), // 增加 20% buffer
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
});
this.logger.log(`[TRANSFER] Transaction built: nonce=${nonce}, gasLimit=${tx.gasLimit}`);
// 获取交易哈希用于签名
const unsignedTxHash = tx.unsignedHash;
this.logger.log(`[TRANSFER] Unsigned tx hash: ${unsignedTxHash}`);
// 使用 MPC 签名
this.logger.log(`[TRANSFER] Requesting MPC signature...`);
const signatureHex = await this.mpcSigningClient.signMessage(unsignedTxHash);
this.logger.log(`[TRANSFER] MPC signature obtained: ${signatureHex.slice(0, 20)}...`);
// 解析签名
const signature = Signature.from(signatureHex);
// 创建已签名交易
const signedTx = tx.clone();
signedTx.signature = signature;
// 广播交易
this.logger.log(`[TRANSFER] Broadcasting transaction...`);
const txResponse = await provider.broadcastTransaction(signedTx.serialized);
this.logger.log(`[TRANSFER] Transaction sent: ${txResponse.hash}`);
// 等待确认
this.logger.log(`[TRANSFER] Waiting for confirmation...`);
const receipt = await tx.wait();
const receipt = await txResponse.wait();
if (receipt.status === 1) {
if (receipt && receipt.status === 1) {
this.logger.log(`[TRANSFER] Transaction confirmed!`);
this.logger.log(`[TRANSFER] Block: ${receipt.blockNumber}`);
this.logger.log(`[TRANSFER] Gas used: ${receipt.gasUsed.toString()}`);
return {
success: true,
txHash: tx.hash,
txHash: txResponse.hash,
gasUsed: receipt.gasUsed.toString(),
blockNumber: receipt.blockNumber,
};
} else {
const error = 'Transaction failed (reverted)';
this.logger.error(`[TRANSFER] ${error}`);
return { success: false, txHash: tx.hash, error };
return { success: false, txHash: txResponse.hash, error };
}
} catch (error: any) {
this.logger.error(`[TRANSFER] Transfer failed:`, error);
@ -173,6 +258,8 @@ export class Erc20TransferService {
*
*/
isConfigured(chainType: ChainTypeEnum): boolean {
return this.hotWallets.has(chainType);
return this.providers.has(chainType) &&
!!this.hotWalletAddress &&
!!this.mpcSigningClient?.isConfigured();
}
}

View File

@ -1,8 +1,10 @@
import { Global, Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { PrismaService } from './persistence/prisma/prisma.service';
import { RedisService, AddressCacheService } from './redis';
import { EventPublisherService, MpcEventConsumerService, WithdrawalEventConsumerService } from './kafka';
import { EvmProviderAdapter, AddressDerivationAdapter, MnemonicDerivationAdapter, RecoveryMnemonicAdapter, BlockScannerService } from './blockchain';
import { MpcSigningClient } from './mpc';
import { DomainModule } from '@/domain/domain.module';
import {
DEPOSIT_TRANSACTION_REPOSITORY,
@ -21,7 +23,7 @@ import {
@Global()
@Module({
imports: [DomainModule],
imports: [DomainModule, HttpModule],
providers: [
// 核心服务
PrismaService,
@ -29,6 +31,7 @@ import {
EventPublisherService,
MpcEventConsumerService,
WithdrawalEventConsumerService,
MpcSigningClient,
// 区块链适配器
EvmProviderAdapter,
@ -68,6 +71,7 @@ import {
EventPublisherService,
MpcEventConsumerService,
WithdrawalEventConsumerService,
MpcSigningClient,
EvmProviderAdapter,
AddressDerivationAdapter,
MnemonicDerivationAdapter,

View File

@ -0,0 +1 @@
export * from './mpc-signing.client';

View File

@ -0,0 +1,187 @@
/**
* MPC Signing Client
*
* mpc-service MPC
* ERC20
*/
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
export interface CreateSigningInput {
username: string;
messageHash: string;
}
export interface SigningResult {
sessionId: string;
status: string;
signature?: string;
}
@Injectable()
export class MpcSigningClient {
private readonly logger = new Logger(MpcSigningClient.name);
private readonly mpcServiceUrl: string;
private readonly hotWalletUsername: string;
private readonly hotWalletAddress: string;
private readonly pollingIntervalMs: number = 2000;
private readonly maxPollingAttempts: number = 150; // 5 minutes max
constructor(
private readonly configService: ConfigService,
private readonly httpService: HttpService,
) {
this.mpcServiceUrl = this.configService.get<string>('MPC_SERVICE_URL', 'http://localhost:3013');
this.hotWalletUsername = this.configService.get<string>('HOT_WALLET_USERNAME', '');
this.hotWalletAddress = this.configService.get<string>('HOT_WALLET_ADDRESS', '');
if (!this.hotWalletUsername) {
this.logger.warn('[INIT] HOT_WALLET_USERNAME not configured');
}
if (!this.hotWalletAddress) {
this.logger.warn('[INIT] HOT_WALLET_ADDRESS not configured');
}
this.logger.log(`[INIT] MPC Service URL: ${this.mpcServiceUrl}`);
this.logger.log(`[INIT] Hot Wallet Username: ${this.hotWalletUsername || '(not configured)'}`);
this.logger.log(`[INIT] Hot Wallet Address: ${this.hotWalletAddress || '(not configured)'}`);
}
/**
*
*/
isConfigured(): boolean {
return !!this.hotWalletUsername && !!this.hotWalletAddress;
}
/**
*
*/
getHotWalletAddress(): string {
return this.hotWalletAddress;
}
/**
*
*/
getHotWalletUsername(): string {
return this.hotWalletUsername;
}
/**
* MPC
*/
async createSigningSession(messageHash: string): Promise<{ sessionId: string; status: string }> {
this.logger.log(`[SIGN] Creating signing session for messageHash: ${messageHash.slice(0, 16)}...`);
if (!this.hotWalletUsername) {
throw new Error('Hot wallet username not configured');
}
const response = await firstValueFrom(
this.httpService.post<{
sessionId: string;
status: string;
}>(
`${this.mpcServiceUrl}/mpc/sign`,
{
username: this.hotWalletUsername,
messageHash,
},
{
headers: { 'Content-Type': 'application/json' },
timeout: 30000,
},
),
);
this.logger.log(`[SIGN] Session created: ${response.data.sessionId}`);
return {
sessionId: response.data.sessionId,
status: response.data.status,
};
}
/**
*
*/
async getSigningStatus(sessionId: string): Promise<SigningResult> {
const response = await firstValueFrom(
this.httpService.get<{
sessionId: string;
status: string;
signature?: string;
}>(
`${this.mpcServiceUrl}/mpc/sign/${sessionId}/status`,
{
headers: { 'Content-Type': 'application/json' },
timeout: 10000,
},
),
);
return {
sessionId: response.data.sessionId,
status: response.data.status,
signature: response.data.signature,
};
}
/**
*
*
* @param messageHash (hex string with 0x prefix)
* @returns (hex string)
*/
async signMessage(messageHash: string): Promise<string> {
this.logger.log(`[SIGN] Starting MPC signing for: ${messageHash.slice(0, 16)}...`);
// Step 1: 创建签名会话
const session = await this.createSigningSession(messageHash);
this.logger.log(`[SIGN] Session ID: ${session.sessionId}`);
// Step 2: 轮询等待签名完成
const result = await this.pollForCompletion(session.sessionId);
if (result.status === 'completed' && result.signature) {
this.logger.log(`[SIGN] Signature obtained: ${result.signature.slice(0, 20)}...`);
return result.signature;
}
throw new Error(`MPC signing failed with status: ${result.status}`);
}
/**
*
*/
private async pollForCompletion(sessionId: string): Promise<SigningResult> {
for (let attempt = 0; attempt < this.maxPollingAttempts; attempt++) {
const result = await this.getSigningStatus(sessionId);
this.logger.debug(`[POLL] Session ${sessionId}: status=${result.status}, attempt=${attempt + 1}`);
if (result.status === 'completed') {
return result;
}
if (result.status === 'failed' || result.status === 'expired') {
return result;
}
// 等待下一次轮询
await this.sleep(this.pollingIntervalMs);
}
return {
sessionId,
status: 'timeout',
};
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

View File

@ -614,6 +614,10 @@ services:
# - BSC_RPC_URL=https://data-seed-prebsc-1-s1.binance.org:8545
# - BSC_CHAIN_ID=97
# - BSC_USDT_CONTRACT=0x337610d27c682E347C9cD60BD4b3b107C9d34dDd
# MPC Hot Wallet (用于提现转账)
- MPC_SERVICE_URL=http://rwa-mpc-service:3006
- HOT_WALLET_USERNAME=${HOT_WALLET_USERNAME:-}
- HOT_WALLET_ADDRESS=${HOT_WALLET_ADDRESS:-}
depends_on:
postgres:
condition: service_healthy