From 306b7f271f000f4e0df81d0ae027cc828b38021f Mon Sep 17 00:00:00 2001 From: devchenyan Date: Sun, 7 Jul 2024 23:56:02 +0800 Subject: [PATCH 1/2] feat: Periodic validation of pending transactions --- .../sync/indexer-cache-service.ts | 2 +- .../block-sync-renderer/tx-status-listener.ts | 34 ++++++++--- .../src/models/chain/tx-status.ts | 5 ++ .../neuron-wallet/src/services/rpc-service.ts | 18 +++--- .../src/services/tx/failed-transaction.ts | 56 ++++++++++--------- .../src/services/tx/transaction-generator.ts | 2 +- 6 files changed, 73 insertions(+), 44 deletions(-) diff --git a/packages/neuron-wallet/src/block-sync-renderer/sync/indexer-cache-service.ts b/packages/neuron-wallet/src/block-sync-renderer/sync/indexer-cache-service.ts index b3401f6626..efb9d7f3fd 100644 --- a/packages/neuron-wallet/src/block-sync-renderer/sync/indexer-cache-service.ts +++ b/packages/neuron-wallet/src/block-sync-renderer/sync/indexer-cache-service.ts @@ -194,7 +194,7 @@ export default class IndexerCacheService { const txsWithStatus: TransactionWithStatus[] = [] const fetchBlockDetailsQueue = queue(async (hash: string) => { const txWithStatus = await this.rpcService.getTransaction(hash) - if (!txWithStatus) { + if (!txWithStatus?.transaction) { return } const blockHeader = await this.rpcService.getHeader(txWithStatus!.txStatus.blockHash!) diff --git a/packages/neuron-wallet/src/block-sync-renderer/tx-status-listener.ts b/packages/neuron-wallet/src/block-sync-renderer/tx-status-listener.ts index 1a2532559b..1877253587 100644 --- a/packages/neuron-wallet/src/block-sync-renderer/tx-status-listener.ts +++ b/packages/neuron-wallet/src/block-sync-renderer/tx-status-listener.ts @@ -7,6 +7,7 @@ import TransactionWithStatus from '../models/chain/transaction-with-status' import logger from '../utils/logger' import { getConnection } from '../database/chain/connection' import { interval } from 'rxjs' +import TxStatus from '../models/chain/tx-status' type TransactionDetail = { hash: string @@ -18,7 +19,8 @@ type TransactionDetail = { const getTransactionStatus = async (hash: string) => { const network = NetworksService.getInstance().getCurrent() const rpcService = new RpcService(network.remote, network.type) - const txWithStatus: TransactionWithStatus | undefined = await rpcService.getTransaction(hash) + const txWithStatus: TransactionWithStatus | undefined | { transaction: null; txStatus: TxStatus } = + await rpcService.getTransaction(hash) if (!txWithStatus) { return { tx: txWithStatus, @@ -33,6 +35,13 @@ const getTransactionStatus = async (hash: string) => { blockHash: txWithStatus.txStatus.blockHash, } } + if (txWithStatus.txStatus.isRejected()) { + return { + status: TransactionStatus.Failed, + isRejected: true, + blockHash: null, + } + } return { tx: txWithStatus.transaction, status: TransactionStatus.Pending, @@ -41,16 +50,16 @@ const getTransactionStatus = async (hash: string) => { } const trackingStatus = async () => { - const pendingTransactions = await FailedTransaction.pendings() + const pendingOrFailedTransactions = await FailedTransaction.pendingOrFaileds() await FailedTransaction.processAmendFailedTxs() - if (!pendingTransactions.length) { + if (!pendingOrFailedTransactions.length) { return } - const pendingHashes = pendingTransactions.map(tx => tx.hash) + const pendingOrFailedHashes = pendingOrFailedTransactions.map(tx => tx.hash) const txs = await Promise.all( - pendingHashes.map(async hash => { + pendingOrFailedHashes.map(async hash => { try { const txWithStatus = await getTransactionStatus(hash) return { @@ -58,6 +67,7 @@ const trackingStatus = async () => { tx: txWithStatus.tx, status: txWithStatus.status, blockHash: txWithStatus.blockHash, + isRejected: !!txWithStatus.isRejected, } } catch (error) { // ignore error, get failed skip current update @@ -66,16 +76,26 @@ const trackingStatus = async () => { ) const failedTxs = txs.filter( - (tx): tx is TransactionDetail & { status: TransactionStatus.Failed } => tx?.status === TransactionStatus.Failed + (tx): tx is (TransactionDetail | null) & { status: TransactionStatus.Failed; isRejected: boolean } => + tx?.status === TransactionStatus.Failed ) const successTxs = txs.filter( - (tx): tx is TransactionDetail & { status: TransactionStatus.Success } => tx?.status === TransactionStatus.Success + (tx): tx is (TransactionDetail | null) & { status: TransactionStatus.Success; isRejected: boolean } => + tx?.status === TransactionStatus.Success + ) + const rejectedTxs = txs.filter( + (tx): tx is (TransactionDetail | null) & { status: TransactionStatus.Failed; isRejected: boolean } => + !!tx?.isRejected ) if (failedTxs.length) { await FailedTransaction.updateFailedTxs(failedTxs.map(tx => tx.hash)) } + if (rejectedTxs.length) { + await FailedTransaction.deleteFailedTxs(rejectedTxs.map(tx => tx.hash)) + } + if (successTxs.length > 0) { const network = NetworksService.getInstance().getCurrent() const rpcService = new RpcService(network.remote, network.type) diff --git a/packages/neuron-wallet/src/models/chain/tx-status.ts b/packages/neuron-wallet/src/models/chain/tx-status.ts index a2c54c553a..ba2292e8d1 100644 --- a/packages/neuron-wallet/src/models/chain/tx-status.ts +++ b/packages/neuron-wallet/src/models/chain/tx-status.ts @@ -4,6 +4,7 @@ export enum TxStatusType { Pending = 'pending', Proposed = 'proposed', Committed = 'committed', + Rejected = 'rejected', } export default class TxStatus { @@ -28,6 +29,10 @@ export default class TxStatus { return this.status === TxStatusType.Committed } + public isRejected(): boolean { + return this.status === TxStatusType.Rejected + } + public toSDK() { return { blockHash: this.blockHash, diff --git a/packages/neuron-wallet/src/services/rpc-service.ts b/packages/neuron-wallet/src/services/rpc-service.ts index 8b59adf858..3f6aab645e 100644 --- a/packages/neuron-wallet/src/services/rpc-service.ts +++ b/packages/neuron-wallet/src/services/rpc-service.ts @@ -6,6 +6,7 @@ import TransactionWithStatus from '../models/chain/transaction-with-status' import logger from '../utils/logger' import { generateRPC } from '../utils/ckb-rpc' import { NetworkType } from '../models/network' +import TxStatus, { TxStatusType } from '../models/chain/tx-status' export default class RpcService { private retryTime: number @@ -29,20 +30,19 @@ export default class RpcService { return BlockHeader.fromSDK(result) } - /** - * TODO: rejected tx should be handled - * { - * transaction: null, - * txStatus: { blockHash: null, status: 'rejected' } - * } - */ - public async getTransaction(hash: string): Promise { + public async getTransaction( + hash: string + ): Promise { const result = await this.rpc.getTransaction(hash) if (result?.transaction) { return TransactionWithStatus.fromSDK(result) } - if ((result.txStatus as any) === 'rejected') { + if (result.txStatus.status === TxStatusType.Rejected) { logger.warn(`Transaction[${hash}] was rejected`) + return { + transaction: null, + txStatus: TxStatus.fromSDK(result.txStatus), + } } return undefined } diff --git a/packages/neuron-wallet/src/services/tx/failed-transaction.ts b/packages/neuron-wallet/src/services/tx/failed-transaction.ts index 996c53d336..96c8f754cf 100644 --- a/packages/neuron-wallet/src/services/tx/failed-transaction.ts +++ b/packages/neuron-wallet/src/services/tx/failed-transaction.ts @@ -9,16 +9,41 @@ import { TransactionStatus } from '../../models/chain/transaction' import AmendTransactionEntity from '../../database/chain/entities/amend-transaction' export class FailedTransaction { - public static pendings = async (): Promise => { - const pendingTransactions = await getConnection() + public static pendingOrFaileds = async (): Promise => { + const transactions = await getConnection() .getRepository(TransactionEntity) .createQueryBuilder('tx') .where({ - status: TransactionStatus.Pending, + status: In([TransactionStatus.Pending, TransactionStatus.Failed]), }) .getMany() - return pendingTransactions + return transactions + } + + public static deleteFailedTxs = async (hashes: string[]) => { + await getConnection().manager.transaction(async transactionalEntityManager => { + await transactionalEntityManager + .createQueryBuilder() + .delete() + .from(TransactionEntity) + .where({ hash: In(hashes) }) + .execute() + + await transactionalEntityManager + .createQueryBuilder() + .delete() + .from(OutputEntity) + .where({ outPointTxHash: In(hashes) }) + .execute() + + await transactionalEntityManager + .createQueryBuilder() + .delete() + .from(InputEntity) + .where({ outPointTxHash: In(hashes) }) + .execute() + }) } public static processAmendFailedTxs = async () => { @@ -56,28 +81,7 @@ export class FailedTransaction { } }) - await getConnection().manager.transaction(async transactionalEntityManager => { - await transactionalEntityManager - .createQueryBuilder() - .delete() - .from(TransactionEntity) - .where({ hash: In(removeTxs) }) - .execute() - - await transactionalEntityManager - .createQueryBuilder() - .delete() - .from(OutputEntity) - .where({ outPointTxHash: In(removeTxs) }) - .execute() - - await transactionalEntityManager - .createQueryBuilder() - .delete() - .from(InputEntity) - .where({ outPointTxHash: In(removeTxs) }) - .execute() - }) + await FailedTransaction.deleteFailedTxs(removeTxs) } // update tx status to TransactionStatus.Failed diff --git a/packages/neuron-wallet/src/services/tx/transaction-generator.ts b/packages/neuron-wallet/src/services/tx/transaction-generator.ts index 78746dc8b5..cb6d7528b9 100644 --- a/packages/neuron-wallet/src/services/tx/transaction-generator.ts +++ b/packages/neuron-wallet/src/services/tx/transaction-generator.ts @@ -72,7 +72,7 @@ export class TransactionGenerator { if (!nftCell) return const nftTx = await this.getRpcService().getTransaction(outPoint.txHash) - const nftOriginalOutputData = nftTx?.transaction.outputsData[Number(outPoint.index)] + const nftOriginalOutputData = nftTx?.transaction?.outputsData[Number(outPoint.index)] if (!nftOriginalOutputData) return nftCell.data = nftOriginalOutputData From c6e1fcd47c7b3fe13c85948aca90cd51f140d79f Mon Sep 17 00:00:00 2001 From: devchenyan Date: Tue, 9 Jul 2024 14:50:38 +0800 Subject: [PATCH 2/2] fix: comments --- .../block-sync-renderer/tx-status-listener.ts | 20 +++++-------------- .../src/models/chain/transaction.ts | 1 + 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/packages/neuron-wallet/src/block-sync-renderer/tx-status-listener.ts b/packages/neuron-wallet/src/block-sync-renderer/tx-status-listener.ts index 1877253587..3d996b3cf7 100644 --- a/packages/neuron-wallet/src/block-sync-renderer/tx-status-listener.ts +++ b/packages/neuron-wallet/src/block-sync-renderer/tx-status-listener.ts @@ -37,8 +37,8 @@ const getTransactionStatus = async (hash: string) => { } if (txWithStatus.txStatus.isRejected()) { return { - status: TransactionStatus.Failed, - isRejected: true, + tx: null, + status: TransactionStatus.Rejected, blockHash: null, } } @@ -67,7 +67,6 @@ const trackingStatus = async () => { tx: txWithStatus.tx, status: txWithStatus.status, blockHash: txWithStatus.blockHash, - isRejected: !!txWithStatus.isRejected, } } catch (error) { // ignore error, get failed skip current update @@ -75,18 +74,9 @@ const trackingStatus = async () => { }) ) - const failedTxs = txs.filter( - (tx): tx is (TransactionDetail | null) & { status: TransactionStatus.Failed; isRejected: boolean } => - tx?.status === TransactionStatus.Failed - ) - const successTxs = txs.filter( - (tx): tx is (TransactionDetail | null) & { status: TransactionStatus.Success; isRejected: boolean } => - tx?.status === TransactionStatus.Success - ) - const rejectedTxs = txs.filter( - (tx): tx is (TransactionDetail | null) & { status: TransactionStatus.Failed; isRejected: boolean } => - !!tx?.isRejected - ) + const failedTxs = txs.filter((tx): tx is TransactionDetail => tx?.status === TransactionStatus.Failed) + const successTxs = txs.filter((tx): tx is TransactionDetail => tx?.status === TransactionStatus.Success) + const rejectedTxs = txs.filter((tx): tx is TransactionDetail => tx?.status === TransactionStatus.Rejected) if (failedTxs.length) { await FailedTransaction.updateFailedTxs(failedTxs.map(tx => tx.hash)) diff --git a/packages/neuron-wallet/src/models/chain/transaction.ts b/packages/neuron-wallet/src/models/chain/transaction.ts index 613143dbb4..cc6a0ca38a 100644 --- a/packages/neuron-wallet/src/models/chain/transaction.ts +++ b/packages/neuron-wallet/src/models/chain/transaction.ts @@ -14,6 +14,7 @@ export enum TransactionStatus { Pending = 'pending', Success = 'success', Failed = 'failed', + Rejected = 'rejected', } export interface SudtTokenInfo {