diff --git a/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts b/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts index 6d85126f..d226e4b8 100644 --- a/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts +++ b/backend/services/mining-blockchain-service/src/api/controllers/transfer.controller.ts @@ -112,15 +112,16 @@ export class TransferController { }; } - // ============ eUSDT (积分股) 转账接口 ============ + // ============ eUSDT (积分股) 转账接口 - 使用做市商钱包 ============ @Post('eusdt') - @ApiOperation({ summary: '转账 eUSDT(积分股)到指定地址' }) + @ApiOperation({ summary: '转账 eUSDT(积分股)到指定地址 - 使用做市商钱包' }) @ApiResponse({ status: 200, description: '转账结果', type: TransferResponseDto }) @ApiResponse({ status: 400, description: '参数错误' }) @ApiResponse({ status: 500, description: '转账失败' }) async transferEusdt(@Body() dto: TransferDusdtDto): Promise { - const result: TransferResult = await this.erc20TransferService.transferToken( + // eUSDT 使用做市商钱包转账 + const result: TransferResult = await this.erc20TransferService.transferTokenAsMarketMaker( ChainTypeEnum.KAVA, 'EUSDT', dto.toAddress, @@ -137,11 +138,11 @@ export class TransferController { } @Get('eusdt/balance') - @ApiOperation({ summary: '查询热钱包 eUSDT(积分股)余额' }) + @ApiOperation({ summary: '查询做市商钱包 eUSDT(积分股)余额' }) @ApiResponse({ status: 200, description: '余额信息', type: BalanceResponseDto }) async getEusdtBalance(): Promise { - const address = this.erc20TransferService.getHotWalletAddress(ChainTypeEnum.KAVA); - const balance = await this.erc20TransferService.getTokenBalance(ChainTypeEnum.KAVA, 'EUSDT'); + const address = this.erc20TransferService.getMarketMakerAddress(ChainTypeEnum.KAVA); + const balance = await this.erc20TransferService.getMarketMakerTokenBalance(ChainTypeEnum.KAVA, 'EUSDT'); return { address: address || '', @@ -150,15 +151,16 @@ export class TransferController { }; } - // ============ fUSDT (积分值) 转账接口 ============ + // ============ fUSDT (积分值) 转账接口 - 使用做市商钱包 ============ @Post('fusdt') - @ApiOperation({ summary: '转账 fUSDT(积分值)到指定地址' }) + @ApiOperation({ summary: '转账 fUSDT(积分值)到指定地址 - 使用做市商钱包' }) @ApiResponse({ status: 200, description: '转账结果', type: TransferResponseDto }) @ApiResponse({ status: 400, description: '参数错误' }) @ApiResponse({ status: 500, description: '转账失败' }) async transferFusdt(@Body() dto: TransferDusdtDto): Promise { - const result: TransferResult = await this.erc20TransferService.transferToken( + // fUSDT 使用做市商钱包转账 + const result: TransferResult = await this.erc20TransferService.transferTokenAsMarketMaker( ChainTypeEnum.KAVA, 'FUSDT', dto.toAddress, @@ -175,11 +177,11 @@ export class TransferController { } @Get('fusdt/balance') - @ApiOperation({ summary: '查询热钱包 fUSDT(积分值)余额' }) + @ApiOperation({ summary: '查询做市商钱包 fUSDT(积分值)余额' }) @ApiResponse({ status: 200, description: '余额信息', type: BalanceResponseDto }) async getFusdtBalance(): Promise { - const address = this.erc20TransferService.getHotWalletAddress(ChainTypeEnum.KAVA); - const balance = await this.erc20TransferService.getTokenBalance(ChainTypeEnum.KAVA, 'FUSDT'); + const address = this.erc20TransferService.getMarketMakerAddress(ChainTypeEnum.KAVA); + const balance = await this.erc20TransferService.getMarketMakerTokenBalance(ChainTypeEnum.KAVA, 'FUSDT'); return { address: address || '', @@ -187,4 +189,19 @@ export class TransferController { chain: 'KAVA', }; } + + // ============ 服务状态接口 ============ + + @Get('market-maker/status') + @ApiOperation({ summary: '检查做市商转账服务状态' }) + @ApiResponse({ status: 200, description: '服务状态' }) + async getMarketMakerStatus(): Promise<{ configured: boolean; marketMakerAddress: string | null }> { + const configured = this.erc20TransferService.isMarketMakerConfigured(ChainTypeEnum.KAVA); + const marketMakerAddress = this.erc20TransferService.getMarketMakerAddress(ChainTypeEnum.KAVA); + + return { + configured, + marketMakerAddress, + }; + } } diff --git a/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts b/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts index 955d186c..1a8455cc 100644 --- a/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts +++ b/backend/services/mining-blockchain-service/src/domain/services/erc20-transfer.service.ts @@ -34,9 +34,14 @@ export type TokenType = 'DUSDT' | 'EUSDT' | 'FUSDT'; // MPC 签名客户端接口(避免循环依赖) export interface IMpcSigningClient { + // C2C Bot 热钱包 isConfigured(): boolean; getHotWalletAddress(): string; signMessage(messageHash: string): Promise; + // 做市商钱包 + isMarketMakerConfigured(): boolean; + getMarketMakerAddress(): string; + signMessageAsMarketMaker(messageHash: string): Promise; } export const MPC_SIGNING_CLIENT = Symbol('MPC_SIGNING_CLIENT'); @@ -51,7 +56,10 @@ export const MPC_SIGNING_CLIENT = Symbol('MPC_SIGNING_CLIENT'); export class Erc20TransferService { private readonly logger = new Logger(Erc20TransferService.name); private readonly providers: Map = new Map(); + // C2C Bot 热钱包地址 private readonly hotWalletAddress: string; + // 做市商钱包地址 + private readonly marketMakerAddress: string; private mpcSigningClient: IMpcSigningClient | null = null; constructor( @@ -59,6 +67,7 @@ export class Erc20TransferService { private readonly chainConfig: ChainConfigService, ) { this.hotWalletAddress = this.configService.get('HOT_WALLET_ADDRESS', ''); + this.marketMakerAddress = this.configService.get('MARKET_MAKER_WALLET_ADDRESS', ''); this.initializeProviders(); } @@ -85,20 +94,34 @@ export class Erc20TransferService { // 检查热钱包地址配置 if (this.hotWalletAddress) { - this.logger.log(`[INIT] Hot wallet address configured: ${this.hotWalletAddress}`); + this.logger.log(`[INIT] C2C Bot wallet address configured: ${this.hotWalletAddress}`); } else { - this.logger.warn('[INIT] HOT_WALLET_ADDRESS not configured, transfers will fail'); + this.logger.warn('[INIT] HOT_WALLET_ADDRESS not configured, C2C transfers will fail'); + } + + // 检查做市商钱包地址配置 + if (this.marketMakerAddress) { + this.logger.log(`[INIT] Market Maker wallet address configured: ${this.marketMakerAddress}`); + } else { + this.logger.warn('[INIT] MARKET_MAKER_WALLET_ADDRESS not configured, Market Maker transfers will fail'); } } /** - * 获取热钱包地址 + * 获取 C2C Bot 热钱包地址 */ getHotWalletAddress(chainType: ChainTypeEnum): string | null { // MPC 钱包地址在所有 EVM 链上相同 return this.hotWalletAddress || null; } + /** + * 获取做市商钱包地址 + */ + getMarketMakerAddress(chainType: ChainTypeEnum): string | null { + return this.marketMakerAddress || null; + } + /** * 获取热钱包 USDT 余额 */ @@ -553,11 +576,237 @@ export class Erc20TransferService { } /** - * 检查热钱包是否已配置 + * 检查 C2C Bot 热钱包是否已配置 */ isConfigured(chainType: ChainTypeEnum): boolean { return this.providers.has(chainType) && !!this.hotWalletAddress && !!this.mpcSigningClient?.isConfigured(); } + + /** + * 检查做市商钱包是否已配置 + */ + isMarketMakerConfigured(chainType: ChainTypeEnum): boolean { + return this.providers.has(chainType) && + !!this.marketMakerAddress && + !!this.mpcSigningClient?.isMarketMakerConfigured(); + } + + /** + * 做市商代币转账(使用做市商 MPC 钱包签名) + * + * @param chainType 链类型 (KAVA, BSC) + * @param tokenType 代币类型 (DUSDT, EUSDT, FUSDT) + * @param toAddress 接收地址 + * @param amount 转账金额 (人类可读格式,如 "100.5") + * @returns 转账结果 + */ + async transferTokenAsMarketMaker( + chainType: ChainTypeEnum, + tokenType: TokenType, + toAddress: string, + amount: string, + ): Promise { + const tokenName = tokenType === 'EUSDT' ? '积分股' : tokenType === 'FUSDT' ? '积分值' : 'dUSDT'; + this.logger.log(`[MM-TRANSFER] Starting Market Maker ${tokenType} (${tokenName}) transfer`); + this.logger.log(`[MM-TRANSFER] Chain: ${chainType}`); + this.logger.log(`[MM-TRANSFER] From: ${this.marketMakerAddress}`); + this.logger.log(`[MM-TRANSFER] To: ${toAddress}`); + this.logger.log(`[MM-TRANSFER] Amount: ${amount} ${tokenType}`); + + const provider = this.providers.get(chainType); + if (!provider) { + const error = `Provider not configured for chain: ${chainType}`; + this.logger.error(`[MM-TRANSFER] ${error}`); + return { success: false, error }; + } + + if (!this.mpcSigningClient || !this.mpcSigningClient.isMarketMakerConfigured()) { + const error = 'Market Maker MPC signing not configured'; + this.logger.error(`[MM-TRANSFER] ${error}`); + return { success: false, error }; + } + + if (!this.marketMakerAddress) { + const error = 'Market Maker wallet address not configured'; + this.logger.error(`[MM-TRANSFER] ${error}`); + return { success: false, error }; + } + + try { + const config = this.chainConfig.getConfig(ChainType.fromEnum(chainType)); + const contractAddress = this.getTokenContract(chainType, tokenType); + + if (!contractAddress) { + const error = `Token ${tokenType} not configured for chain ${chainType}`; + this.logger.error(`[MM-TRANSFER] ${error}`); + return { success: false, error }; + } + + const contract = new Contract(contractAddress, ERC20_TRANSFER_ABI, provider); + + // 获取代币精度 + const decimals = await contract.decimals(); + this.logger.log(`[MM-TRANSFER] Token decimals: ${decimals}`); + + // 转换金额 + const amountInWei = parseUnits(amount, decimals); + this.logger.log(`[MM-TRANSFER] Amount in wei: ${amountInWei.toString()}`); + + // 检查余额 + const balance = await contract.balanceOf(this.marketMakerAddress); + this.logger.log(`[MM-TRANSFER] Market Maker balance: ${formatUnits(balance, decimals)} ${tokenType}`); + + if (balance < amountInWei) { + const error = `Insufficient ${tokenType} balance in Market Maker wallet`; + this.logger.error(`[MM-TRANSFER] ${error}`); + return { success: false, error }; + } + + // 构建交易 + this.logger.log(`[MM-TRANSFER] Building transaction...`); + const nonce = await provider.getTransactionCount(this.marketMakerAddress); + const feeData = await provider.getFeeData(); + + // ERC20 transfer 的 calldata + const transferData = contract.interface.encodeFunctionData('transfer', [toAddress, amountInWei]); + + // 估算 gas + const gasEstimate = await provider.estimateGas({ + from: this.marketMakerAddress, + to: contractAddress, + data: transferData, + }); + + const gasLimit = gasEstimate * BigInt(120) / BigInt(100); // 增加 20% buffer + + // 检测链是否支持 EIP-1559 + const supportsEip1559 = feeData.maxFeePerGas && feeData.maxFeePerGas > BigInt(0); + + let tx: Transaction; + if (supportsEip1559) { + tx = Transaction.from({ + type: 2, + chainId: config.chainId, + nonce, + to: contractAddress, + data: transferData, + gasLimit, + maxFeePerGas: feeData.maxFeePerGas, + maxPriorityFeePerGas: feeData.maxPriorityFeePerGas, + }); + } else { + const gasPrice = feeData.gasPrice || BigInt(1000000000); + tx = Transaction.from({ + type: 0, + chainId: config.chainId, + nonce, + to: contractAddress, + data: transferData, + gasLimit, + gasPrice, + }); + } + + this.logger.log(`[MM-TRANSFER] Transaction built: nonce=${nonce}, gasLimit=${tx.gasLimit}`); + + // 获取交易哈希用于签名 + const unsignedTxHash = tx.unsignedHash; + this.logger.log(`[MM-TRANSFER] Unsigned tx hash: ${unsignedTxHash}`); + + // 使用做市商 MPC 钱包签名 + this.logger.log(`[MM-TRANSFER] Requesting Market Maker MPC signature...`); + const signatureHex = await this.mpcSigningClient.signMessageAsMarketMaker(unsignedTxHash); + this.logger.log(`[MM-TRANSFER] MPC signature obtained: ${signatureHex.slice(0, 20)}...`); + + // 解析签名 + const normalizedSig = signatureHex.startsWith('0x') ? signatureHex : `0x${signatureHex}`; + const sigBytes = normalizedSig.slice(2); + const r = `0x${sigBytes.slice(0, 64)}`; + const s = `0x${sigBytes.slice(64, 128)}`; + + // 尝试 yParity 0 和 1 来找到正确的 recovery id + let signature: Signature | null = null; + for (const yParity of [0, 1] as const) { + try { + const testSig = Signature.from({ r, s, yParity }); + const recoveredAddress = recoverAddress(unsignedTxHash, testSig); + + if (recoveredAddress.toLowerCase() === this.marketMakerAddress.toLowerCase()) { + this.logger.log(`[MM-TRANSFER] Found correct yParity: ${yParity}`); + signature = testSig; + break; + } + } catch (e) { + this.logger.debug(`[MM-TRANSFER] yParity=${yParity} failed: ${e}`); + } + } + + if (!signature) { + throw new Error('Failed to recover correct signature - address mismatch'); + } + + // 创建已签名交易 + const signedTx = tx.clone(); + signedTx.signature = signature; + + // 广播交易 + this.logger.log(`[MM-TRANSFER] Broadcasting transaction...`); + const txResponse = await provider.broadcastTransaction(signedTx.serialized); + this.logger.log(`[MM-TRANSFER] Transaction sent: ${txResponse.hash}`); + + // 等待确认 + this.logger.log(`[MM-TRANSFER] Waiting for confirmation...`); + const receipt = await txResponse.wait(); + + if (receipt && receipt.status === 1) { + this.logger.log(`[MM-TRANSFER] Transaction confirmed!`); + this.logger.log(`[MM-TRANSFER] Block: ${receipt.blockNumber}`); + this.logger.log(`[MM-TRANSFER] Gas used: ${receipt.gasUsed.toString()}`); + + return { + success: true, + txHash: txResponse.hash, + gasUsed: receipt.gasUsed.toString(), + blockNumber: receipt.blockNumber, + }; + } else { + const error = 'Transaction failed (reverted)'; + this.logger.error(`[MM-TRANSFER] ${error}`); + return { success: false, txHash: txResponse.hash, error }; + } + } catch (error: any) { + this.logger.error(`[MM-TRANSFER] Transfer failed:`, error); + return { + success: false, + error: error.message || 'Unknown error during transfer', + }; + } + } + + /** + * 获取做市商指定代币余额 + */ + async getMarketMakerTokenBalance(chainType: ChainTypeEnum, tokenType: TokenType): Promise { + const provider = this.providers.get(chainType); + if (!provider) { + throw new Error(`Provider not configured for chain: ${chainType}`); + } + + if (!this.marketMakerAddress) { + throw new Error('Market Maker wallet address not configured'); + } + + const contractAddress = this.getTokenContract(chainType, tokenType); + if (!contractAddress) { + throw new Error(`Token ${tokenType} not configured for chain ${chainType}`); + } + + const contract = new Contract(contractAddress, ERC20_TRANSFER_ABI, provider); + const balance = await contract.balanceOf(this.marketMakerAddress); + const decimals = await contract.decimals(); + + return formatUnits(balance, decimals); + } } diff --git a/backend/services/mining-blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts b/backend/services/mining-blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts index 7f4fcea2..c77ae758 100644 --- a/backend/services/mining-blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts +++ b/backend/services/mining-blockchain-service/src/infrastructure/mpc/mpc-signing.client.ts @@ -39,8 +39,12 @@ export const MPC_SIGNING_TOPIC = 'mpc.SigningRequested'; @Injectable() export class MpcSigningClient implements OnModuleInit { private readonly logger = new Logger(MpcSigningClient.name); + // C2C Bot 热钱包 private readonly hotWalletUsername: string; private readonly hotWalletAddress: string; + // 做市商 MPC 钱包 + private readonly marketMakerUsername: string; + private readonly marketMakerAddress: string; private readonly signingTimeoutMs: number = 300000; // 5 minutes // 待处理的签名请求回调 Map @@ -55,18 +59,28 @@ export class MpcSigningClient implements OnModuleInit { private readonly eventPublisher: EventPublisherService, private readonly mpcEventConsumer: MpcEventConsumerService, ) { + // C2C Bot 热钱包配置 this.hotWalletUsername = this.configService.get('HOT_WALLET_USERNAME', ''); this.hotWalletAddress = this.configService.get('HOT_WALLET_ADDRESS', ''); + // 做市商 MPC 钱包配置 + this.marketMakerUsername = this.configService.get('MARKET_MAKER_MPC_USERNAME', ''); + this.marketMakerAddress = this.configService.get('MARKET_MAKER_WALLET_ADDRESS', ''); if (!this.hotWalletUsername) { - this.logger.warn('[INIT] HOT_WALLET_USERNAME not configured'); + this.logger.warn('[INIT] HOT_WALLET_USERNAME not configured (C2C Bot disabled)'); } if (!this.hotWalletAddress) { - this.logger.warn('[INIT] HOT_WALLET_ADDRESS not configured'); + this.logger.warn('[INIT] HOT_WALLET_ADDRESS not configured (C2C Bot disabled)'); + } + if (!this.marketMakerUsername) { + this.logger.warn('[INIT] MARKET_MAKER_MPC_USERNAME not configured (Market Maker signing disabled)'); + } + if (!this.marketMakerAddress) { + this.logger.warn('[INIT] MARKET_MAKER_WALLET_ADDRESS not configured (Market Maker disabled)'); } - this.logger.log(`[INIT] Hot Wallet Username: ${this.hotWalletUsername || '(not configured)'}`); - this.logger.log(`[INIT] Hot Wallet Address: ${this.hotWalletAddress || '(not configured)'}`); + this.logger.log(`[INIT] C2C Bot Wallet: ${this.hotWalletAddress || '(not configured)'}`); + this.logger.log(`[INIT] Market Maker Wallet: ${this.marketMakerAddress || '(not configured)'}`); this.logger.log(`[INIT] Using Kafka event-driven signing`); } @@ -78,38 +92,86 @@ export class MpcSigningClient implements OnModuleInit { } /** - * 检查热钱包是否已配置 + * 检查 C2C Bot 热钱包是否已配置 */ isConfigured(): boolean { return !!this.hotWalletUsername && !!this.hotWalletAddress; } /** - * 获取热钱包地址 + * 检查做市商钱包是否已配置 + */ + isMarketMakerConfigured(): boolean { + return !!this.marketMakerUsername && !!this.marketMakerAddress; + } + + /** + * 获取 C2C Bot 热钱包地址 */ getHotWalletAddress(): string { return this.hotWalletAddress; } /** - * 获取热钱包用户名 + * 获取 C2C Bot 热钱包用户名 */ getHotWalletUsername(): string { return this.hotWalletUsername; } /** - * 签名消息(通过 Kafka 事件驱动) + * 获取做市商钱包地址 + */ + getMarketMakerAddress(): string { + return this.marketMakerAddress; + } + + /** + * 获取做市商 MPC 用户名 + */ + getMarketMakerUsername(): string { + return this.marketMakerUsername; + } + + /** + * 签名消息(使用 C2C Bot 热钱包,通过 Kafka 事件驱动) * * @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)}...`); - if (!this.hotWalletUsername) { throw new Error('Hot wallet username not configured'); } + return this.signMessageWithUsername(this.hotWalletUsername, messageHash); + } + + /** + * 使用做市商钱包签名消息 + * + * @param messageHash 要签名的消息哈希 (hex string with 0x prefix) + * @returns 签名结果 (hex string) + */ + async signMessageAsMarketMaker(messageHash: string): Promise { + if (!this.marketMakerUsername) { + throw new Error('Market maker MPC username not configured'); + } + return this.signMessageWithUsername(this.marketMakerUsername, messageHash); + } + + /** + * 使用指定用户名签名消息(通过 Kafka 事件驱动) + * + * @param username MPC 用户名 + * @param messageHash 要签名的消息哈希 (hex string with 0x prefix) + * @returns 签名结果 (hex string) + */ + async signMessageWithUsername(username: string, messageHash: string): Promise { + this.logger.log(`[SIGN] Starting MPC signing for: ${messageHash.slice(0, 16)}... (username: ${username})`); + + if (!username) { + throw new Error('MPC username not provided'); + } const sessionId = randomUUID(); this.logger.log(`[SIGN] Session ID: ${sessionId}`); @@ -132,16 +194,16 @@ export class MpcSigningClient implements OnModuleInit { eventType: 'blockchain.mpc.signing.requested', toPayload: () => ({ sessionId, - userId: 'system', // 系统热钱包 - username: this.hotWalletUsername, + userId: 'system', + username, messageHash, - source: 'blockchain-service', + source: 'mining-blockchain-service', }), eventId: sessionId, occurredAt: new Date(), }); - this.logger.log(`[SIGN] Signing request published to Kafka: sessionId=${sessionId}`); + this.logger.log(`[SIGN] Signing request published to Kafka: sessionId=${sessionId}, username=${username}`); } catch (error) { // 发布失败,清理待处理队列 const pending = this.pendingRequests.get(sessionId);