From 9cac91b5f0c2d5370efe52758047dcf42c4879ec Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 15 Dec 2025 08:04:17 -0800 Subject: [PATCH] =?UTF-8?q?feat(blockchain):=20=E5=B0=86=E6=8F=90=E7=8E=B0?= =?UTF-8?q?=E8=BD=AC=E8=B4=A6=E4=BB=8E=E7=A7=81=E9=92=A5=E7=AD=BE=E5=90=8D?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=20MPC=20=E7=AD=BE=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景: - 原实现使用 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 --- backend/scripts/init-hot-wallet.sh | 187 ++++++++++++++++++ backend/services/.env.example | 11 ++ .../services/blockchain-service/.env.example | 16 ++ .../blockchain-service/docker-compose.yml | 4 + .../src/application/application.module.ts | 2 + .../src/application/services/index.ts | 1 + .../mpc-transfer-initializer.service.ts | 26 +++ .../domain/services/erc20-transfer.service.ts | 169 ++++++++++++---- .../infrastructure/infrastructure.module.ts | 6 +- .../src/infrastructure/mpc/index.ts | 1 + .../infrastructure/mpc/mpc-signing.client.ts | 187 ++++++++++++++++++ backend/services/docker-compose.yml | 4 + 12 files changed, 572 insertions(+), 42 deletions(-) create mode 100644 backend/scripts/init-hot-wallet.sh create mode 100644 backend/services/blockchain-service/src/application/services/mpc-transfer-initializer.service.ts create mode 100644 backend/services/blockchain-service/src/infrastructure/mpc/index.ts create mode 100644 backend/services/blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts diff --git a/backend/scripts/init-hot-wallet.sh b/backend/scripts/init-hot-wallet.sh new file mode 100644 index 00000000..e90c23df --- /dev/null +++ b/backend/scripts/init-hot-wallet.sh @@ -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 "==============================================" diff --git a/backend/services/.env.example b/backend/services/.env.example index d40a7950..83f9acde 100644 --- a/backend/services/.env.example +++ b/backend/services/.env.example @@ -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= diff --git a/backend/services/blockchain-service/.env.example b/backend/services/blockchain-service/.env.example index b7f6fda0..ef95e72f 100644 --- a/backend/services/blockchain-service/.env.example +++ b/backend/services/blockchain-service/.env.example @@ -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 # ============================================================================= diff --git a/backend/services/blockchain-service/docker-compose.yml b/backend/services/blockchain-service/docker-compose.yml index 50a0ae9c..d593ad13 100644 --- a/backend/services/blockchain-service/docker-compose.yml +++ b/backend/services/blockchain-service/docker-compose.yml @@ -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 diff --git a/backend/services/blockchain-service/src/application/application.module.ts b/backend/services/blockchain-service/src/application/application.module.ts index dde4d075..e46655cd 100644 --- a/backend/services/blockchain-service/src/application/application.module.ts +++ b/backend/services/blockchain-service/src/application/application.module.ts @@ -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, diff --git a/backend/services/blockchain-service/src/application/services/index.ts b/backend/services/blockchain-service/src/application/services/index.ts index b6b792ed..56614ebf 100644 --- a/backend/services/blockchain-service/src/application/services/index.ts +++ b/backend/services/blockchain-service/src/application/services/index.ts @@ -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'; diff --git a/backend/services/blockchain-service/src/application/services/mpc-transfer-initializer.service.ts b/backend/services/blockchain-service/src/application/services/mpc-transfer-initializer.service.ts new file mode 100644 index 00000000..124c190d --- /dev/null +++ b/backend/services/blockchain-service/src/application/services/mpc-transfer-initializer.service.ts @@ -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'); + } +} diff --git a/backend/services/blockchain-service/src/domain/services/erc20-transfer.service.ts b/backend/services/blockchain-service/src/domain/services/erc20-transfer.service.ts index f8975d07..3578aa32 100644 --- a/backend/services/blockchain-service/src/domain/services/erc20-transfer.service.ts +++ b/backend/services/blockchain-service/src/domain/services/erc20-transfer.service.ts @@ -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; +} + +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 = new Map(); + private readonly providers: Map = 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('HOT_WALLET_ADDRESS', ''); + this.initializeProviders(); } - private initializeHotWallets(): void { - // 从环境变量获取热钱包私钥 - const hotWalletPrivateKey = this.configService.get('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 { - 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 { - 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(); } } diff --git a/backend/services/blockchain-service/src/infrastructure/infrastructure.module.ts b/backend/services/blockchain-service/src/infrastructure/infrastructure.module.ts index 50695888..00266b78 100644 --- a/backend/services/blockchain-service/src/infrastructure/infrastructure.module.ts +++ b/backend/services/blockchain-service/src/infrastructure/infrastructure.module.ts @@ -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, diff --git a/backend/services/blockchain-service/src/infrastructure/mpc/index.ts b/backend/services/blockchain-service/src/infrastructure/mpc/index.ts new file mode 100644 index 00000000..6c568d04 --- /dev/null +++ b/backend/services/blockchain-service/src/infrastructure/mpc/index.ts @@ -0,0 +1 @@ +export * from './mpc-signing.client'; diff --git a/backend/services/blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts b/backend/services/blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts new file mode 100644 index 00000000..31b58d75 --- /dev/null +++ b/backend/services/blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts @@ -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('MPC_SERVICE_URL', 'http://localhost:3013'); + this.hotWalletUsername = this.configService.get('HOT_WALLET_USERNAME', ''); + this.hotWalletAddress = this.configService.get('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 { + 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 { + 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 { + 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 { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} diff --git a/backend/services/docker-compose.yml b/backend/services/docker-compose.yml index 1c97a2b2..d826c846 100644 --- a/backend/services/docker-compose.yml +++ b/backend/services/docker-compose.yml @@ -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