Skip to content

Task 2-2: Transaction Monitoring & Confirmation Service

Phase: 2 Priority: Medium Module: solana Depends on: Không có Reference: docs/bountyhunter-blockchain-p2/details/feature-solana-integration/SPEC.md

Background

Các Solana transaction long-running hiện đang được await synchronously trong service — có thể timeout khi network congested. Không có cơ chế polling để theo dõi trạng thái transaction sau khi broadcast. Cần tạo SolanaTransactionMonitorService chạy background, poll getSignatureStatus() với exponential backoff, và notify Backend qua webhook khi transaction confirmed hoặc failed.

Tasks

Note: @nestjs/schedule cần được import trong SolanaModule (hoặc AppModule). getSignatureStatus trả về SignatureStatus | nullnull nghĩa là chưa thấy, cần tiếp tục poll. Commitment level 'finalized' đảm bảo không bị rollback nhưng chậm hơn 'confirmed'. Lưu pending signatures vào in-memory store hoặc Redis để không mất khi restart.

  • [ ] Tạo solana-transaction-monitor.service.ts:
    @Injectable()
    export class SolanaTransactionMonitorService {
      private readonly logger = new Logger(SolanaTransactionMonitorService.name);
      private readonly pendingTransactions = new Map<string, {
        signature: string;
        webhookUrl: string;
        createdAt: number;
        attempts: number;
      }>();
    
      constructor(
        private readonly solanaUtil: SolanaUtil,
      ) {}
    
      registerTransaction(signature: string, webhookUrl: string): void {
        this.pendingTransactions.set(signature, {
          signature,
          webhookUrl,
          createdAt: Date.now(),
          attempts: 0,
        });
        this.logger.log(`[SOLANA_MONITOR] Registered signature=${signature}`);
      }
    }
    
  • [ ] Thêm polling job với @Cron('*/15 * * * * *') (mỗi 15 giây):
    @Cron('*/15 * * * * *')
    async pollPendingTransactions(): Promise<void> {
      for (const [signature, tx] of this.pendingTransactions.entries()) {
        try {
          const status = await this.solanaUtil.connection.getSignatureStatus(signature);
          const confirmationStatus = status?.value?.confirmationStatus;
    
          if (confirmationStatus === 'finalized' || confirmationStatus === 'confirmed') {
            this.logger.log(`[SOLANA_MONITOR] signature=${signature} status=${confirmationStatus}`);
            await postWithRetry(tx.webhookUrl, { status: 'SUCCESS', txHash: signature });
            this.pendingTransactions.delete(signature);
          } else if (status?.value?.err) {
            this.logger.error(`[SOLANA_MONITOR] signature=${signature} FAILED err=${JSON.stringify(status.value.err)}`);
            await postWithRetry(tx.webhookUrl, { status: 'FAILED', txHash: signature, error: JSON.stringify(status.value.err) });
            this.pendingTransactions.delete(signature);
          } else if (Date.now() - tx.createdAt > 5 * 60 * 1000) {
            // Timeout sau 5 phút
            this.logger.error(`[SOLANA_MONITOR] signature=${signature} TIMEOUT after 5min`);
            await postWithRetry(tx.webhookUrl, { status: 'FAILED', txHash: signature, error: 'TRANSACTION_TIMEOUT' });
            this.pendingTransactions.delete(signature);
          }
        } catch (error) {
          this.logger.error(`[SOLANA_MONITOR] Error polling signature=${signature}: ${error.message}`);
        }
      }
    }
    
  • [ ] Tích hợp với SolanaService: sau khi broadcast tx, gọi monitorService.registerTransaction(signature, webhookUrl) thay vì await confirm
  • [ ] Thêm SolanaTransactionMonitorService vào SolanaModule providers
  • [ ] Import ScheduleModule.forRoot() vào SolanaModule hoặc AppModule nếu chưa có

Verification / Acceptance Criteria

  • [ ] Sau khi broadcast tx → registerTransaction được gọi, tx trong pendingTransactions
  • [ ] Sau tối đa 15 giây poll → getSignatureStatus được gọi
  • [ ] Status finalized → webhook success callback với txHash
  • [ ] Status err → webhook failure callback với error details
  • [ ] Timeout 5 phút → webhook failure với TRANSACTION_TIMEOUT
  • [ ] Không có memory leak: entries được xóa khỏi Map sau khi xử lý
  • [ ] TypeScript compile không có lỗi

Files to Modify

  • src/solana/solana-transaction-monitor.service.ts (tạo mới)
  • src/solana/solana.module.ts
  • src/solana/solana.service.ts (tích hợp monitor)