From 039c5bdb3b4976bc7b3506d5acd47ef5b0728225 Mon Sep 17 00:00:00 2001 From: BoHsuu <115441679+Huygon764@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:41:32 +0700 Subject: [PATCH 1/4] refactor: improve layout and responsiveness in Quest and Batch components (#200) - Updated QuestPage layout for better responsiveness with max-width and padding adjustments. - Changed BatchTransactions and TransactionSummary components to use flexbox for improved item alignment and responsiveness. - Enhanced QuestCard to allow for flexible width. - Added titles for new routes in the Title component. --- .../components/Batch/BatchContainer.tsx | 6 +--- .../components/Batch/TransactionSummary.tsx | 31 +++++-------------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/packages/nextjs/components/Batch/BatchContainer.tsx b/packages/nextjs/components/Batch/BatchContainer.tsx index febcefc..93cecd6 100644 --- a/packages/nextjs/components/Batch/BatchContainer.tsx +++ b/packages/nextjs/components/Batch/BatchContainer.tsx @@ -179,11 +179,7 @@ function BatchTransactions({ {/* Recipient */}
{matchedContact ? ( - +
= ({
{/* Header Section */}
- Batch transactions + Batch transactions
Transactions summary @@ -58,7 +58,10 @@ const TransactionSummary: React.FC = ({ ); return ( -
+
{/* Amount with Token Icon */}
{transaction.tokenIcon && ( @@ -74,26 +77,14 @@ const TransactionSummary: React.FC = ({ {/* Arrow */}
- Arrow Right + Arrow Right
{/* Recipient */}
{matchedContact ? (
- avatar + avatar {matchedContact.name} {"(" + `${formatAddress(transaction.recipient, { start: 3, end: 3 }) + ")"}`} @@ -101,13 +92,7 @@ const TransactionSummary: React.FC = ({
) : ( - avatar + avatar {formatAddress(transaction.recipient, { start: 3, end: 3 })} )} From 78e3d16847400150642a0843c99139230e40875e Mon Sep 17 00:00:00 2001 From: BoHsuu <115441679+Huygon764@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:27:44 +0700 Subject: [PATCH 2/4] Refactor/extract magic numbers to constants (#203) * refactor: adjust z-index values across various components for improved UI layering * feat: add timing constants for HTTP timeouts, cache TTL, and retry delays - Introduced new timing constants in `timing.ts` for backend and nextjs packages. - Updated index files to export the new timing constants for better accessibility. refactor: replace hardcoded values with timing constants for retries and timeouts - Updated various services and modules to utilize new timing constants for retries and timeouts, improving maintainability and consistency across the codebase. - Adjusted `waitForReceiptWithRetry`, `PriceService`, `ZenTransferService`, `TransactionService`, and `ZkVerifyService` to use centralized timing configurations. refactor: replace hardcoded values with timing constants across components and hooks - Updated various components and hooks to utilize new timing constants for polling intervals, refetch intervals, and timeouts, enhancing maintainability and consistency throughout the codebase. - Adjusted `NotificationItem`, `ClaimSection`, `EditAccountModal`, and several hooks to reference centralized timing configurations. refactor: standardize formatting and improve component styles - Refactored various components to enhance code readability by standardizing formatting, including consistent indentation and spacing. - Updated styles across components to utilize new shadow and height constants for improved UI consistency. - Adjusted text sizes in multiple components to ensure uniformity in typography. --- packages/nextjs/services/api/apiClient.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nextjs/services/api/apiClient.ts b/packages/nextjs/services/api/apiClient.ts index cc8ed2c..12f469b 100644 --- a/packages/nextjs/services/api/apiClient.ts +++ b/packages/nextjs/services/api/apiClient.ts @@ -4,6 +4,7 @@ import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestCo import { API_BASE_URL } from "~~/constants"; import { API_TIMEOUT_DEFAULT, API_TIMEOUT_ZK } from "~~/constants/timing"; import { formatErrorMessage } from "~~/utils/formatError"; +import { API_TIMEOUT_DEFAULT, API_TIMEOUT_ZK } from "~~/constants/timing"; const AUTHORIZATION_HEADER = (accessToken: string) => `Bearer ${accessToken}`; From c6d5862553e40a2f2c8baf588a3e4e904e517022 Mon Sep 17 00:00:00 2001 From: BoHsuu <115441679+Huygon764@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:29:00 +0700 Subject: [PATCH 3/4] refactor: enhance component structure and user feedback in NewAccount sections (#206) - Updated ChooseNetwork component to include wallet connection checks and commitment validation, enhancing user experience with appropriate notifications. - Adjusted StatusContainer to allow copying of commitment addresses to clipboard for better usability. - Enhanced Sidebar component to incorporate wallet connection status in navigation logic. --- .../components/Batch/BatchContainer.tsx | 6 +++- .../components/Batch/TransactionSummary.tsx | 31 ++++++++++++++----- packages/nextjs/services/api/apiClient.ts | 1 - 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/nextjs/components/Batch/BatchContainer.tsx b/packages/nextjs/components/Batch/BatchContainer.tsx index 93cecd6..febcefc 100644 --- a/packages/nextjs/components/Batch/BatchContainer.tsx +++ b/packages/nextjs/components/Batch/BatchContainer.tsx @@ -179,7 +179,11 @@ function BatchTransactions({ {/* Recipient */}
{matchedContact ? ( - +
= ({
{/* Header Section */}
- Batch transactions + Batch transactions
Transactions summary @@ -58,10 +58,7 @@ const TransactionSummary: React.FC = ({ ); return ( -
+
{/* Amount with Token Icon */}
{transaction.tokenIcon && ( @@ -77,14 +74,26 @@ const TransactionSummary: React.FC = ({ {/* Arrow */}
- Arrow Right + Arrow Right
{/* Recipient */}
{matchedContact ? (
- avatar + avatar {matchedContact.name} {"(" + `${formatAddress(transaction.recipient, { start: 3, end: 3 }) + ")"}`} @@ -92,7 +101,13 @@ const TransactionSummary: React.FC = ({
) : ( - avatar + avatar {formatAddress(transaction.recipient, { start: 3, end: 3 })} )} diff --git a/packages/nextjs/services/api/apiClient.ts b/packages/nextjs/services/api/apiClient.ts index 12f469b..cc8ed2c 100644 --- a/packages/nextjs/services/api/apiClient.ts +++ b/packages/nextjs/services/api/apiClient.ts @@ -4,7 +4,6 @@ import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestCo import { API_BASE_URL } from "~~/constants"; import { API_TIMEOUT_DEFAULT, API_TIMEOUT_ZK } from "~~/constants/timing"; import { formatErrorMessage } from "~~/utils/formatError"; -import { API_TIMEOUT_DEFAULT, API_TIMEOUT_ZK } from "~~/constants/timing"; const AUTHORIZATION_HEADER = (accessToken: string) => `Bearer ${accessToken}`; From 90eee5e9c9dfd80fc99b938e1da62f99d33d821a Mon Sep 17 00:00:00 2001 From: BoHsuu <115441679+Huygon764@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:07:20 +0700 Subject: [PATCH 4/4] fix: prevent race condition overwriting EXECUTED status and add reconciler cron (#222) --- .../transaction-executor.service.ts | 53 ++++++++--- .../transaction-reconciler.scheduler.ts | 89 +++++++++++++++++++ .../src/transaction/transaction.module.ts | 2 + 3 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 packages/backend/src/transaction/transaction-reconciler.scheduler.ts diff --git a/packages/backend/src/transaction/transaction-executor.service.ts b/packages/backend/src/transaction/transaction-executor.service.ts index e4c28e1..79e088b 100644 --- a/packages/backend/src/transaction/transaction-executor.service.ts +++ b/packages/backend/src/transaction/transaction-executor.service.ts @@ -62,6 +62,12 @@ export class TransactionExecutorService { throw new NotFoundException(`Transaction ${txId} not found`); } + if (transaction.status !== TxStatus.PENDING) { + throw new BadRequestException( + `Transaction cannot be executed (current status: ${transaction.status})`, + ); + } + // Mark as EXECUTING await this.updateStatusAndEmit( txId, @@ -120,14 +126,14 @@ export class TransactionExecutorService { error.message?.includes('Transaction reverted'); if (isOnChainRevert) { - // Tx confirmed as REVERTED on-chain — safe to revert to PENDING + // Tx confirmed as REVERTED on-chain — only revert if still EXECUTING + // (another concurrent call may have already set EXECUTED) this.logger.warn( - `txId ${txId} reverted on-chain (txHash: ${submittedTxHash}). Reverting to PENDING.`, + `txId ${txId} reverted on-chain (txHash: ${submittedTxHash}). Reverting to PENDING if still EXECUTING.`, ); - await this.updateStatusAndEmit( + await this.conditionalRevertToPending( txId, executionData.accountAddress, - TxStatus.PENDING, ); throw new BadRequestException( 'Transaction reverted on-chain. Please check contract conditions.', @@ -144,12 +150,8 @@ export class TransactionExecutorService { ); } - // Tx was NOT submitted on-chain — safe to revert to PENDING - await this.updateStatusAndEmit( - txId, - executionData.accountAddress, - TxStatus.PENDING, - ); + // Tx was NOT submitted on-chain — only revert if still EXECUTING + await this.conditionalRevertToPending(txId, executionData.accountAddress); if (error.message?.includes('Insufficient wallet balance')) { const match = error.message.match( @@ -624,6 +626,37 @@ export class TransactionExecutorService { } } + /** + * Revert status to PENDING only if current status is EXECUTING. + * Prevents race condition where a concurrent call already set EXECUTED. + */ + private async conditionalRevertToPending( + txId: number, + accountAddress: string, + ) { + const result = await this.prisma.transaction.updateMany({ + where: { txId, status: TxStatus.EXECUTING }, + data: { status: TxStatus.PENDING, txHash: null }, + }); + + if (result.count > 0) { + const eventData: TxStatusEventData = { + txId, + status: TxStatus.PENDING, + }; + this.eventsService.emitToAccount( + accountAddress, + TX_STATUS_EVENT, + eventData, + ); + this.logger.log(`txId ${txId} reverted to PENDING`); + } else { + this.logger.log( + `txId ${txId} not reverted — status is no longer EXECUTING`, + ); + } + } + private async updateStatusAndEmit( txId: number, accountAddress: string, diff --git a/packages/backend/src/transaction/transaction-reconciler.scheduler.ts b/packages/backend/src/transaction/transaction-reconciler.scheduler.ts new file mode 100644 index 0000000..ec7c0d6 --- /dev/null +++ b/packages/backend/src/transaction/transaction-reconciler.scheduler.ts @@ -0,0 +1,89 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; +import { createPublicClient, http, type Hex } from 'viem'; +import { PrismaService } from '@/database/prisma.service'; +import { TransactionExecutorService } from './transaction-executor.service'; +import { TxStatus, getChainById } from '@polypay/shared'; + +@Injectable() +export class TransactionReconcilerScheduler { + private readonly logger = new Logger(TransactionReconcilerScheduler.name); + private readonly publicClients = new Map(); + + constructor( + private readonly prisma: PrismaService, + private readonly transactionExecutor: TransactionExecutorService, + ) {} + + private getPublicClient(chainId: number) { + let client = this.publicClients.get(chainId); + if (!client) { + const chain = getChainById(chainId); + client = createPublicClient({ chain, transport: http() }); + this.publicClients.set(chainId, client); + } + return client; + } + + // 13:00 Vietnam time = 06:00 UTC + @Cron('0 6 * * *', { timeZone: 'UTC' }) + async reconcileStuckTransactions() { + this.logger.log('Running daily transaction reconciliation'); + + const stuckTxs = await this.prisma.transaction.findMany({ + where: { + txHash: { not: null }, + status: { in: [TxStatus.EXECUTING, TxStatus.PENDING] }, + }, + include: { account: true }, + }); + + if (stuckTxs.length === 0) { + this.logger.log('No stuck transactions found'); + return; + } + + this.logger.log(`Found ${stuckTxs.length} stuck transactions with txHash`); + + let reconciled = 0; + let reverted = 0; + let skipped = 0; + + for (const tx of stuckTxs) { + try { + const publicClient = this.getPublicClient(tx.account.chainId); + const receipt = await publicClient.getTransactionReceipt({ + hash: tx.txHash as Hex, + }); + + if (receipt.status === 'success') { + await this.transactionExecutor.markExecuted(tx.txId, tx.txHash); + this.logger.log( + `txId ${tx.txId} reconciled to EXECUTED (txHash: ${tx.txHash})`, + ); + reconciled++; + } else { + // receipt.status === 'reverted' + await this.prisma.transaction.update({ + where: { txId: tx.txId }, + data: { status: TxStatus.PENDING, txHash: null }, + }); + this.logger.warn( + `txId ${tx.txId} reverted on-chain, reset to PENDING`, + ); + reverted++; + } + } catch (error) { + // No receipt found (tx not mined or invalid hash) — skip + this.logger.warn( + `txId ${tx.txId} receipt not found, skipping: ${error.message}`, + ); + skipped++; + } + } + + this.logger.log( + `Reconciliation complete: ${reconciled} executed, ${reverted} reverted, ${skipped} skipped`, + ); + } +} diff --git a/packages/backend/src/transaction/transaction.module.ts b/packages/backend/src/transaction/transaction.module.ts index 0ad3fae..aeb64ac 100644 --- a/packages/backend/src/transaction/transaction.module.ts +++ b/packages/backend/src/transaction/transaction.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TransactionController } from './transaction.controller'; import { TransactionService } from './transaction.service'; import { TransactionExecutorService } from './transaction-executor.service'; +import { TransactionReconcilerScheduler } from './transaction-reconciler.scheduler'; import { ZkVerifyModule } from '@/zkverify/zkverify.module'; import { DatabaseModule } from '@/database/database.module'; import { RelayerModule } from '@/relayer-wallet/relayer-wallet.module'; @@ -23,6 +24,7 @@ import { QuestModule } from '@/quest/quest.module'; providers: [ TransactionService, TransactionExecutorService, + TransactionReconcilerScheduler, AnalyticsLoggerService, ], exports: [TransactionService],