2791 lines
100 KiB
Markdown
2791 lines
100 KiB
Markdown
# 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<TId = bigint> {
|
||
private readonly _domainEvents: DomainEvent[] = [];
|
||
|
||
/**
|
||
* 聚合根唯一标识
|
||
*/
|
||
abstract get id(): TId | undefined;
|
||
|
||
/**
|
||
* 获取所有待发布的领域事件
|
||
*/
|
||
get domainEvents(): ReadonlyArray<DomainEvent> {
|
||
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<bigint> {
|
||
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<DepositTransaction>;
|
||
findById(id: bigint): Promise<DepositTransaction | null>;
|
||
findByTxHash(txHash: TxHash): Promise<DepositTransaction | null>;
|
||
existsByTxHash(txHash: TxHash): Promise<boolean>;
|
||
findByStatus(status: DepositStatus): Promise<DepositTransaction[]>;
|
||
findPendingNotification(limit?: number): Promise<DepositTransaction[]>;
|
||
findByUserIdAndChain(userId: bigint, chainType: ChainType): Promise<DepositTransaction[]>;
|
||
findUnconfirmed(chainType: ChainType, minBlockNumber: bigint): Promise<DepositTransaction[]>;
|
||
}
|
||
|
||
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<MonitoredAddressData, 'id' | 'createdAt'>): Promise<MonitoredAddressData>;
|
||
findByAddress(chainType: ChainType, address: EvmAddress): Promise<MonitoredAddressData | null>;
|
||
findByUserId(userId: bigint): Promise<MonitoredAddressData[]>;
|
||
findActiveByChain(chainType: ChainType): Promise<MonitoredAddressData[]>;
|
||
getAllActiveAddresses(): Promise<Set<string>>; // 返回所有激活地址的 Set
|
||
deactivate(id: bigint): Promise<void>;
|
||
}
|
||
|
||
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<ChainTypeEnum, JsonRpcProvider> = new Map();
|
||
private wsProviders: Map<ChainTypeEnum, WebSocketProvider> = new Map();
|
||
private contracts: Map<ChainTypeEnum, Contract> = new Map();
|
||
|
||
private readonly chainConfigs: Record<ChainTypeEnum, ChainConfig> = {
|
||
[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<void> {
|
||
await this.initializeProviders();
|
||
}
|
||
|
||
async onModuleDestroy(): Promise<void> {
|
||
await this.closeProviders();
|
||
}
|
||
|
||
private async initializeProviders(): Promise<void> {
|
||
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<void> {
|
||
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<bigint> {
|
||
const provider = this.getRpcProvider(chainType);
|
||
const blockNumber = await provider.getBlockNumber();
|
||
return BigInt(blockNumber);
|
||
}
|
||
|
||
async getBalance(chainType: ChainTypeEnum, address: string): Promise<string> {
|
||
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<ChainTypeEnum, any> = new Map();
|
||
|
||
constructor(
|
||
private readonly evmProvider: EvmProviderAdapter,
|
||
private readonly depositService: DepositDetectionService,
|
||
private readonly addressCache: AddressCacheService,
|
||
) {}
|
||
|
||
async onModuleInit(): Promise<void> {
|
||
await this.startListening();
|
||
}
|
||
|
||
async onModuleDestroy(): Promise<void> {
|
||
await this.stopListening();
|
||
}
|
||
|
||
async startListening(): Promise<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<ChainTypeEnum, boolean> = 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<void> {
|
||
for (const chainType of Object.values(ChainTypeEnum)) {
|
||
await this.scanChain(chainType);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 扫描指定链的区块
|
||
*/
|
||
async scanChain(chainType: ChainTypeEnum): Promise<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<ChainTypeEnum, Set<string>> = new Map();
|
||
|
||
constructor(
|
||
private readonly redis: RedisService,
|
||
@Inject(MONITORED_ADDRESS_REPOSITORY)
|
||
private readonly addressRepo: IMonitoredAddressRepository,
|
||
) {}
|
||
|
||
async onModuleInit(): Promise<void> {
|
||
await this.refreshCache();
|
||
}
|
||
|
||
/**
|
||
* 检查地址是否被监控
|
||
*/
|
||
async isMonitoredAddress(chainType: ChainTypeEnum, address: string): Promise<boolean> {
|
||
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<Set<string>> {
|
||
const cacheKey = `${CACHE_KEY_PREFIX}${chainType}`;
|
||
const addresses = await this.redis.smembers(cacheKey);
|
||
return new Set(addresses);
|
||
}
|
||
|
||
/**
|
||
* 添加监控地址
|
||
*/
|
||
async addAddress(chainType: ChainTypeEnum, address: string): Promise<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<BalanceResult> {
|
||
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<BalanceResult[]> {
|
||
return Promise.all(
|
||
addresses.map(({ chainType, address }) => this.getBalance(chainType, address)),
|
||
);
|
||
}
|
||
|
||
private async getNativeBalance(chainType: ChainTypeEnum, address: string): Promise<string> {
|
||
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<void> {
|
||
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<void> {
|
||
this.logger.log(`Received UserAccountCreated: ${message.payload.userId}`);
|
||
|
||
// 注册用户的钱包地址到监控列表
|
||
// 注意:地址信息可能在后续的 WalletBound 事件中
|
||
}
|
||
|
||
/**
|
||
* 监听钱包绑定事件,注册监控地址
|
||
*/
|
||
@MessagePattern('identity.WalletBound')
|
||
async handleWalletBound(
|
||
@Payload() message: any,
|
||
@Ctx() context: KafkaContext,
|
||
): Promise<void> {
|
||
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<void> {
|
||
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<string, boolean> }> {
|
||
// 检查各链连接状态
|
||
}
|
||
}
|
||
```
|
||
|
||
### 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<BalanceResult> {
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 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 事件*
|