# Blockchain Service 开发指南 ## 1. 服务概述 ### 1.1 服务定位 blockchain-service 是 RWA 榴莲皇后平台的区块链基础设施服务,负责: - **公钥→地址派生**:从 MPC 公钥派生多链钱包地址 (EVM/Cosmos) - **链上事件监听**:监听 ERC20 Transfer 事件,检测用户充值 - **充值入账触发**:检测到充值后通知 wallet-service 入账 - **余额查询**:查询链上 USDT/原生代币余额 - **交易广播**:提交签名后的交易到链上 - **地址管理**:管理平台充值地址池 ### 1.4 与其他服务的关系 ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ identity-service│ │ mpc-service │ │blockchain-service│ ├─────────────────┤ ├─────────────────┤ ├─────────────────┤ │ - 用户账户 │ │ - 密钥分片生成 │ │ - 公钥→地址派生 │ │ - 设备绑定 │ │ - 签名协调 │ │ - 充值检测 │ │ - KYC验证 │ │ - 分片存储 │ │ - 交易广播 │ │ - 身份认证 │ │ - 阈值策略 │ │ - 余额查询 │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │ │ │ MpcKeygenRequested │ KeygenCompleted │ │──────────────────────>│ (with publicKey) │ │ │──────────────────────>│ │ │ │ derive address │ WalletAddressCreated │ │<──────────────────────────────────────────────│ │ (存储 userId ↔ walletAddress 关联) │ │ │ │ │ wallet-service │ │ │ │ │ WalletAddressCreated │ │<──────────────────────────────────────────────│ │ (存储钱包地址、管理余额) │ ``` **职责边界原则**: - **identity-service**:只关心用户身份,不处理区块链技术细节 - **mpc-service**:只关心 MPC 协议,生成公钥后发布事件 - **blockchain-service**:封装所有区块链技术,包括地址派生、链交互 - **wallet-service**:管理用户钱包的业务逻辑,不直接与链交互 ### 1.2 技术栈 | 组件 | 技术选型 | |------|----------| | 框架 | NestJS 10.x | | 语言 | TypeScript 5.x | | 数据库 | PostgreSQL 15 + Prisma | | 消息队列 | Kafka | | 缓存 | Redis | | 区块链 | ethers.js 6.x | | 容器化 | Docker | ### 1.3 端口分配 - HTTP API: `3012` - 数据库: `rwa_blockchain` (共享 PostgreSQL) - Redis DB: `11` --- ## 2. 架构设计 ### 2.1 六边形架构 (Hexagonal Architecture) ``` ┌─────────────────────────────────────────────────────────────────────┐ │ blockchain-service │ ├─────────────────────────────────────────────────────────────────────┤ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ API Layer │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ Health Ctrl │ │ Balance Ctrl │ │ Internal Ctrl│ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Application Layer │ │ │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ │ │ DepositDetection │ │ BalanceQuery │ │ │ │ │ │ Service │ │ Service │ │ │ │ │ └──────────────────┘ └──────────────────┘ │ │ │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ │ │ TransactionBroad │ │ AddressRegistry │ │ │ │ │ │ castService │ │ Service │ │ │ │ │ └──────────────────┘ └──────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Domain Layer │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ │ │ Aggregates │ │ │ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │ │ │ │ │ │ │DepositTx │ │MonitoredAddr│ │TransactionRequest │ │ │ │ │ │ │ └────────────┘ └────────────┘ └────────────────────┘ │ │ │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ │ │ Domain Events │ │ │ │ │ │ DepositDetected, TransactionBroadcasted, BlockScanned │ │ │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ │ │ Repository Interfaces (Ports) │ │ │ │ │ │ IDepositTxRepository, IMonitoredAddressRepository │ │ │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ Infrastructure Layer │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ Prisma Repos │ │ Kafka │ │ Redis Cache │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ EVM Provider │ │ Event │ │ Block │ │ │ │ │ │ Adapter │ │ Listener │ │ Scanner │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ ``` ### 2.2 目录结构 ``` blockchain-service/ ├── prisma/ │ └── schema.prisma ├── src/ │ ├── modules/ # NestJS 模块定义 (与其他服务保持一致) │ │ ├── api.module.ts # API 层模块 │ │ ├── application.module.ts # 应用层模块 │ │ ├── domain.module.ts # 领域层模块 │ │ └── infrastructure.module.ts # 基础设施层模块 │ │ │ ├── api/ # 入站适配器 (Driving Adapters) │ │ ├── controllers/ │ │ │ ├── health.controller.ts │ │ │ ├── balance.controller.ts │ │ │ └── internal.controller.ts │ │ └── dto/ │ │ ├── request/ │ │ │ └── query-balance.dto.ts │ │ └── response/ │ │ └── balance.dto.ts │ │ │ ├── application/ # 应用层 (Use Cases) │ │ ├── services/ │ │ │ ├── deposit-detection.service.ts │ │ │ ├── balance-query.service.ts │ │ │ ├── transaction-broadcast.service.ts │ │ │ ├── address-registry.service.ts │ │ │ └── address-derivation.service.ts # 公钥→地址派生 │ │ ├── commands/ │ │ │ ├── register-address/ │ │ │ │ ├── register-address.command.ts │ │ │ │ └── register-address.handler.ts │ │ │ └── broadcast-transaction/ │ │ │ ├── broadcast-transaction.command.ts │ │ │ └── broadcast-transaction.handler.ts │ │ └── queries/ │ │ ├── get-balance/ │ │ │ ├── get-balance.query.ts │ │ │ └── get-balance.handler.ts │ │ └── get-deposit-history/ │ │ ├── get-deposit-history.query.ts │ │ └── get-deposit-history.handler.ts │ │ │ ├── domain/ # 领域层 (核心业务) │ │ ├── aggregates/ │ │ │ ├── aggregate-root.base.ts # 聚合根基类 (统一事件管理) │ │ │ ├── deposit-transaction/ │ │ │ │ ├── deposit-transaction.aggregate.ts │ │ │ │ ├── deposit-transaction.factory.ts │ │ │ │ └── index.ts │ │ │ ├── monitored-address/ │ │ │ │ ├── monitored-address.aggregate.ts │ │ │ │ └── index.ts │ │ │ └── transaction-request/ │ │ │ ├── transaction-request.aggregate.ts │ │ │ └── index.ts │ │ ├── entities/ │ │ │ └── block-checkpoint.entity.ts │ │ ├── events/ │ │ │ ├── domain-event.base.ts │ │ │ ├── deposit-detected.event.ts │ │ │ ├── deposit-confirmed.event.ts │ │ │ ├── transaction-broadcasted.event.ts │ │ │ ├── wallet-address-created.event.ts # 地址派生完成事件 │ │ │ └── index.ts │ │ ├── repositories/ │ │ │ ├── deposit-transaction.repository.interface.ts │ │ │ ├── monitored-address.repository.interface.ts │ │ │ ├── block-checkpoint.repository.interface.ts │ │ │ └── index.ts │ │ ├── services/ │ │ │ ├── confirmation-policy.service.ts │ │ │ └── chain-config.service.ts │ │ ├── value-objects/ │ │ │ ├── chain-type.vo.ts │ │ │ ├── tx-hash.vo.ts │ │ │ ├── evm-address.vo.ts │ │ │ ├── token-amount.vo.ts │ │ │ ├── block-number.vo.ts │ │ │ └── index.ts │ │ └── enums/ │ │ ├── deposit-status.enum.ts │ │ ├── chain-type.enum.ts │ │ └── index.ts │ │ │ ├── infrastructure/ # 出站适配器 (Driven Adapters) │ │ ├── blockchain/ │ │ │ ├── evm-provider.adapter.ts │ │ │ ├── event-listener.service.ts │ │ │ ├── block-scanner.service.ts │ │ │ ├── transaction-sender.service.ts │ │ │ ├── address-derivation.adapter.ts # 地址派生适配器 │ │ │ └── blockchain.module.ts │ │ ├── persistence/ │ │ │ ├── prisma/ │ │ │ │ └── prisma.service.ts │ │ │ ├── repositories/ │ │ │ │ ├── deposit-transaction.repository.impl.ts │ │ │ │ ├── monitored-address.repository.impl.ts │ │ │ │ └── block-checkpoint.repository.impl.ts │ │ │ └── mappers/ │ │ │ ├── deposit-transaction.mapper.ts │ │ │ ├── monitored-address.mapper.ts │ │ │ └── transaction-request.mapper.ts │ │ ├── kafka/ │ │ │ ├── event-publisher.service.ts │ │ │ ├── event-consumer.controller.ts │ │ │ └── kafka.module.ts │ │ ├── redis/ │ │ │ ├── redis.service.ts │ │ │ ├── address-cache.service.ts │ │ │ └── redis.module.ts │ │ ├── external/ │ │ │ ├── wallet-service/ │ │ │ │ └── wallet-client.service.ts │ │ │ └── identity-service/ │ │ │ └── identity-client.service.ts │ │ └── infrastructure.module.ts │ │ │ ├── config/ │ │ ├── app.config.ts │ │ ├── database.config.ts │ │ ├── kafka.config.ts │ │ ├── redis.config.ts │ │ ├── chain.config.ts │ │ └── index.ts │ │ │ ├── shared/ │ │ ├── decorators/ │ │ │ ├── public.decorator.ts │ │ │ └── index.ts │ │ ├── exceptions/ │ │ │ ├── domain.exception.ts │ │ │ ├── blockchain.exception.ts │ │ │ └── index.ts │ │ ├── filters/ │ │ │ ├── global-exception.filter.ts │ │ │ └── domain-exception.filter.ts │ │ └── interceptors/ │ │ └── transform.interceptor.ts │ │ │ ├── app.module.ts │ └── main.ts │ ├── test/ │ ├── unit/ │ ├── integration/ │ └── e2e/ │ ├── .env.example ├── Dockerfile ├── docker-compose.yml ├── package.json ├── tsconfig.json ├── nest-cli.json └── DEVELOPMENT_GUIDE.md ``` ### 2.3 模块化设计 (与其他服务保持一致) 为了与 identity-service、wallet-service、leaderboard-service 等服务保持架构一致性,采用分层模块化设计: ```typescript // src/modules/infrastructure.module.ts import { Global, Module } from '@nestjs/common'; import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.service'; import { RedisService } from '@/infrastructure/redis/redis.service'; import { EvmProviderAdapter } from '@/infrastructure/blockchain/evm-provider.adapter'; import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; import { WalletClientService } from '@/infrastructure/external/wallet-service/wallet-client.service'; import { IdentityClientService } from '@/infrastructure/external/identity-service/identity-client.service'; @Global() @Module({ providers: [ PrismaService, RedisService, EvmProviderAdapter, EventPublisherService, WalletClientService, IdentityClientService, ], exports: [ PrismaService, RedisService, EvmProviderAdapter, EventPublisherService, WalletClientService, IdentityClientService, ], }) export class InfrastructureModule {} ``` ```typescript // src/modules/domain.module.ts import { Module } from '@nestjs/common'; import { InfrastructureModule } from './infrastructure.module'; import { DEPOSIT_TRANSACTION_REPOSITORY, MONITORED_ADDRESS_REPOSITORY, BLOCK_CHECKPOINT_REPOSITORY, } from '@/domain/repositories'; import { DepositTransactionRepositoryImpl } from '@/infrastructure/persistence/repositories/deposit-transaction.repository.impl'; import { MonitoredAddressRepositoryImpl } from '@/infrastructure/persistence/repositories/monitored-address.repository.impl'; import { BlockCheckpointRepositoryImpl } from '@/infrastructure/persistence/repositories/block-checkpoint.repository.impl'; import { ConfirmationPolicyService } from '@/domain/services/confirmation-policy.service'; import { ChainConfigService } from '@/domain/services/chain-config.service'; @Module({ imports: [InfrastructureModule], providers: [ // 仓储实现 (依赖倒置) { provide: DEPOSIT_TRANSACTION_REPOSITORY, useClass: DepositTransactionRepositoryImpl, }, { provide: MONITORED_ADDRESS_REPOSITORY, useClass: MonitoredAddressRepositoryImpl, }, { provide: BLOCK_CHECKPOINT_REPOSITORY, useClass: BlockCheckpointRepositoryImpl, }, // 领域服务 ConfirmationPolicyService, ChainConfigService, ], exports: [ DEPOSIT_TRANSACTION_REPOSITORY, MONITORED_ADDRESS_REPOSITORY, BLOCK_CHECKPOINT_REPOSITORY, ConfirmationPolicyService, ChainConfigService, ], }) export class DomainModule {} ``` ```typescript // src/modules/application.module.ts import { Module } from '@nestjs/common'; import { DomainModule } from './domain.module'; import { InfrastructureModule } from './infrastructure.module'; import { DepositDetectionService } from '@/application/services/deposit-detection.service'; import { BalanceQueryService } from '@/application/services/balance-query.service'; import { TransactionBroadcastService } from '@/application/services/transaction-broadcast.service'; import { AddressRegistryService } from '@/application/services/address-registry.service'; import { AddressCacheService } from '@/infrastructure/redis/address-cache.service'; import { EventListenerService } from '@/infrastructure/blockchain/event-listener.service'; import { BlockScannerService } from '@/infrastructure/blockchain/block-scanner.service'; @Module({ imports: [DomainModule, InfrastructureModule], providers: [ // 应用服务 DepositDetectionService, BalanceQueryService, TransactionBroadcastService, AddressRegistryService, // 基础设施服务 (需要应用层依赖) AddressCacheService, EventListenerService, BlockScannerService, ], exports: [ DepositDetectionService, BalanceQueryService, TransactionBroadcastService, AddressRegistryService, AddressCacheService, ], }) export class ApplicationModule {} ``` ```typescript // src/modules/api.module.ts import { Module } from '@nestjs/common'; import { ApplicationModule } from './application.module'; import { HealthController } from '@/api/controllers/health.controller'; import { BalanceController } from '@/api/controllers/balance.controller'; import { InternalController } from '@/api/controllers/internal.controller'; import { EventConsumerController } from '@/infrastructure/kafka/event-consumer.controller'; @Module({ imports: [ApplicationModule], controllers: [ HealthController, BalanceController, InternalController, EventConsumerController, ], }) export class ApiModule {} ``` ```typescript // src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; import { ApiModule } from '@/modules/api.module'; import appConfig from '@/config/app.config'; import databaseConfig from '@/config/database.config'; import kafkaConfig from '@/config/kafka.config'; import redisConfig from '@/config/redis.config'; import chainConfig from '@/config/chain.config'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, load: [appConfig, databaseConfig, kafkaConfig, redisConfig, chainConfig], }), ScheduleModule.forRoot(), ApiModule, ], }) export class AppModule {} ``` **模块依赖关系图:** ``` ┌─────────────────────────────────────────────────────────────┐ │ AppModule │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ ApiModule │ │ │ │ Controllers: Health, Balance, Internal, EventConsumer │ │ │ └───────────────────────┬───────────────────────────────┘ │ │ │ imports │ │ ┌───────────────────────▼───────────────────────────────┐ │ │ │ ApplicationModule │ │ │ │ Services: DepositDetection, BalanceQuery, ... │ │ │ └───────────────────────┬───────────────────────────────┘ │ │ │ imports │ │ ┌───────────────────────▼───────────────────────────────┐ │ │ │ DomainModule │ │ │ │ Repositories (interfaces → impl), Domain Services │ │ │ └───────────────────────┬───────────────────────────────┘ │ │ │ imports │ │ ┌───────────────────────▼───────────────────────────────┐ │ │ │ InfrastructureModule (@Global) │ │ │ │ Prisma, Redis, Kafka, EVM Provider, External Clients │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 3. 数据模型设计 ### 3.1 Prisma Schema ```prisma // prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ============================================ // 监控地址表 // 存储需要监听充值的地址 // ============================================ model MonitoredAddress { id BigInt @id @default(autoincrement()) @map("address_id") chainType String @map("chain_type") @db.VarChar(20) // KAVA, BSC address String @db.VarChar(42) // 0x地址 userId BigInt @map("user_id") // 关联用户ID isActive Boolean @default(true) @map("is_active") // 是否激活监听 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deposits DepositTransaction[] @@unique([chainType, address], name: "uk_chain_address") @@index([userId], name: "idx_user") @@index([chainType, isActive], name: "idx_chain_active") @@map("monitored_addresses") } // ============================================ // 充值交易表 (Append-Only) // 记录检测到的所有充值交易 // ============================================ model DepositTransaction { id BigInt @id @default(autoincrement()) @map("deposit_id") chainType String @map("chain_type") @db.VarChar(20) txHash String @unique @map("tx_hash") @db.VarChar(66) fromAddress String @map("from_address") @db.VarChar(42) toAddress String @map("to_address") @db.VarChar(42) tokenContract String @map("token_contract") @db.VarChar(42) // USDT合约地址 amount Decimal @db.Decimal(36, 18) // 原始金额 amountFormatted Decimal @map("amount_formatted") @db.Decimal(20, 8) // 格式化金额 blockNumber BigInt @map("block_number") blockTimestamp DateTime @map("block_timestamp") logIndex Int @map("log_index") // 确认状态 confirmations Int @default(0) status String @default("DETECTED") @db.VarChar(20) // DETECTED, CONFIRMING, CONFIRMED, NOTIFIED // 关联 addressId BigInt @map("address_id") userId BigInt @map("user_id") // 通知状态 notifiedAt DateTime? @map("notified_at") notifyAttempts Int @default(0) @map("notify_attempts") lastNotifyError String? @map("last_notify_error") @db.Text createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") monitoredAddress MonitoredAddress @relation(fields: [addressId], references: [id]) @@index([chainType, status], name: "idx_chain_status") @@index([userId], name: "idx_deposit_user") @@index([blockNumber], name: "idx_block") @@index([status, notifiedAt], name: "idx_pending_notify") @@map("deposit_transactions") } // ============================================ // 区块扫描检查点 (每条链一条记录) // 记录扫描进度,用于断点续扫 // ============================================ model BlockCheckpoint { id BigInt @id @default(autoincrement()) @map("checkpoint_id") chainType String @unique @map("chain_type") @db.VarChar(20) lastScannedBlock BigInt @map("last_scanned_block") lastScannedAt DateTime @map("last_scanned_at") // 健康状态 isHealthy Boolean @default(true) @map("is_healthy") lastError String? @map("last_error") @db.Text createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("block_checkpoints") } // ============================================ // 交易广播请求表 // 记录待广播和已广播的交易 // ============================================ model TransactionRequest { id BigInt @id @default(autoincrement()) @map("request_id") chainType String @map("chain_type") @db.VarChar(20) // 请求来源 sourceService String @map("source_service") @db.VarChar(50) sourceOrderId String @map("source_order_id") @db.VarChar(100) // 交易数据 fromAddress String @map("from_address") @db.VarChar(42) toAddress String @map("to_address") @db.VarChar(42) value Decimal @db.Decimal(36, 18) data String? @db.Text // 合约调用数据 // 签名数据 (由 MPC 服务提供) signedTx String? @map("signed_tx") @db.Text // 广播结果 txHash String? @map("tx_hash") @db.VarChar(66) status String @default("PENDING") @db.VarChar(20) // PENDING, SIGNED, BROADCASTED, CONFIRMED, FAILED // Gas 信息 gasLimit BigInt? @map("gas_limit") gasPrice Decimal? @map("gas_price") @db.Decimal(36, 18) nonce Int? // 错误信息 errorMessage String? @map("error_message") @db.Text retryCount Int @default(0) @map("retry_count") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@unique([sourceService, sourceOrderId], name: "uk_source_order") @@index([chainType, status], name: "idx_tx_chain_status") @@index([txHash], name: "idx_tx_hash") @@map("transaction_requests") } // ============================================ // 区块链事件日志 (Append-Only 审计) // ============================================ model BlockchainEvent { id BigInt @id @default(autoincrement()) @map("event_id") eventType String @map("event_type") @db.VarChar(50) aggregateId String @map("aggregate_id") @db.VarChar(100) aggregateType String @map("aggregate_type") @db.VarChar(50) eventData Json @map("event_data") chainType String? @map("chain_type") @db.VarChar(20) txHash String? @map("tx_hash") @db.VarChar(66) occurredAt DateTime @default(now()) @map("occurred_at") @db.Timestamp(6) @@index([aggregateType, aggregateId], name: "idx_event_aggregate") @@index([eventType], name: "idx_event_type") @@index([chainType], name: "idx_event_chain") @@index([occurredAt], name: "idx_event_occurred") @@map("blockchain_events") } ``` --- ## 4. 领域层设计 ### 4.1 聚合根基类 (与其他服务保持一致) 为了与 authorization-service、wallet-service 等服务保持架构一致性,抽取统一的聚合根基类: ```typescript // src/domain/aggregates/aggregate-root.base.ts import { DomainEvent } from '@/domain/events/domain-event.base'; /** * 聚合根基类 * * 所有聚合根都应继承此基类,统一管理领域事件的收集和清理。 * 参考: authorization-service/src/domain/aggregates/aggregate-root.base.ts */ export abstract class AggregateRoot { private readonly _domainEvents: DomainEvent[] = []; /** * 聚合根唯一标识 */ abstract get id(): TId | undefined; /** * 获取所有待发布的领域事件 */ get domainEvents(): ReadonlyArray { return [...this._domainEvents]; } /** * 添加领域事件 * @param event 领域事件 */ protected addDomainEvent(event: DomainEvent): void { this._domainEvents.push(event); } /** * 清空领域事件(在事件发布后调用) */ clearDomainEvents(): void { this._domainEvents.length = 0; } /** * 检查是否有待发布的领域事件 */ hasDomainEvents(): boolean { return this._domainEvents.length > 0; } } ``` ### 4.2 聚合根:DepositTransaction ```typescript // src/domain/aggregates/deposit-transaction/deposit-transaction.aggregate.ts import { AggregateRoot } from '../aggregate-root.base'; import { DomainEvent, DepositDetectedEvent, DepositConfirmedEvent } from '@/domain/events'; import { ChainType, TxHash, EvmAddress, TokenAmount, BlockNumber } from '@/domain/value-objects'; import { DepositStatus } from '@/domain/enums'; export interface DepositTransactionProps { id?: bigint; chainType: ChainType; txHash: TxHash; fromAddress: EvmAddress; toAddress: EvmAddress; tokenContract: EvmAddress; amount: TokenAmount; blockNumber: BlockNumber; blockTimestamp: Date; logIndex: number; confirmations: number; status: DepositStatus; addressId: bigint; userId: bigint; notifiedAt?: Date; notifyAttempts: number; lastNotifyError?: string; createdAt?: Date; updatedAt?: Date; } export class DepositTransaction extends AggregateRoot { private constructor(private props: DepositTransactionProps) { super(); } // Getters get id(): bigint | undefined { return this.props.id; } get chainType(): ChainType { return this.props.chainType; } get txHash(): TxHash { return this.props.txHash; } get fromAddress(): EvmAddress { return this.props.fromAddress; } get toAddress(): EvmAddress { return this.props.toAddress; } get amount(): TokenAmount { return this.props.amount; } get blockNumber(): BlockNumber { return this.props.blockNumber; } get confirmations(): number { return this.props.confirmations; } get status(): DepositStatus { return this.props.status; } get userId(): bigint { return this.props.userId; } get isConfirmed(): boolean { return this.props.status === DepositStatus.CONFIRMED; } get isNotified(): boolean { return this.props.status === DepositStatus.NOTIFIED; } get domainEvents(): DomainEvent[] { return [...this._domainEvents]; } /** * 创建新的充值交易(检测到时) */ static create(params: { chainType: ChainType; txHash: TxHash; fromAddress: EvmAddress; toAddress: EvmAddress; tokenContract: EvmAddress; amount: TokenAmount; blockNumber: BlockNumber; blockTimestamp: Date; logIndex: number; addressId: bigint; userId: bigint; }): DepositTransaction { const deposit = new DepositTransaction({ ...params, confirmations: 1, status: DepositStatus.DETECTED, notifyAttempts: 0, }); deposit.addDomainEvent(new DepositDetectedEvent({ chainType: params.chainType.value, txHash: params.txHash.value, fromAddress: params.fromAddress.value, toAddress: params.toAddress.value, amount: params.amount.formatted, userId: params.userId.toString(), blockNumber: params.blockNumber.value, })); return deposit; } /** * 从数据库重建 */ static reconstruct(props: DepositTransactionProps): DepositTransaction { return new DepositTransaction(props); } /** * 更新确认数 */ updateConfirmations(newConfirmations: number, requiredConfirmations: number): void { this.props.confirmations = newConfirmations; if (newConfirmations >= requiredConfirmations && this.props.status === DepositStatus.DETECTED) { this.props.status = DepositStatus.CONFIRMING; } if (newConfirmations >= requiredConfirmations && this.props.status === DepositStatus.CONFIRMING) { this.confirm(); } } /** * 确认充值 */ private confirm(): void { if (this.props.status === DepositStatus.CONFIRMED) { return; } this.props.status = DepositStatus.CONFIRMED; this.addDomainEvent(new DepositConfirmedEvent({ depositId: this.props.id!.toString(), chainType: this.props.chainType.value, txHash: this.props.txHash.value, amount: this.props.amount.formatted, userId: this.props.userId.toString(), confirmations: this.props.confirmations, })); } /** * 标记已通知 */ markNotified(): void { this.props.status = DepositStatus.NOTIFIED; this.props.notifiedAt = new Date(); } /** * 记录通知失败 */ recordNotifyFailure(error: string): void { this.props.notifyAttempts++; this.props.lastNotifyError = error; } // 注意:addDomainEvent 和 clearDomainEvents 方法已在 AggregateRoot 基类中定义 toProps(): DepositTransactionProps { return { ...this.props }; } } ``` ### 4.3 值对象 #### 4.3.1 EVM 地址值对象 ```typescript // src/domain/value-objects/evm-address.vo.ts import { ethers } from 'ethers'; export class EvmAddress { private constructor(private readonly _value: string) {} get value(): string { return this._value; } get checksummed(): string { return ethers.getAddress(this._value); } static create(address: string): EvmAddress { if (!ethers.isAddress(address)) { throw new Error(`Invalid EVM address: ${address}`); } return new EvmAddress(ethers.getAddress(address)); // 标准化为 checksum 格式 } equals(other: EvmAddress): boolean { return this._value.toLowerCase() === other._value.toLowerCase(); } toString(): string { return this._value; } } ``` #### 4.3.2 代币金额值对象 ```typescript // src/domain/value-objects/token-amount.vo.ts import Decimal from 'decimal.js'; export class TokenAmount { private constructor( private readonly _raw: Decimal, // 链上原始值 (wei) private readonly _decimals: number, // 代币精度 ) {} get raw(): Decimal { return this._raw; } get decimals(): number { return this._decimals; } get formatted(): string { return this._raw.div(new Decimal(10).pow(this._decimals)).toFixed(8); } get value(): number { return parseFloat(this.formatted); } static create(raw: string | bigint, decimals: number): TokenAmount { return new TokenAmount(new Decimal(raw.toString()), decimals); } static fromFormatted(amount: string, decimals: number): TokenAmount { const raw = new Decimal(amount).mul(new Decimal(10).pow(decimals)); return new TokenAmount(raw, decimals); } isZero(): boolean { return this._raw.isZero(); } greaterThan(other: TokenAmount): boolean { return this._raw.greaterThan(other._raw); } } ``` #### 4.3.3 链类型值对象 ```typescript // src/domain/value-objects/chain-type.vo.ts export enum ChainTypeEnum { KAVA = 'KAVA', BSC = 'BSC', } export class ChainType { private constructor(private readonly _value: ChainTypeEnum) {} get value(): ChainTypeEnum { return this._value; } get name(): string { switch (this._value) { case ChainTypeEnum.KAVA: return 'KAVA EVM'; case ChainTypeEnum.BSC: return 'BSC'; } } get requiredConfirmations(): number { switch (this._value) { case ChainTypeEnum.KAVA: return 12; case ChainTypeEnum.BSC: return 15; } } get blockTime(): number { switch (this._value) { case ChainTypeEnum.KAVA: return 6; // 约 6 秒 case ChainTypeEnum.BSC: return 3; // 约 3 秒 } } static create(value: string): ChainType { if (!Object.values(ChainTypeEnum).includes(value as ChainTypeEnum)) { throw new Error(`Invalid chain type: ${value}`); } return new ChainType(value as ChainTypeEnum); } static KAVA(): ChainType { return new ChainType(ChainTypeEnum.KAVA); } static BSC(): ChainType { return new ChainType(ChainTypeEnum.BSC); } equals(other: ChainType): boolean { return this._value === other._value; } } ``` #### 4.3.4 区块号值对象 ```typescript // src/domain/value-objects/block-number.vo.ts /** * 区块号值对象 * * 封装区块号,确保类型安全和业务规则验证 */ export class BlockNumber { private constructor(private readonly _value: bigint) {} get value(): bigint { return this._value; } /** * 创建区块号值对象 * @param value 区块号(支持 number 或 bigint) * @throws Error 如果区块号为负数 */ static create(value: number | bigint): BlockNumber { const bn = BigInt(value); if (bn < 0n) { throw new Error(`Invalid block number: ${value}. Block number cannot be negative.`); } return new BlockNumber(bn); } /** * 计算两个区块之间的差值 */ diff(other: BlockNumber): bigint { return this._value - other._value; } /** * 检查是否大于另一个区块号 */ greaterThan(other: BlockNumber): boolean { return this._value > other._value; } /** * 检查是否小于另一个区块号 */ lessThan(other: BlockNumber): boolean { return this._value < other._value; } /** * 加上指定数量的区块 */ add(blocks: number | bigint): BlockNumber { return new BlockNumber(this._value + BigInt(blocks)); } /** * 减去指定数量的区块 */ subtract(blocks: number | bigint): BlockNumber { const result = this._value - BigInt(blocks); if (result < 0n) { throw new Error('Block number cannot be negative after subtraction'); } return new BlockNumber(result); } equals(other: BlockNumber): boolean { return this._value === other._value; } toString(): string { return this._value.toString(); } toNumber(): number { return Number(this._value); } } ``` #### 4.3.5 交易哈希值对象 ```typescript // src/domain/value-objects/tx-hash.vo.ts /** * 交易哈希值对象 * * 封装 EVM 交易哈希,确保格式正确(0x + 64位十六进制字符) */ export class TxHash { private static readonly PATTERN = /^0x[a-fA-F0-9]{64}$/; private constructor(private readonly _value: string) {} get value(): string { return this._value; } /** * 获取标准化(小写)的哈希值 */ get normalized(): string { return this._value.toLowerCase(); } /** * 创建交易哈希值对象 * @param hash 交易哈希字符串 * @throws Error 如果哈希格式无效 */ static create(hash: string): TxHash { if (!this.isValid(hash)) { throw new Error(`Invalid transaction hash: ${hash}. Expected format: 0x + 64 hex characters.`); } // 统一存储为小写格式 return new TxHash(hash.toLowerCase()); } /** * 验证交易哈希格式是否有效 */ static isValid(hash: string): boolean { return this.PATTERN.test(hash); } /** * 获取缩短显示格式 (0x1234...abcd) */ toShortString(prefixLength = 6, suffixLength = 4): string { return `${this._value.slice(0, prefixLength + 2)}...${this._value.slice(-suffixLength)}`; } equals(other: TxHash): boolean { return this._value === other._value; } toString(): string { return this._value; } } ``` #### 4.3.6 值对象导出索引 ```typescript // src/domain/value-objects/index.ts export { ChainType, ChainTypeEnum } from './chain-type.vo'; export { EvmAddress } from './evm-address.vo'; export { TokenAmount } from './token-amount.vo'; export { BlockNumber } from './block-number.vo'; export { TxHash } from './tx-hash.vo'; ``` ### 4.4 领域事件 ### 4.6 WalletAddressCreated 事件 ```typescript // src/domain/events/wallet-address-created.event.ts import { DomainEvent } from './domain-event.base'; export interface WalletAddressCreatedPayload { userId: string; username: string; publicKey: string; // 压缩公钥 (33 bytes hex) addresses: Array<{ chainType: string; // BSC, KAVA, DST address: string; // 派生的地址 addressType: string; // EVM 或 COSMOS }>; mpcSessionId: string; // MPC 会话 ID delegateShare?: { partyId: string; partyIndex: number; encryptedShare: string; }; } export class WalletAddressCreatedEvent extends DomainEvent { constructor(public readonly payload: WalletAddressCreatedPayload) { super(); } get eventType(): string { return 'WalletAddressCreated'; } get aggregateId(): string { return this.payload.userId; } get aggregateType(): string { return 'WalletAddress'; } } ``` --- ```typescript // src/domain/events/deposit-detected.event.ts import { DomainEvent } from './domain-event.base'; export interface DepositDetectedPayload { chainType: string; txHash: string; fromAddress: string; toAddress: string; amount: string; userId: string; blockNumber: number; } export class DepositDetectedEvent extends DomainEvent { constructor(public readonly payload: DepositDetectedPayload) { super(); } get eventType(): string { return 'DepositDetected'; } get aggregateId(): string { return this.payload.txHash; } get aggregateType(): string { return 'DepositTransaction'; } } ``` ```typescript // src/domain/events/deposit-confirmed.event.ts import { DomainEvent } from './domain-event.base'; export interface DepositConfirmedPayload { depositId: string; chainType: string; txHash: string; amount: string; userId: string; confirmations: number; } export class DepositConfirmedEvent extends DomainEvent { constructor(public readonly payload: DepositConfirmedPayload) { super(); } get eventType(): string { return 'DepositConfirmed'; } get aggregateId(): string { return this.payload.depositId; } get aggregateType(): string { return 'DepositTransaction'; } } ``` ### 4.5 仓储接口 (Ports) ```typescript // src/domain/repositories/deposit-transaction.repository.interface.ts import { DepositTransaction } from '@/domain/aggregates/deposit-transaction'; import { ChainType, TxHash } from '@/domain/value-objects'; import { DepositStatus } from '@/domain/enums'; export interface IDepositTransactionRepository { save(deposit: DepositTransaction): Promise; findById(id: bigint): Promise; findByTxHash(txHash: TxHash): Promise; existsByTxHash(txHash: TxHash): Promise; findByStatus(status: DepositStatus): Promise; findPendingNotification(limit?: number): Promise; findByUserIdAndChain(userId: bigint, chainType: ChainType): Promise; findUnconfirmed(chainType: ChainType, minBlockNumber: bigint): Promise; } export const DEPOSIT_TRANSACTION_REPOSITORY = Symbol('DEPOSIT_TRANSACTION_REPOSITORY'); ``` ```typescript // src/domain/repositories/monitored-address.repository.interface.ts import { ChainType, EvmAddress } from '@/domain/value-objects'; export interface MonitoredAddressData { id: bigint; chainType: ChainType; address: EvmAddress; userId: bigint; isActive: boolean; createdAt: Date; } export interface IMonitoredAddressRepository { save(data: Omit): Promise; findByAddress(chainType: ChainType, address: EvmAddress): Promise; findByUserId(userId: bigint): Promise; findActiveByChain(chainType: ChainType): Promise; getAllActiveAddresses(): Promise>; // 返回所有激活地址的 Set deactivate(id: bigint): Promise; } export const MONITORED_ADDRESS_REPOSITORY = Symbol('MONITORED_ADDRESS_REPOSITORY'); ``` --- ## 5. 基础设施层设计 ### 5.1 EVM Provider 适配器 ```typescript // src/infrastructure/blockchain/evm-provider.adapter.ts import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ethers, JsonRpcProvider, WebSocketProvider, Contract } from 'ethers'; import { ChainTypeEnum } from '@/domain/value-objects'; interface ChainConfig { rpcUrl: string; wsUrl?: string; usdtContract: string; decimals: number; chainId: number; } const ERC20_ABI = [ 'event Transfer(address indexed from, address indexed to, uint256 value)', 'function balanceOf(address owner) view returns (uint256)', 'function decimals() view returns (uint8)', ]; @Injectable() export class EvmProviderAdapter implements OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(EvmProviderAdapter.name); private rpcProviders: Map = new Map(); private wsProviders: Map = new Map(); private contracts: Map = new Map(); private readonly chainConfigs: Record = { [ChainTypeEnum.KAVA]: { rpcUrl: this.configService.get('KAVA_RPC_URL', 'https://evm.kava.io'), wsUrl: this.configService.get('KAVA_WS_URL'), usdtContract: '0x919C1c267BC06a7039e03fcc2eF738525769109c', decimals: 6, chainId: 2222, }, [ChainTypeEnum.BSC]: { rpcUrl: this.configService.get('BSC_RPC_URL', 'https://bsc-dataseed.binance.org'), wsUrl: this.configService.get('BSC_WS_URL'), usdtContract: '0x55d398326f99059fF775485246999027B3197955', decimals: 18, chainId: 56, }, }; constructor(private readonly configService: ConfigService) {} async onModuleInit(): Promise { await this.initializeProviders(); } async onModuleDestroy(): Promise { await this.closeProviders(); } private async initializeProviders(): Promise { for (const [chainType, config] of Object.entries(this.chainConfigs)) { try { // RPC Provider (用于查询) const rpcProvider = new JsonRpcProvider(config.rpcUrl, config.chainId, { staticNetwork: true, }); this.rpcProviders.set(chainType as ChainTypeEnum, rpcProvider); // WebSocket Provider (用于事件监听) if (config.wsUrl) { const wsProvider = new WebSocketProvider(config.wsUrl, config.chainId); this.wsProviders.set(chainType as ChainTypeEnum, wsProvider); } // USDT Contract const provider = config.wsUrl ? this.wsProviders.get(chainType as ChainTypeEnum)! : rpcProvider; const contract = new Contract(config.usdtContract, ERC20_ABI, provider); this.contracts.set(chainType as ChainTypeEnum, contract); this.logger.log(`Initialized ${chainType} provider`); } catch (error) { this.logger.error(`Failed to initialize ${chainType} provider: ${error.message}`); } } } private async closeProviders(): Promise { for (const wsProvider of this.wsProviders.values()) { await wsProvider.destroy(); } } getRpcProvider(chainType: ChainTypeEnum): JsonRpcProvider { const provider = this.rpcProviders.get(chainType); if (!provider) { throw new Error(`No RPC provider for chain: ${chainType}`); } return provider; } getWsProvider(chainType: ChainTypeEnum): WebSocketProvider | undefined { return this.wsProviders.get(chainType); } getUsdtContract(chainType: ChainTypeEnum): Contract { const contract = this.contracts.get(chainType); if (!contract) { throw new Error(`No USDT contract for chain: ${chainType}`); } return contract; } getChainConfig(chainType: ChainTypeEnum): ChainConfig { return this.chainConfigs[chainType]; } async getCurrentBlockNumber(chainType: ChainTypeEnum): Promise { const provider = this.getRpcProvider(chainType); const blockNumber = await provider.getBlockNumber(); return BigInt(blockNumber); } async getBalance(chainType: ChainTypeEnum, address: string): Promise { const contract = this.getUsdtContract(chainType); const config = this.getChainConfig(chainType); const balance = await contract.balanceOf(address); return ethers.formatUnits(balance, config.decimals); } } ``` ### 5.2 事件监听器服务 ```typescript // src/infrastructure/blockchain/event-listener.service.ts import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { EvmProviderAdapter } from './evm-provider.adapter'; import { DepositDetectionService } from '@/application/services/deposit-detection.service'; import { AddressCacheService } from '@/infrastructure/redis/address-cache.service'; import { ChainTypeEnum } from '@/domain/value-objects'; import { Contract, EventLog } from 'ethers'; @Injectable() export class EventListenerService implements OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(EventListenerService.name); private isListening = false; private listeners: Map = new Map(); constructor( private readonly evmProvider: EvmProviderAdapter, private readonly depositService: DepositDetectionService, private readonly addressCache: AddressCacheService, ) {} async onModuleInit(): Promise { await this.startListening(); } async onModuleDestroy(): Promise { await this.stopListening(); } async startListening(): Promise { if (this.isListening) return; for (const chainType of Object.values(ChainTypeEnum)) { await this.subscribeToChain(chainType); } this.isListening = true; this.logger.log('Event listeners started'); } private async subscribeToChain(chainType: ChainTypeEnum): Promise { const wsProvider = this.evmProvider.getWsProvider(chainType); if (!wsProvider) { this.logger.warn(`No WebSocket provider for ${chainType}, using polling`); return; } const contract = this.evmProvider.getUsdtContract(chainType); // 监听所有 Transfer 事件 const filter = contract.filters.Transfer(); const listener = async (from: string, to: string, value: bigint, event: EventLog) => { try { // 检查目标地址是否是平台监控地址 const isMonitored = await this.addressCache.isMonitoredAddress(chainType, to); if (!isMonitored) return; this.logger.log(`Detected deposit: ${chainType} ${event.transactionHash}`); await this.depositService.handleTransferEvent({ chainType, txHash: event.transactionHash, fromAddress: from, toAddress: to, amount: value.toString(), blockNumber: event.blockNumber, logIndex: event.index, }); } catch (error) { this.logger.error(`Error processing Transfer event: ${error.message}`); } }; contract.on(filter, listener); this.listeners.set(chainType, { contract, filter, listener }); this.logger.log(`Subscribed to ${chainType} Transfer events`); } async stopListening(): Promise { for (const [chainType, { contract, filter, listener }] of this.listeners) { contract.off(filter, listener); this.logger.log(`Unsubscribed from ${chainType}`); } this.listeners.clear(); this.isListening = false; } } ``` ### 5.3 地址派生适配器 ```typescript // src/infrastructure/blockchain/address-derivation.adapter.ts import { Injectable, Logger } from '@nestjs/common'; import { createHash } from 'crypto'; import { bech32 } from 'bech32'; import { ethers } from 'ethers'; import * as secp256k1 from 'secp256k1'; export enum AddressType { EVM = 'EVM', COSMOS = 'COSMOS', } export interface DerivedAddress { chainType: string; address: string; addressType: AddressType; } /** * 地址派生适配器 * * 从压缩的 secp256k1 公钥派生多链钱包地址 * - EVM 链 (BSC, KAVA EVM): 使用 keccak256(uncompressed_pubkey[1:])[-20:] * - Cosmos 链 (KAVA, DST): 使用 bech32(ripemd160(sha256(compressed_pubkey))) */ @Injectable() export class AddressDerivationAdapter { private readonly logger = new Logger(AddressDerivationAdapter.name); /** * 从压缩公钥派生所有支持链的地址 * @param compressedPubKeyHex 33 字节压缩公钥 (hex 格式,不含 0x 前缀) */ deriveAllAddresses(compressedPubKeyHex: string): DerivedAddress[] { const pubKeyBytes = Buffer.from(compressedPubKeyHex, 'hex'); if (pubKeyBytes.length !== 33) { throw new Error(`Invalid compressed public key length: ${pubKeyBytes.length}, expected 33`); } const addresses: DerivedAddress[] = []; // EVM 地址 (BSC, KAVA EVM) const evmAddress = this.deriveEvmAddress(pubKeyBytes); addresses.push({ chainType: 'BSC', address: evmAddress, addressType: AddressType.EVM }); addresses.push({ chainType: 'KAVA', address: evmAddress, addressType: AddressType.EVM }); // Cosmos 地址 (KAVA Native 使用 kava 前缀,DST 使用 dst 前缀) const kavaCosmosAddress = this.deriveCosmosAddress(pubKeyBytes, 'kava'); const dstAddress = this.deriveCosmosAddress(pubKeyBytes, 'dst'); addresses.push({ chainType: 'KAVA_COSMOS', address: kavaCosmosAddress, addressType: AddressType.COSMOS }); addresses.push({ chainType: 'DST', address: dstAddress, addressType: AddressType.COSMOS }); this.logger.log(`Derived ${addresses.length} addresses from public key`); return addresses; } /** * 派生 EVM 地址 * 1. 解压公钥 (33 bytes → 65 bytes) * 2. 取非压缩公钥去掉前缀 (64 bytes) * 3. keccak256 哈希后取后 20 bytes */ private deriveEvmAddress(compressedPubKey: Buffer): string { // 使用 secp256k1 解压公钥 const uncompressedPubKey = Buffer.from( secp256k1.publicKeyConvert(compressedPubKey, false) ); // 去掉 0x04 前缀,取 64 bytes const pubKeyWithoutPrefix = uncompressedPubKey.slice(1); // keccak256 哈希后取后 20 bytes const hash = ethers.keccak256(pubKeyWithoutPrefix); const address = '0x' + hash.slice(-40); return ethers.getAddress(address); // 返回 checksum 格式 } /** * 派生 Cosmos 地址 * 1. SHA256(compressed_pubkey) * 2. RIPEMD160(sha256_result) * 3. bech32 编码 */ private deriveCosmosAddress(compressedPubKey: Buffer, prefix: string): string { // SHA256 const sha256Hash = createHash('sha256').update(compressedPubKey).digest(); // RIPEMD160 const ripemd160Hash = createHash('ripemd160').update(sha256Hash).digest(); // Bech32 编码 const words = bech32.toWords(ripemd160Hash); return bech32.encode(prefix, words); } /** * 验证地址格式 */ validateAddress(chainType: string, address: string): boolean { switch (chainType) { case 'BSC': case 'KAVA': return ethers.isAddress(address); case 'KAVA_COSMOS': return /^kava1[a-z0-9]{38}$/.test(address); case 'DST': return /^dst1[a-z0-9]{38}$/.test(address); default: return false; } } } ``` ### 5.4 区块扫描器(补扫服务) ```typescript // src/infrastructure/blockchain/block-scanner.service.ts import { Injectable, Logger } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { EvmProviderAdapter } from './evm-provider.adapter'; import { DepositDetectionService } from '@/application/services/deposit-detection.service'; import { AddressCacheService } from '@/infrastructure/redis/address-cache.service'; import { BlockCheckpointRepository } from '@/infrastructure/persistence/repositories/block-checkpoint.repository.impl'; import { ChainTypeEnum } from '@/domain/value-objects'; import { ethers } from 'ethers'; const BATCH_SIZE = 1000; // 每次扫描的区块数 @Injectable() export class BlockScannerService { private readonly logger = new Logger(BlockScannerService.name); private isScanning: Map = new Map(); constructor( private readonly evmProvider: EvmProviderAdapter, private readonly depositService: DepositDetectionService, private readonly addressCache: AddressCacheService, private readonly checkpointRepo: BlockCheckpointRepository, ) {} /** * 定时补扫任务 (每 5 分钟) */ @Cron(CronExpression.EVERY_5_MINUTES) async scheduledScan(): Promise { for (const chainType of Object.values(ChainTypeEnum)) { await this.scanChain(chainType); } } /** * 扫描指定链的区块 */ async scanChain(chainType: ChainTypeEnum): Promise { if (this.isScanning.get(chainType)) { this.logger.debug(`${chainType} scan already in progress`); return; } this.isScanning.set(chainType, true); try { const checkpoint = await this.checkpointRepo.findByChain(chainType); const currentBlock = await this.evmProvider.getCurrentBlockNumber(chainType); let fromBlock = checkpoint ? checkpoint.lastScannedBlock + 1n : currentBlock - 1000n; // 首次启动扫描最近 1000 块 while (fromBlock <= currentBlock) { const toBlock = fromBlock + BigInt(BATCH_SIZE) - 1n > currentBlock ? currentBlock : fromBlock + BigInt(BATCH_SIZE) - 1n; await this.scanBlockRange(chainType, fromBlock, toBlock); // 更新检查点 await this.checkpointRepo.updateCheckpoint(chainType, toBlock); fromBlock = toBlock + 1n; } this.logger.debug(`${chainType} scan completed up to block ${currentBlock}`); } catch (error) { this.logger.error(`${chainType} scan failed: ${error.message}`); await this.checkpointRepo.markUnhealthy(chainType, error.message); } finally { this.isScanning.set(chainType, false); } } /** * 扫描指定区块范围 */ private async scanBlockRange( chainType: ChainTypeEnum, fromBlock: bigint, toBlock: bigint, ): Promise { const contract = this.evmProvider.getUsdtContract(chainType); const monitoredAddresses = await this.addressCache.getMonitoredAddresses(chainType); if (monitoredAddresses.size === 0) { return; } // 查询 Transfer 事件 const filter = contract.filters.Transfer(null, [...monitoredAddresses]); const events = await contract.queryFilter(filter, Number(fromBlock), Number(toBlock)); for (const event of events) { if (!('args' in event)) continue; const [from, to, value] = event.args; try { await this.depositService.handleTransferEvent({ chainType, txHash: event.transactionHash, fromAddress: from, toAddress: to, amount: value.toString(), blockNumber: event.blockNumber, logIndex: event.index, }); } catch (error) { // 重复交易等预期错误,忽略 if (!error.message.includes('already exists')) { this.logger.error(`Error processing event: ${error.message}`); } } } } /** * 手动触发全量扫描 */ async fullScan(chainType: ChainTypeEnum, fromBlock: bigint, toBlock: bigint): Promise { this.logger.log(`Starting full scan for ${chainType} from ${fromBlock} to ${toBlock}`); await this.scanBlockRange(chainType, fromBlock, toBlock); this.logger.log(`Full scan completed for ${chainType}`); } } ``` ### 5.4 地址缓存服务 ```typescript // src/infrastructure/redis/address-cache.service.ts import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { RedisService } from './redis.service'; import { IMonitoredAddressRepository, MONITORED_ADDRESS_REPOSITORY } from '@/domain/repositories'; import { Inject } from '@nestjs/common'; import { ChainTypeEnum } from '@/domain/value-objects'; const CACHE_KEY_PREFIX = 'blockchain:addresses:'; const CACHE_TTL = 300; // 5 分钟 @Injectable() export class AddressCacheService implements OnModuleInit { private readonly logger = new Logger(AddressCacheService.name); private localCache: Map> = new Map(); constructor( private readonly redis: RedisService, @Inject(MONITORED_ADDRESS_REPOSITORY) private readonly addressRepo: IMonitoredAddressRepository, ) {} async onModuleInit(): Promise { await this.refreshCache(); } /** * 检查地址是否被监控 */ async isMonitoredAddress(chainType: ChainTypeEnum, address: string): Promise { const normalizedAddress = address.toLowerCase(); // 先查本地缓存 const localSet = this.localCache.get(chainType); if (localSet?.has(normalizedAddress)) { return true; } // 再查 Redis const cacheKey = `${CACHE_KEY_PREFIX}${chainType}`; const exists = await this.redis.sismember(cacheKey, normalizedAddress); if (exists) { // 更新本地缓存 if (!localSet) { this.localCache.set(chainType, new Set([normalizedAddress])); } else { localSet.add(normalizedAddress); } } return exists; } /** * 获取链的所有监控地址 */ async getMonitoredAddresses(chainType: ChainTypeEnum): Promise> { const cacheKey = `${CACHE_KEY_PREFIX}${chainType}`; const addresses = await this.redis.smembers(cacheKey); return new Set(addresses); } /** * 添加监控地址 */ async addAddress(chainType: ChainTypeEnum, address: string): Promise { const normalizedAddress = address.toLowerCase(); const cacheKey = `${CACHE_KEY_PREFIX}${chainType}`; await this.redis.sadd(cacheKey, normalizedAddress); // 更新本地缓存 let localSet = this.localCache.get(chainType); if (!localSet) { localSet = new Set(); this.localCache.set(chainType, localSet); } localSet.add(normalizedAddress); } /** * 移除监控地址 */ async removeAddress(chainType: ChainTypeEnum, address: string): Promise { const normalizedAddress = address.toLowerCase(); const cacheKey = `${CACHE_KEY_PREFIX}${chainType}`; await this.redis.srem(cacheKey, normalizedAddress); // 更新本地缓存 this.localCache.get(chainType)?.delete(normalizedAddress); } /** * 从数据库刷新缓存 */ async refreshCache(): Promise { this.logger.log('Refreshing address cache from database...'); for (const chainType of Object.values(ChainTypeEnum)) { const addresses = await this.addressRepo.findActiveByChain( { value: chainType } as any ); const cacheKey = `${CACHE_KEY_PREFIX}${chainType}`; const addressSet = new Set(addresses.map(a => a.address.value.toLowerCase())); // 更新 Redis if (addressSet.size > 0) { await this.redis.del(cacheKey); await this.redis.sadd(cacheKey, ...addressSet); } // 更新本地缓存 this.localCache.set(chainType, addressSet); this.logger.log(`Loaded ${addressSet.size} addresses for ${chainType}`); } } } ``` --- ## 6. 应用层设计 ### 6.1 充值检测服务 ```typescript // src/application/services/deposit-detection.service.ts import { Injectable, Inject, Logger } from '@nestjs/common'; import { IDepositTransactionRepository, DEPOSIT_TRANSACTION_REPOSITORY, IMonitoredAddressRepository, MONITORED_ADDRESS_REPOSITORY, } from '@/domain/repositories'; import { DepositTransaction } from '@/domain/aggregates/deposit-transaction'; import { ChainType, TxHash, EvmAddress, TokenAmount, BlockNumber, ChainTypeEnum } from '@/domain/value-objects'; import { DepositStatus } from '@/domain/enums'; import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; import { WalletClientService } from '@/infrastructure/external/wallet-service/wallet-client.service'; import { EvmProviderAdapter } from '@/infrastructure/blockchain/evm-provider.adapter'; export interface TransferEventData { chainType: ChainTypeEnum; txHash: string; fromAddress: string; toAddress: string; amount: string; blockNumber: number; logIndex: number; } @Injectable() export class DepositDetectionService { private readonly logger = new Logger(DepositDetectionService.name); constructor( @Inject(DEPOSIT_TRANSACTION_REPOSITORY) private readonly depositRepo: IDepositTransactionRepository, @Inject(MONITORED_ADDRESS_REPOSITORY) private readonly addressRepo: IMonitoredAddressRepository, private readonly eventPublisher: EventPublisherService, private readonly walletClient: WalletClientService, private readonly evmProvider: EvmProviderAdapter, ) {} /** * 处理检测到的 Transfer 事件 */ async handleTransferEvent(data: TransferEventData): Promise { const txHash = TxHash.create(data.txHash); // 检查是否已处理 const exists = await this.depositRepo.existsByTxHash(txHash); if (exists) { this.logger.debug(`Deposit ${data.txHash} already exists, skipping`); return; } // 查找监控地址信息 const chainType = ChainType.create(data.chainType); const toAddress = EvmAddress.create(data.toAddress); const addressInfo = await this.addressRepo.findByAddress(chainType, toAddress); if (!addressInfo) { this.logger.warn(`Address ${data.toAddress} not found in monitored addresses`); return; } // 获取区块时间 const block = await this.evmProvider.getRpcProvider(data.chainType).getBlock(data.blockNumber); const blockTimestamp = new Date(block!.timestamp * 1000); // 获取代币精度 const chainConfig = this.evmProvider.getChainConfig(data.chainType); // 创建充值记录 const deposit = DepositTransaction.create({ chainType, txHash, fromAddress: EvmAddress.create(data.fromAddress), toAddress, tokenContract: EvmAddress.create(chainConfig.usdtContract), amount: TokenAmount.create(data.amount, chainConfig.decimals), blockNumber: BlockNumber.create(data.blockNumber), blockTimestamp, logIndex: data.logIndex, addressId: addressInfo.id, userId: addressInfo.userId, }); // 保存并发布事件 await this.depositRepo.save(deposit); await this.eventPublisher.publishAll(deposit.domainEvents); deposit.clearDomainEvents(); this.logger.log(`Deposit detected: ${data.txHash}, amount: ${deposit.amount.formatted}`); // 检查确认数 await this.checkConfirmationsAndNotify(deposit); } /** * 检查确认数并通知 wallet-service */ async checkConfirmationsAndNotify(deposit: DepositTransaction): Promise { const chainType = deposit.chainType; const currentBlock = await this.evmProvider.getCurrentBlockNumber(chainType.value as ChainTypeEnum); const confirmations = Number(currentBlock - deposit.blockNumber.value) + 1; const requiredConfirmations = chainType.requiredConfirmations; deposit.updateConfirmations(confirmations, requiredConfirmations); if (deposit.isConfirmed && !deposit.isNotified) { try { // 调用 wallet-service 入账 await this.walletClient.handleDeposit({ userId: deposit.userId.toString(), amount: deposit.amount.value, chainType: deposit.chainType.value, txHash: deposit.txHash.value, }); deposit.markNotified(); this.logger.log(`Deposit notified: ${deposit.txHash.value}`); } catch (error) { deposit.recordNotifyFailure(error.message); this.logger.error(`Failed to notify deposit: ${error.message}`); } } await this.depositRepo.save(deposit); await this.eventPublisher.publishAll(deposit.domainEvents); deposit.clearDomainEvents(); } /** * 定时任务:更新未确认交易的确认数 */ async updatePendingConfirmations(): Promise { for (const chainType of Object.values(ChainTypeEnum)) { const currentBlock = await this.evmProvider.getCurrentBlockNumber(chainType); const minBlock = currentBlock - 100n; // 只处理最近 100 块内的 const pendingDeposits = await this.depositRepo.findUnconfirmed( ChainType.create(chainType), minBlock, ); for (const deposit of pendingDeposits) { await this.checkConfirmationsAndNotify(deposit); } } } /** * 定时任务:重试失败的通知 */ async retryFailedNotifications(): Promise { const pendingNotifications = await this.depositRepo.findPendingNotification(50); for (const deposit of pendingNotifications) { if (deposit.toProps().notifyAttempts >= 10) { this.logger.error(`Deposit ${deposit.txHash.value} exceeded max retry attempts`); continue; } await this.checkConfirmationsAndNotify(deposit); } } } ``` ### 6.2 余额查询服务 ```typescript // src/application/services/balance-query.service.ts import { Injectable, Logger } from '@nestjs/common'; import { EvmProviderAdapter } from '@/infrastructure/blockchain/evm-provider.adapter'; import { ChainTypeEnum } from '@/domain/value-objects'; export interface BalanceResult { chainType: string; address: string; usdtBalance: string; nativeBalance: string; } @Injectable() export class BalanceQueryService { private readonly logger = new Logger(BalanceQueryService.name); constructor(private readonly evmProvider: EvmProviderAdapter) {} /** * 查询单个地址的余额 */ async getBalance(chainType: ChainTypeEnum, address: string): Promise { const [usdtBalance, nativeBalance] = await Promise.all([ this.evmProvider.getBalance(chainType, address), this.getNativeBalance(chainType, address), ]); return { chainType, address, usdtBalance, nativeBalance, }; } /** * 批量查询余额 */ async getBatchBalances( addresses: Array<{ chainType: ChainTypeEnum; address: string }>, ): Promise { return Promise.all( addresses.map(({ chainType, address }) => this.getBalance(chainType, address)), ); } private async getNativeBalance(chainType: ChainTypeEnum, address: string): Promise { const provider = this.evmProvider.getRpcProvider(chainType); const balance = await provider.getBalance(address); const { ethers } = await import('ethers'); return ethers.formatEther(balance); } } ``` --- ## 7. Kafka 事件设计 ### 7.1 发布的事件 ```typescript // src/infrastructure/kafka/event-publisher.service.ts export const BLOCKCHAIN_TOPICS = { // 地址派生完成事件 (发给 identity-service, wallet-service) WALLET_ADDRESS_CREATED: 'blockchain.WalletAddressCreated', // 充值相关事件 DEPOSIT_DETECTED: 'blockchain.DepositDetected', DEPOSIT_CONFIRMED: 'blockchain.DepositConfirmed', // 交易相关事件 TRANSACTION_BROADCASTED: 'blockchain.TransactionBroadcasted', TRANSACTION_CONFIRMED: 'blockchain.TransactionConfirmed', // 区块扫描事件 BLOCK_SCANNED: 'blockchain.BlockScanned', } as const; ``` ### 7.2 消费的事件 ```typescript // src/infrastructure/kafka/mpc-event-consumer.service.ts // 消费 MPC 服务发布的事件 export const MPC_CONSUME_TOPICS = { KEYGEN_COMPLETED: 'mpc.KeygenCompleted', SESSION_FAILED: 'mpc.SessionFailed', } as const; export interface KeygenCompletedPayload { sessionId: string; partyId: string; publicKey: string; // 33 bytes 压缩公钥 (hex) shareId: string; threshold: string; // "2-of-3" extraPayload?: { userId: string; username: string; delegateShare?: { partyId: string; partyIndex: number; encryptedShare: string; }; serverParties?: string[]; }; } ``` ### 7.3 MPC Keygen 完成事件处理 ```typescript // src/application/event-handlers/mpc-keygen-completed.handler.ts import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { MpcEventConsumerService, KeygenCompletedPayload } from '@/infrastructure/kafka/mpc-event-consumer.service'; import { AddressDerivationAdapter } from '@/infrastructure/blockchain/address-derivation.adapter'; import { EventPublisherService } from '@/infrastructure/kafka/event-publisher.service'; import { WalletAddressCreatedEvent } from '@/domain/events'; @Injectable() export class MpcKeygenCompletedHandler implements OnModuleInit { private readonly logger = new Logger(MpcKeygenCompletedHandler.name); constructor( private readonly mpcEventConsumer: MpcEventConsumerService, private readonly addressDerivation: AddressDerivationAdapter, private readonly eventPublisher: EventPublisherService, ) {} async onModuleInit() { this.mpcEventConsumer.onKeygenCompleted(this.handleKeygenCompleted.bind(this)); this.logger.log('Registered KeygenCompleted event handler'); } private async handleKeygenCompleted(payload: KeygenCompletedPayload): Promise { const userId = payload.extraPayload?.userId; const username = payload.extraPayload?.username; const publicKey = payload.publicKey; if (!userId || !username || !publicKey) { this.logger.error('Missing required fields in KeygenCompleted payload'); return; } this.logger.log(`[DERIVE] Processing keygen completion for userId=${userId}`); this.logger.log(`[DERIVE] PublicKey: ${publicKey.substring(0, 20)}...`); try { // 从公钥派生所有链的地址 const addresses = this.addressDerivation.deriveAllAddresses(publicKey); this.logger.log(`[DERIVE] Derived ${addresses.length} addresses:`); addresses.forEach(addr => { this.logger.log(`[DERIVE] ${addr.chainType}: ${addr.address}`); }); // 发布 WalletAddressCreated 事件 const event = new WalletAddressCreatedEvent({ userId, username, publicKey, addresses: addresses.map(a => ({ chainType: a.chainType, address: a.address, addressType: a.addressType, })), mpcSessionId: payload.sessionId, delegateShare: payload.extraPayload?.delegateShare, }); await this.eventPublisher.publish(event); this.logger.log(`[DERIVE] Published WalletAddressCreated event for userId=${userId}`); } catch (error) { this.logger.error(`[DERIVE] Failed to derive addresses: ${error.message}`, error.stack); } } } ``` ### 7.4 消费的 Identity/Wallet 事件 ```typescript // src/infrastructure/kafka/event-consumer.controller.ts import { Controller, Logger } from '@nestjs/common'; import { MessagePattern, Payload, Ctx, KafkaContext } from '@nestjs/microservices'; import { AddressRegistryService } from '@/application/services/address-registry.service'; @Controller() export class EventConsumerController { private readonly logger = new Logger(EventConsumerController.name); constructor(private readonly addressRegistry: AddressRegistryService) {} /** * 监听用户创建事件,注册监控地址 */ @MessagePattern('identity.UserAccountCreated') async handleUserCreated( @Payload() message: any, @Ctx() context: KafkaContext, ): Promise { this.logger.log(`Received UserAccountCreated: ${message.payload.userId}`); // 注册用户的钱包地址到监控列表 // 注意:地址信息可能在后续的 WalletBound 事件中 } /** * 监听钱包绑定事件,注册监控地址 */ @MessagePattern('identity.WalletBound') async handleWalletBound( @Payload() message: any, @Ctx() context: KafkaContext, ): Promise { this.logger.log(`Received WalletBound: userId=${message.payload.userId}`); await this.addressRegistry.registerAddress({ userId: BigInt(message.payload.userId), chainType: message.payload.chainType, address: message.payload.address, }); } /** * 监听多钱包绑定事件 */ @MessagePattern('identity.MultipleWalletsBound') async handleMultipleWalletsBound( @Payload() message: any, @Ctx() context: KafkaContext, ): Promise { this.logger.log(`Received MultipleWalletsBound: userId=${message.payload.userId}`); for (const wallet of message.payload.wallets) { await this.addressRegistry.registerAddress({ userId: BigInt(message.payload.userId), chainType: wallet.chainType, address: wallet.address, }); } } } ``` --- ## 8. API 设计 ### 8.1 健康检查 ```typescript // src/api/controllers/health.controller.ts @Controller('blockchain/health') export class HealthController { @Get() @Public() async check(): Promise<{ status: string; chains: Record }> { // 检查各链连接状态 } } ``` ### 8.2 余额查询 (内部 API) ```typescript // src/api/controllers/balance.controller.ts @Controller('blockchain/balance') export class BalanceController { @Get(':chainType/:address') @Public() // 内部服务调用 async getBalance( @Param('chainType') chainType: string, @Param('address') address: string, ): Promise { // ... } } ``` --- ## 9. 配置设计 ### 9.1 环境变量 ```bash # .env.example # 服务配置 PORT=3012 NODE_ENV=development # 数据库 DATABASE_URL=postgresql://rwa_user:password@localhost:5432/rwa_blockchain # Redis REDIS_HOST=localhost REDIS_PORT=6379 REDIS_DB=11 # Kafka KAFKA_BROKERS=localhost:9092 KAFKA_CLIENT_ID=blockchain-service # 区块链 RPC KAVA_RPC_URL=https://evm.kava.io KAVA_WS_URL=wss://evm.kava.io/ws BSC_RPC_URL=https://bsc-dataseed.binance.org BSC_WS_URL=wss://bsc-ws-node.nariox.org:443 # 服务间调用 WALLET_SERVICE_URL=http://localhost:3001 IDENTITY_SERVICE_URL=http://localhost:3000 ``` --- ## 10. 部署配置 ### 10.1 docker-compose.yml 配置 在主 `docker-compose.yml` 中添加: ```yaml blockchain-service: build: context: ./blockchain-service dockerfile: Dockerfile container_name: rwa-blockchain-service ports: - "3012:3012" environment: - NODE_ENV=production - DATABASE_URL=postgresql://${POSTGRES_USER:-rwa_user}:${POSTGRES_PASSWORD:-rwa_secure_password}@postgres:5432/rwa_blockchain?schema=public - REDIS_HOST=redis - REDIS_PORT=6379 - REDIS_DB=11 - KAFKA_BROKERS=kafka:29092 - KAFKA_CLIENT_ID=blockchain-service - KAVA_RPC_URL=${KAVA_RPC_URL:-https://evm.kava.io} - KAVA_WS_URL=${KAVA_WS_URL} - BSC_RPC_URL=${BSC_RPC_URL:-https://bsc-dataseed.binance.org} - BSC_WS_URL=${BSC_WS_URL} - WALLET_SERVICE_URL=http://rwa-wallet-service:3001 - IDENTITY_SERVICE_URL=http://rwa-identity-service:3000 depends_on: postgres: condition: service_healthy redis: condition: service_healthy kafka: condition: service_healthy networks: - rwa-network restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3012/api/v1/blockchain/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s ``` ### 10.2 Kong 路由配置 ```yaml # 在 kong.yml 中添加 - name: blockchain-service url: http://192.168.1.111:3012 routes: - name: blockchain-api paths: - /api/v1/blockchain strip_path: false ``` --- ## 11. 初始化脚本 在 `scripts/init-databases.sh` 中添加 `rwa_blockchain`: ```bash for db in rwa_identity rwa_wallet ... rwa_blockchain; do ``` --- ## 12. 关键设计决策 ### 12.1 双重保障机制 ``` 实时监听 (WebSocket) 补扫任务 (定时) │ │ ▼ ▼ 检测充值 ───────────────> 去重处理 │ │ └──────────┬───────────┘ ▼ 确认数检查 │ ▼ 通知 wallet-service ``` ### 12.2 确认数策略 | 链 | 要求确认数 | 约等待时间 | |----|-----------|-----------| | KAVA | 12 块 | ~72 秒 | | BSC | 15 块 | ~45 秒 | ### 12.3 失败重试策略 - 通知失败最多重试 10 次 - 指数退避:1s, 2s, 4s, 8s, ... - 超过最大重试次数告警 --- ## 13. 测试策略 ### 13.1 单元测试 - 领域对象测试 (DepositTransaction, ValueObjects) - 确认数计算逻辑 - 事件生成逻辑 ### 13.2 集成测试 - 区块链 Provider 连接 - 事件监听和解析 - 数据库读写 ### 13.3 E2E 测试 - 完整充值流程模拟 - 补扫逻辑验证 - 失败重试验证 --- ## 14. 监控指标 - `blockchain_deposit_detected_total` - 检测到的充值数 - `blockchain_deposit_confirmed_total` - 确认的充值数 - `blockchain_notify_failed_total` - 通知失败数 - `blockchain_block_scan_lag` - 扫描延迟 (当前块 - 已扫描块) - `blockchain_rpc_latency` - RPC 调用延迟 - `blockchain_ws_reconnect_total` - WebSocket 重连次数 --- ## 15. 迁移计划 ### 从 identity-service 迁移 1. 将 `BlockchainQueryService` 逻辑迁移到 `BalanceQueryService` 2. 将 `DepositService` 逻辑拆分: - 地址获取 → 保留在 identity-service - 余额查询 → 迁移到 blockchain-service 3. 新增事件监听、补扫逻辑 4. identity-service 发布 `WalletBound` 事件 5. blockchain-service 消费事件并注册监控地址 --- ## 16. 架构兼容性说明 ### 16.1 与其他服务的架构对比 本服务严格遵循与其他微服务相同的 **DDD + 六边形架构 + 微服务** 设计原则: | 特性 | blockchain-service | identity-service | wallet-service | admin-service | |-----|-------------------|------------------|----------------|---------------| | **DDD 分层** | ✅ | ✅ | ✅ | ✅ | | **六边形架构** | ✅ | ✅ | ✅ | ✅ | | **模块化设计** | ✅ (modules/) | ✅ | ✅ | ✅ | | **聚合根基类** | ✅ (AggregateRoot) | ✅ | ✅ | ✅ | | **值对象** | 5个 (ChainType, EvmAddress, TokenAmount, BlockNumber, TxHash) | 2个+ | 5个+ | 7个+ | | **领域事件** | ✅ | ❌ | ✅ | ❌ | | **CQRS** | ✅ | ✅ | ✅ | ✅ | | **Kafka 集成** | ✅ | ✅ | ❌ | ❌ | | **Redis 缓存** | ✅ | ✅ | ❌ | ❌ | | **定时任务** | ✅ (@Cron) | ❌ | ❌ | ❌ | ### 16.2 关键架构原则 #### 依赖倒置原则 (Dependency Inversion) ``` ┌─────────────────────────────────────────────────────────┐ │ Domain Layer │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Repository Interfaces (Ports) │ │ │ │ - IDepositTransactionRepository │ │ │ │ - IMonitoredAddressRepository │ │ │ └─────────────────────────────────────────────────┘ │ │ ▲ │ │ │ 依赖 │ │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Infrastructure Layer │ │ │ │ Repository Implementations │ │ │ │ - DepositTransactionRepositoryImpl │ │ │ │ - MonitoredAddressRepositoryImpl │ │ │ └─────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` #### 六边形架构端口与适配器 | 类型 | 端口 (Port) | 适配器 (Adapter) | |------|------------|------------------| | **入站 (Driving)** | Controllers | HTTP API | | **入站 (Driving)** | Kafka Consumer | Event Handler | | **出站 (Driven)** | Repository Interfaces | Prisma Repositories | | **出站 (Driven)** | Cache Interface | Redis Cache | | **出站 (Driven)** | Event Publisher | Kafka Producer | | **出站 (Driven)** | External Services | HTTP Clients | ### 16.3 命名约定 为与其他服务保持一致,请遵循以下命名约定: | 类型 | 命名规则 | 示例 | |------|---------|------| | **聚合根** | `*.aggregate.ts` | `deposit-transaction.aggregate.ts` | | **值对象** | `*.vo.ts` | `chain-type.vo.ts`, `block-number.vo.ts` | | **领域事件** | `*.event.ts` | `deposit-detected.event.ts` | | **仓储接口** | `*.repository.interface.ts` | `deposit-transaction.repository.interface.ts` | | **仓储实现** | `*.repository.impl.ts` | `deposit-transaction.repository.impl.ts` | | **映射器** | `*.mapper.ts` | `deposit-transaction.mapper.ts` | | **模块** | `*.module.ts` | `api.module.ts`, `domain.module.ts` | ### 16.4 Symbol Token 注入规范 使用 Symbol 作为依赖注入 Token,与其他服务保持一致: ```typescript // 定义 (domain/repositories/index.ts) export const DEPOSIT_TRANSACTION_REPOSITORY = Symbol('DEPOSIT_TRANSACTION_REPOSITORY'); export const MONITORED_ADDRESS_REPOSITORY = Symbol('MONITORED_ADDRESS_REPOSITORY'); export const BLOCK_CHECKPOINT_REPOSITORY = Symbol('BLOCK_CHECKPOINT_REPOSITORY'); // 注册 (modules/domain.module.ts) { provide: DEPOSIT_TRANSACTION_REPOSITORY, useClass: DepositTransactionRepositoryImpl, } // 注入 (application/services/*.ts) constructor( @Inject(DEPOSIT_TRANSACTION_REPOSITORY) private readonly depositRepo: IDepositTransactionRepository, ) {} ``` --- ## 17. MPC 集成事件流 ### 17.1 完整事件流程图 ``` ┌──────────────────────────────────────────────────────────────────────────────┐ │ MPC 钱包创建事件流 │ ├──────────────────────────────────────────────────────────────────────────────┤ │ │ │ [1] 用户创建账户 │ │ │ │ │ ▼ │ │ ┌─────────────────┐ │ │ │ identity-service│ │ │ │ │ ────────────────────────────────────────┐ │ │ │ 发布事件: │ │ │ │ │ mpc.KeygenRequested │ │ │ └─────────────────┘ │ │ │ ▼ │ │ ┌─────────────────┐ │ │ [2] MPC 密钥生成 │ mpc-service │ │ │ │ │ │ │ ┌────────────────────────────────────────────│ 消费事件: │ │ │ │ │ mpc.KeygenRequested │ │ │ │ │ │ │ │ [3] 生成公钥 │ 发布事件: │ │ │ │ │ │ mpc.KeygenStarted │ │ │ │ │ mpc.KeygenCompleted │ │ │ ▼ └─────────────────┘ │ │ │ publicKey: 33 bytes │ │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ blockchain-service │ │ │ │ │ │ │ │ [4] 消费: mpc.KeygenCompleted │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ │ │ AddressDerivationAdapter │ │ │ │ │ │ │ │ │ │ │ │ publicKey ──┬──> deriveEvmAddress() ──> 0x1234... │ │ │ │ │ │ │ (BSC, KAVA) │ │ │ │ │ │ │ │ │ │ │ │ │ └──> deriveCosmosAddress() ──> kava1... │ │ │ │ │ │ dst1... │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ [5] 发布: blockchain.WalletAddressCreated │ │ │ │ │ │ │ │ └───────┼──────────────────────────────────────────────────────────┘ │ │ │ │ │ ├──────────────────────────────┐ │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ identity-service│ │ wallet-service │ │ │ │ │ │ │ │ │ │ [6] 存储关联: │ │ [7] 存储钱包: │ │ │ │ userId ↔ address│ │ 地址、余额管理 │ │ │ └─────────────────┘ └─────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────────────────┘ ``` ### 17.2 事件 Topic 汇总 | Topic | 发布者 | 消费者 | 描述 | |-------|--------|--------|------| | `mpc.KeygenRequested` | identity-service | mpc-service | 请求生成 MPC 密钥 | | `mpc.KeygenStarted` | mpc-service | identity-service | 密钥生成开始 | | `mpc.KeygenCompleted` | mpc-service | blockchain-service | 密钥生成完成,包含公钥 | | `mpc.SessionFailed` | mpc-service | identity-service, blockchain-service | 会话失败 | | `blockchain.WalletAddressCreated` | blockchain-service | identity-service, wallet-service | 地址派生完成 | ### 17.3 关键设计决策 1. **职责分离**: - mpc-service 只负责 MPC 协议,不了解区块链地址格式 - blockchain-service 封装所有区块链技术细节 - identity-service 不直接处理公钥→地址转换 2. **多链扩展性**: - 新增链类型只需在 `AddressDerivationAdapter` 添加派生逻辑 - 事件格式保持不变,下游服务无需修改 3. **事件溯源**: - 所有状态变更通过事件传递 - 支持事件重放和故障恢复 --- *文档版本: 1.2.0* *最后更新: 2025-12-06* *变更说明: 添加公钥→地址派生职责、MPC 事件集成、AddressDerivationAdapter、WalletAddressCreated 事件*