Task 2-2: Transaction Monitoring & Confirmation Service
Phase: 2 Priority: Medium Module:
solanaDepends 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/schedulecần được import trongSolanaModule(hoặcAppModule).getSignatureStatustrả vềSignatureStatus | null—nullnghĩ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ọimonitorService.registerTransaction(signature, webhookUrl)thay vìawait confirm - [ ] Thêm
SolanaTransactionMonitorServicevàoSolanaModuleproviders - [ ] Import
ScheduleModule.forRoot()vàoSolanaModulehoặcAppModulenếu chưa có
Verification / Acceptance Criteria
- [ ] Sau khi broadcast tx →
registerTransactionđược gọi, tx trongpendingTransactions - [ ] Sau tối đa 15 giây poll →
getSignatureStatusđược gọi - [ ] Status
finalized→ webhook success callback vớitxHash - [ ] 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.tssrc/solana/solana.service.ts(tích hợp monitor)