feat(mining-blockchain-service): 支持做市商独立MPC钱包签名

- MpcSigningClient 支持两个钱包: C2C Bot 和做市商
  - HOT_WALLET_USERNAME/ADDRESS: C2C Bot 热钱包
  - MARKET_MAKER_MPC_USERNAME/WALLET_ADDRESS: 做市商钱包
- Erc20TransferService 新增 transferTokenAsMarketMaker() 方法
- eUSDT/fUSDT 转账使用做市商钱包签名和转账
- 新增 /transfer/market-maker/status 状态检查接口

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-28 18:27:09 -08:00
parent 4283a369ae
commit cfdcd9352a
3 changed files with 358 additions and 30 deletions

View File

@ -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<TransferResponseDto> {
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<BalanceResponseDto> {
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<TransferResponseDto> {
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<BalanceResponseDto> {
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,
};
}
}

View File

@ -34,9 +34,14 @@ export type TokenType = 'DUSDT' | 'EUSDT' | 'FUSDT';
// MPC 签名客户端接口(避免循环依赖)
export interface IMpcSigningClient {
// C2C Bot 热钱包
isConfigured(): boolean;
getHotWalletAddress(): string;
signMessage(messageHash: string): Promise<string>;
// 做市商钱包
isMarketMakerConfigured(): boolean;
getMarketMakerAddress(): string;
signMessageAsMarketMaker(messageHash: string): Promise<string>;
}
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<ChainTypeEnum, JsonRpcProvider> = 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<string>('HOT_WALLET_ADDRESS', '');
this.marketMakerAddress = this.configService.get<string>('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<TransferResult> {
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<string> {
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);
}
}

View File

@ -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<sessionId, { resolve, reject, timeout }>
@ -55,18 +59,28 @@ export class MpcSigningClient implements OnModuleInit {
private readonly eventPublisher: EventPublisherService,
private readonly mpcEventConsumer: MpcEventConsumerService,
) {
// C2C Bot 热钱包配置
this.hotWalletUsername = this.configService.get<string>('HOT_WALLET_USERNAME', '');
this.hotWalletAddress = this.configService.get<string>('HOT_WALLET_ADDRESS', '');
// 做市商 MPC 钱包配置
this.marketMakerUsername = this.configService.get<string>('MARKET_MAKER_MPC_USERNAME', '');
this.marketMakerAddress = this.configService.get<string>('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<string> {
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<string> {
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<string> {
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);