From ad28a09078d501dd5b57b9529f45f28f36429740 Mon Sep 17 00:00:00 2001 From: devchenyan Date: Sun, 30 Jun 2024 22:48:26 +0800 Subject: [PATCH] feat: Periodic validation of pending transactions --- .../block-sync-renderer/tx-status-listener.ts | 35 +++++++++--- .../src/models/chain/tx-status.ts | 5 ++ .../neuron-wallet/src/services/rpc-service.ts | 20 ++++++- .../src/services/tx/failed-transaction.ts | 56 ++++++++++--------- 4 files changed, 82 insertions(+), 34 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 1a2532559b..3a20c5b8b8 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.getTransactionIncludeRejected(hash) if (!txWithStatus) { return { tx: txWithStatus, @@ -26,6 +28,13 @@ const getTransactionStatus = async (hash: string) => { blockHash: null, } } + if (txWithStatus.txStatus.isRejected()) { + return { + status: TransactionStatus.Failed, + isRejected: true, + blockHash: null, + } + } if (txWithStatus.txStatus.isCommitted()) { return { tx: txWithStatus.transaction, @@ -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,27 @@ const trackingStatus = async () => { ) const failedTxs = txs.filter( - (tx): tx is TransactionDetail & { status: TransactionStatus.Failed } => tx?.status === TransactionStatus.Failed + (tx): tx is TransactionDetail & { status: TransactionStatus.Failed; isRejected: undefined | boolean } => + tx?.status === TransactionStatus.Failed ) const successTxs = txs.filter( - (tx): tx is TransactionDetail & { status: TransactionStatus.Success } => tx?.status === TransactionStatus.Success + (tx): tx is TransactionDetail & { status: TransactionStatus.Success; isRejected: undefined | boolean } => + tx?.status === TransactionStatus.Success + ) + + const rejectedTxs = txs.filter( + (tx): tx is TransactionDetail & { status: TransactionStatus.Failed; isRejected: undefined | 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..7d9e5787fd 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,6 +30,23 @@ export default class RpcService { return BlockHeader.fromSDK(result) } + public async getTransactionIncludeRejected( + hash: string + ): Promise { + const result = await this.rpc.getTransaction(hash) + if (result?.transaction) { + return TransactionWithStatus.fromSDK(result) + } + if (result.txStatus.status === TxStatusType.Rejected) { + logger.warn(`Transaction[${hash}] was rejected`) + return { + transaction: null, + txStatus: TxStatus.fromSDK(result.txStatus), + } + } + return undefined + } + /** * TODO: rejected tx should be handled * { @@ -41,7 +59,7 @@ export default class RpcService { 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 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..a58c5b4b39 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() - }) + FailedTransaction.deleteFailedTxs(removeTxs) } // update tx status to TransactionStatus.Failed