From ca8861fbb73b29e9c5a30d4e98f280ef9e1938ec Mon Sep 17 00:00:00 2001 From: Danut Date: Thu, 19 Dec 2024 17:42:54 +0200 Subject: [PATCH] added transaction tracking --- .../TransactionManager/TransactionManager.ts | 19 ++- src/core/methods/initApp/initApp.ts | 7 +- src/core/methods/initApp/initApp.types.ts | 4 - .../checkTransactionStatus/checkBatch.ts | 18 +-- .../checkTransactionStatus.ts | 5 +- .../manageFailedTransactions.ts | 10 +- .../getPendingStoreTrackedTransactions.ts | 26 ++++ .../helpers/getPendingStoreTransactions.ts | 25 --- .../trackTransactions/trackTransactions.ts | 17 +-- src/store/actions/toasts/toastsActions.ts | 63 ++++++++ .../trackedTransactionsActions.ts | 107 +++++++++++++ .../transactionStateByStatus.ts | 143 ++++++++++++++++++ src/store/selectors/toastsSelectors.ts | 11 ++ .../selectors/trackedTransactionsSelector.ts | 40 +++++ src/store/slices/index.ts | 1 + src/store/slices/toast/index.ts | 1 + src/store/slices/toast/toastSlice.ts | 19 +++ src/store/slices/toast/toastSlice.types.ts | 82 ++++++++++ src/store/slices/trackedTransactions/index.ts | 1 + .../trackedTransactionsSlice.ts | 16 ++ .../trackedTransactionsSlice.types.ts | 11 ++ src/store/store.ts | 10 +- src/store/store.types.ts | 4 + 23 files changed, 570 insertions(+), 70 deletions(-) create mode 100644 src/core/methods/trackTransactions/helpers/getPendingStoreTrackedTransactions.ts delete mode 100644 src/core/methods/trackTransactions/helpers/getPendingStoreTransactions.ts create mode 100644 src/store/actions/toasts/toastsActions.ts create mode 100644 src/store/actions/trackedTransactions/trackedTransactionsActions.ts create mode 100644 src/store/actions/trackedTransactions/transactionStateByStatus.ts create mode 100644 src/store/selectors/toastsSelectors.ts create mode 100644 src/store/selectors/trackedTransactionsSelector.ts create mode 100644 src/store/slices/toast/index.ts create mode 100644 src/store/slices/toast/toastSlice.ts create mode 100644 src/store/slices/toast/toastSlice.types.ts create mode 100644 src/store/slices/trackedTransactions/index.ts create mode 100644 src/store/slices/trackedTransactions/trackedTransactionsSlice.ts create mode 100644 src/store/slices/trackedTransactions/trackedTransactionsSlice.types.ts diff --git a/src/core/managers/TransactionManager/TransactionManager.ts b/src/core/managers/TransactionManager/TransactionManager.ts index 599dbcb..4ad83a1 100644 --- a/src/core/managers/TransactionManager/TransactionManager.ts +++ b/src/core/managers/TransactionManager/TransactionManager.ts @@ -2,9 +2,10 @@ import { Transaction } from '@multiversx/sdk-core/out'; import axios, { AxiosError } from 'axios'; import { BATCH_TRANSACTIONS_ID_SEPARATOR } from 'constants/transactions.constants'; import { getAccount } from 'core/methods/account/getAccount'; +import { createTrackedTransactionsSession } from 'store/actions/trackedTransactions/trackedTransactionsActions'; import { networkSelector } from 'store/selectors'; import { getState } from 'store/store'; -import { GuardianActionsEnum } from 'types'; +import { GuardianActionsEnum, TransactionServerStatusesEnum } from 'types'; import { BatchTransactionsResponseType } from 'types/serverTransactions.types'; import { SignedTransactionType } from 'types/transactions.types'; @@ -55,6 +56,19 @@ export class TransactionManager { } }; + public track = async ( + signedTransactions: Transaction[], + options?: { enableToasts: boolean } + ) => { + const parsedTransactions = signedTransactions.map((transaction) => + this.parseSignedTransaction(transaction) + ); + createTrackedTransactionsSession({ + transactions: parsedTransactions, + enableToasts: options?.enableToasts ?? true + }); + }; + private sendSignedTransactions = async ( signedTransactions: Transaction[] ): Promise => { @@ -132,7 +146,8 @@ export class TransactionManager { private parseSignedTransaction = (signedTransaction: Transaction) => { const parsedTransaction = { ...signedTransaction.toPlainObject(), - hash: signedTransaction.getHash().hex() + hash: signedTransaction.getHash().hex(), + status: TransactionServerStatusesEnum.pending }; // TODO: Remove when the protocol supports usernames for guardian transactions diff --git a/src/core/methods/initApp/initApp.ts b/src/core/methods/initApp/initApp.ts index fbd1c7d..dbb972f 100644 --- a/src/core/methods/initApp/initApp.ts +++ b/src/core/methods/initApp/initApp.ts @@ -41,9 +41,6 @@ export async function initApp({ }: InitAppType) { initStore(storage.getStorageCallback); - const shouldEnableTransactionTracker = - dAppConfig.enableTansactionTracker !== false; - const { apiAddress } = await initializeNetwork({ customNetworkConfig: dAppConfig.network, environment: dAppConfig.environment @@ -66,9 +63,7 @@ export async function initApp({ setCrossWindowConfig(dAppConfig.providers.crossWindow); } - if (shouldEnableTransactionTracker) { - trackTransactions(); - } + trackTransactions(); const isLoggedIn = getIsLoggedIn(); diff --git a/src/core/methods/initApp/initApp.types.ts b/src/core/methods/initApp/initApp.types.ts index 0b8ad73..8716868 100644 --- a/src/core/methods/initApp/initApp.types.ts +++ b/src/core/methods/initApp/initApp.types.ts @@ -14,10 +14,6 @@ type BaseDappConfigType = { * If set to `NativeAuthConfigType`, will set the native auth configuration. */ nativeAuth?: boolean | NativeAuthConfigType; - /** - * default: `true` - */ - enableTansactionTracker?: boolean; providers?: { crossWindow?: CrossWindowConfig; walletConnect?: WalletConnectConfig; diff --git a/src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkBatch.ts b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkBatch.ts index e789db8..1fbbb61 100644 --- a/src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkBatch.ts +++ b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkBatch.ts @@ -1,8 +1,8 @@ import { getTransactionsByHashes } from 'apiCalls/transactions/getTransactionsByHashes'; import { - updateSignedTransactionStatus, - updateTransactionsSession -} from 'store/actions/transactions/transactionsActions'; + updateTrackedTransactionStatus, + updateTrackedTransactionsSession +} from 'store/actions/trackedTransactions/trackedTransactionsActions'; import { getIsTransactionFailed } from 'store/actions/transactions/transactionStateByStatus'; import { TransactionBatchStatusesEnum, @@ -63,7 +63,7 @@ function manageTransaction({ const retriesForThisHash = retries[hash]; if (retriesForThisHash > 30) { // consider transaction as stuck after 1 minute - updateTransactionsSession({ + updateTrackedTransactionsSession({ sessionId, status: TransactionBatchStatusesEnum.timedOut }); @@ -81,7 +81,7 @@ function manageTransaction({ // The tx is from a sequential batch. // If the transactions before this are not successful then it means that no other tx will be processed if (isSequential && !status) { - updateSignedTransactionStatus({ + updateTrackedTransactionStatus({ sessionId, status, transactionHash: hash, @@ -92,7 +92,7 @@ function manageTransaction({ } if (hasStatusChanged) { - updateSignedTransactionStatus({ + updateTrackedTransactionStatus({ sessionId, status, transactionHash: hash, @@ -111,7 +111,7 @@ function manageTransaction({ } } catch (error) { console.error(error); - updateTransactionsSession({ + updateTrackedTransactionsSession({ sessionId, status: TransactionBatchStatusesEnum.timedOut }); @@ -159,7 +159,7 @@ export async function checkBatch({ ); if (isSuccessful) { - updateTransactionsSession({ + updateTrackedTransactionsSession({ sessionId, status: TransactionBatchStatusesEnum.success }); @@ -171,7 +171,7 @@ export async function checkBatch({ ); if (isFailed) { - updateTransactionsSession({ + updateTrackedTransactionsSession({ sessionId, status: TransactionBatchStatusesEnum.fail }); diff --git a/src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkTransactionStatus.ts b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkTransactionStatus.ts index bf9877c..86eb92f 100644 --- a/src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkTransactionStatus.ts +++ b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkTransactionStatus.ts @@ -1,14 +1,15 @@ import { refreshAccount } from 'utils/account'; import { checkBatch } from './checkBatch'; import { TransactionsTrackerType } from '../../trackTransactions.types'; -import { getPendingStoreTransactions } from '../getPendingStoreTransactions'; +import { getPendingStoreTrackedTransactions } from '../getPendingStoreTrackedTransactions'; export async function checkTransactionStatus( props: TransactionsTrackerType & { shouldRefreshBalance?: boolean; } ) { - const { pendingSessions } = getPendingStoreTransactions(); + const { pendingTrackedSessions: pendingSessions } = + getPendingStoreTrackedTransactions(); if (Object.keys(pendingSessions).length > 0) { for (const [sessionId, { transactions }] of Object.entries( pendingSessions diff --git a/src/core/methods/trackTransactions/helpers/checkTransactionStatus/manageFailedTransactions.ts b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/manageFailedTransactions.ts index 9340877..6c661c6 100644 --- a/src/core/methods/trackTransactions/helpers/checkTransactionStatus/manageFailedTransactions.ts +++ b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/manageFailedTransactions.ts @@ -1,7 +1,7 @@ import { - updateSignedTransactionStatus, - updateTransactionsSession -} from 'store/actions/transactions/transactionsActions'; + updateTrackedTransactionStatus, + updateTrackedTransactionsSession +} from 'store/actions/trackedTransactions/trackedTransactionsActions'; import { TransactionBatchStatusesEnum, TransactionServerStatusesEnum @@ -22,7 +22,7 @@ export function manageFailedTransactions({ (scResult) => scResult?.returnMessage !== '' ); - updateSignedTransactionStatus({ + updateTrackedTransactionStatus({ transactionHash: hash, sessionId, status: TransactionServerStatusesEnum.fail, @@ -31,7 +31,7 @@ export function manageFailedTransactions({ serverTransaction: resultWithError as unknown as ServerTransactionType }); - updateTransactionsSession({ + updateTrackedTransactionsSession({ sessionId, status: TransactionBatchStatusesEnum.fail, errorMessage: resultWithError?.returnMessage diff --git a/src/core/methods/trackTransactions/helpers/getPendingStoreTrackedTransactions.ts b/src/core/methods/trackTransactions/helpers/getPendingStoreTrackedTransactions.ts new file mode 100644 index 0000000..f0fc62e --- /dev/null +++ b/src/core/methods/trackTransactions/helpers/getPendingStoreTrackedTransactions.ts @@ -0,0 +1,26 @@ +import { + pendingTrackedSessionsSelector, + pendingTrackedTransactionsSelector +} from 'store/selectors/trackedTransactionsSelector'; +import { TrackedTransactionsSliceType } from 'store/slices/trackedTransactions/trackedTransactionsSlice.types'; +import { getState } from 'store/store'; +import { SignedTransactionType } from 'types/transactions.types'; + +export interface UseGetPendingTrackedTransactionsReturnType { + pendingTrackedTransactions: SignedTransactionType[]; + pendingTrackedSessions: TrackedTransactionsSliceType; + hasPendingTrackedTransactions: boolean; +} + +export function getPendingStoreTrackedTransactions(): UseGetPendingTrackedTransactionsReturnType { + const pendingTrackedTransactions = + pendingTrackedTransactionsSelector(getState()); + const pendingTrackedSessions = pendingTrackedSessionsSelector(getState()); + const hasPendingTrackedTransactions = pendingTrackedTransactions.length > 0; + + return { + pendingTrackedTransactions, + pendingTrackedSessions, + hasPendingTrackedTransactions + }; +} diff --git a/src/core/methods/trackTransactions/helpers/getPendingStoreTransactions.ts b/src/core/methods/trackTransactions/helpers/getPendingStoreTransactions.ts deleted file mode 100644 index 7d870b6..0000000 --- a/src/core/methods/trackTransactions/helpers/getPendingStoreTransactions.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - pendingSessionsSelector, - pendingTransactionsSelector -} from 'store/selectors/transactionsSelector'; -import { TransactionsSliceType } from 'store/slices/transactions/transacitionsSlice.types'; -import { getState } from 'store/store'; -import { SignedTransactionType } from 'types/transactions.types'; - -export interface UseGetPendingTransactionsReturnType { - pendingTransactions: SignedTransactionType[]; - pendingSessions: TransactionsSliceType; - hasPendingTransactions: boolean; -} - -export function getPendingStoreTransactions(): UseGetPendingTransactionsReturnType { - const pendingTransactions = pendingTransactionsSelector(getState()); - const pendingSessions = pendingSessionsSelector(getState()); - const hasPendingTransactions = pendingTransactions.length > 0; - - return { - pendingTransactions, - pendingSessions, - hasPendingTransactions - }; -} diff --git a/src/core/methods/trackTransactions/trackTransactions.ts b/src/core/methods/trackTransactions/trackTransactions.ts index 62b9f8a..e0e6c15 100644 --- a/src/core/methods/trackTransactions/trackTransactions.ts +++ b/src/core/methods/trackTransactions/trackTransactions.ts @@ -1,9 +1,8 @@ -import { getTransactionsByHashes as defaultGetTxByHash } from 'apiCalls/transactions/getTransactionsByHashes'; +import { getTransactionsByHashes } from 'apiCalls/transactions/getTransactionsByHashes'; import { websocketEventSelector } from 'store/selectors/accountSelectors'; import { getStore } from 'store/store'; import { checkTransactionStatus } from './helpers/checkTransactionStatus'; import { getPollingInterval } from './helpers/getPollingInterval'; -import { TransactionsTrackerType } from './trackTransactions.types'; import { websocketConnection, WebsocketConnectionStatusEnum @@ -11,34 +10,25 @@ import { /** * Tracks transactions using websocket or polling - * @param props - optional object with additional websocket parameters * @returns cleanup function */ -export async function trackTransactions(props?: TransactionsTrackerType) { +export async function trackTransactions() { const store = getStore(); const pollingInterval = getPollingInterval(); // eslint-disable-next-line no-undef let pollingIntervalTimer: NodeJS.Timeout | null = null; let timestamp = websocketEventSelector(store.getState())?.timestamp; - // Check if websocket is completed const isWebsocketCompleted = websocketConnection.status === WebsocketConnectionStatusEnum.COMPLETED; - // Assign getTransactionsByHash based on props or use default - const getTransactionsByHash = - props?.getTransactionsByHash ?? defaultGetTxByHash; - - // Function that handles message (checking transaction status) const recheckStatus = () => { checkTransactionStatus({ shouldRefreshBalance: isWebsocketCompleted, - getTransactionsByHash, - ...props + getTransactionsByHash: getTransactionsByHashes }); }; - // recheck on page initial page load recheckStatus(); if (isWebsocketCompleted) { @@ -60,7 +50,6 @@ export async function trackTransactions(props?: TransactionsTrackerType) { } } - // Return cleanup function for clearing the interval function cleanup() { if (pollingIntervalTimer) { clearInterval(pollingIntervalTimer); diff --git a/src/store/actions/toasts/toastsActions.ts b/src/store/actions/toasts/toastsActions.ts new file mode 100644 index 0000000..a1e8071 --- /dev/null +++ b/src/store/actions/toasts/toastsActions.ts @@ -0,0 +1,63 @@ +import { + CustomToastType, + ToastsEnum +} from 'store/slices/toast/toastSlice.types'; +import { getStore } from 'store/store'; +import { getUnixTimestamp } from 'utils'; + +export const addCustomToast = ( + customToasts: CustomToastType, + currentToastId?: string +) => { + getStore().setState(({ toasts: state }) => { + const toastId = + currentToastId || `custom-toast-${state.customToasts.length + 1}`; + + const existingToastIndex = state.customToasts.findIndex( + (toast) => toast.toastId === toastId + ); + + if (existingToastIndex !== -1) { + state.customToasts[existingToastIndex] = { + ...state.customToasts[existingToastIndex], + ...customToasts, + type: ToastsEnum.custom, + toastId + } as CustomToastType; + return; + } + + state.customToasts.push({ + ...customToasts, + type: ToastsEnum.custom, + toastId + }); + }); +}; + +export const removeCustomToast = (toastId: string) => { + getStore().setState(({ toasts: state }) => { + state.customToasts = state.customToasts.filter( + (toast) => toast.toastId !== toastId + ); + }); +}; + +export const addTransactionToast = (toastId: string) => { + getStore().setState(({ toasts: state }) => { + state.transactionToasts.push({ + type: ToastsEnum.transaction, + startTimestamp: getUnixTimestamp(), + toastId: + toastId || `transaction-toast-${state.transactionToasts.length + 1}` + }); + }); +}; + +export const removeTransactionToast = (toastId: string) => { + getStore().setState(({ toasts: state }) => { + state.transactionToasts = state.transactionToasts.filter((toast) => { + return toast.toastId !== toastId; + }); + }); +}; diff --git a/src/store/actions/trackedTransactions/trackedTransactionsActions.ts b/src/store/actions/trackedTransactions/trackedTransactionsActions.ts new file mode 100644 index 0000000..adadaaa --- /dev/null +++ b/src/store/actions/trackedTransactions/trackedTransactionsActions.ts @@ -0,0 +1,107 @@ +import { getStore } from 'store/store'; +import { + TransactionBatchStatusesEnum, + TransactionServerStatusesEnum +} from 'types/enums.types'; +import { ServerTransactionType } from 'types/serverTransactions.types'; +import { SignedTransactionType } from 'types/transactions.types'; +import { + getIsTransactionFailed, + getIsTransactionNotExecuted, + getIsTransactionSuccessful +} from './transactionStateByStatus'; + +export interface UpdateTrackedTransactionStatusPayloadType { + sessionId: string; + transactionHash: string; + status: TransactionServerStatusesEnum | TransactionBatchStatusesEnum; + serverTransaction?: ServerTransactionType; + errorMessage?: string; + inTransit?: boolean; +} + +export const createTrackedTransactionsSession = ({ + transactions, + enableToasts = true +}: { + transactions: SignedTransactionType[]; + enableToasts?: boolean; +}) => { + const sessionId = Date.now().toString(); + getStore().setState(({ trackedTransactions: state }) => { + state[sessionId] = { + transactions, + status: TransactionBatchStatusesEnum.sent, + enableToasts + }; + }); + return sessionId; +}; + +export const updateTrackedTransactionsSession = ({ + sessionId, + status, + errorMessage +}: { + sessionId: string; + status: TransactionBatchStatusesEnum; + errorMessage?: string; +}) => { + getStore().setState(({ trackedTransactions: state }) => { + state[sessionId].status = status; + state[sessionId].errorMessage = errorMessage; + }); +}; + +export const updateTrackedTransactionStatus = ( + payload: UpdateTrackedTransactionStatusPayloadType +) => { + const { + sessionId, + status, + errorMessage, + transactionHash, + serverTransaction, + inTransit + } = payload; + getStore().setState(({ trackedTransactions: state }) => { + const transactions = state[sessionId]?.transactions; + if (transactions != null) { + state[sessionId].transactions = transactions.map((transaction) => { + if (transaction.hash === transactionHash) { + return { + ...(serverTransaction ?? {}), + ...transaction, + status: status as TransactionServerStatusesEnum, + errorMessage, + inTransit + }; + } + return transaction; + }); + const areTransactionsSuccessful = state[sessionId]?.transactions?.every( + (transaction) => { + return getIsTransactionSuccessful(transaction.status); + } + ); + + const areTransactionsFailed = state[sessionId]?.transactions?.some( + (transaction) => getIsTransactionFailed(transaction.status) + ); + + const areTransactionsNotExecuted = state[sessionId]?.transactions?.every( + (transaction) => getIsTransactionNotExecuted(transaction.status) + ); + + if (areTransactionsSuccessful) { + state[sessionId].status = TransactionBatchStatusesEnum.success; + } + if (areTransactionsFailed) { + state[sessionId].status = TransactionBatchStatusesEnum.fail; + } + if (areTransactionsNotExecuted) { + state[sessionId].status = TransactionBatchStatusesEnum.invalid; + } + } + }); +}; diff --git a/src/store/actions/trackedTransactions/transactionStateByStatus.ts b/src/store/actions/trackedTransactions/transactionStateByStatus.ts new file mode 100644 index 0000000..4397715 --- /dev/null +++ b/src/store/actions/trackedTransactions/transactionStateByStatus.ts @@ -0,0 +1,143 @@ +import { + TransactionBatchStatusesEnum, + TransactionServerStatusesEnum +} from 'types/enums.types'; + +export const pendingBatchTransactionsStates = [ + TransactionBatchStatusesEnum.sent +]; + +export const successBatchTransactionsStates = [ + TransactionBatchStatusesEnum.success +]; + +export const failBatchTransactionsStates = [ + TransactionBatchStatusesEnum.fail, + TransactionBatchStatusesEnum.cancelled, + TransactionBatchStatusesEnum.timedOut +]; + +export const invalidBatchTransactionsStates = [ + TransactionBatchStatusesEnum.invalid +]; + +export const timedOutBatchTransactionsStates = [ + TransactionBatchStatusesEnum.timedOut +]; + +export const pendingServerTransactionsStatuses = [ + TransactionServerStatusesEnum.pending +]; + +export const successServerTransactionsStates = [ + TransactionServerStatusesEnum.success +]; + +export const failServerTransactionsStates = [ + TransactionServerStatusesEnum.fail, + TransactionServerStatusesEnum.invalid +]; + +export const notExecutedServerTransactionsStates = [ + TransactionServerStatusesEnum.notExecuted +]; + +export function getIsTransactionPending( + status?: TransactionServerStatusesEnum | TransactionBatchStatusesEnum +) { + return ( + status != null && + (isBatchTransactionPending(status as TransactionBatchStatusesEnum) || + isServerTransactionPending(status as TransactionServerStatusesEnum)) + ); +} + +export function getIsTransactionSuccessful( + status?: TransactionServerStatusesEnum | TransactionBatchStatusesEnum +) { + return ( + status != null && + (isBatchTransactionSuccessful(status as TransactionBatchStatusesEnum) || + isServerTransactionSuccessful(status as TransactionServerStatusesEnum)) + ); +} + +export function getIsTransactionFailed( + status?: TransactionServerStatusesEnum | TransactionBatchStatusesEnum +) { + return ( + status != null && + (isBatchTransactionFailed(status as TransactionBatchStatusesEnum) || + isServerTransactionFailed(status as TransactionServerStatusesEnum)) + ); +} + +export function getIsTransactionNotExecuted( + status?: TransactionServerStatusesEnum | TransactionBatchStatusesEnum +) { + return ( + status != null && + (isBatchTransactionInvalid(status as TransactionBatchStatusesEnum) || + isServerTransactionNotExecuted(status as TransactionServerStatusesEnum)) + ); +} + +export function getIsTransactionTimedOut( + status?: TransactionServerStatusesEnum | TransactionBatchStatusesEnum +) { + return ( + status != null && + isBatchTransactionTimedOut(status as TransactionBatchStatusesEnum) + ); +} + +export function isBatchTransactionPending( + status?: TransactionBatchStatusesEnum +) { + return status != null && pendingBatchTransactionsStates.includes(status); +} + +export function isBatchTransactionSuccessful( + status: TransactionBatchStatusesEnum +) { + return status != null && successBatchTransactionsStates.includes(status); +} + +export function isBatchTransactionFailed(status: TransactionBatchStatusesEnum) { + return status != null && failBatchTransactionsStates.includes(status); +} + +export function isBatchTransactionInvalid( + status: TransactionBatchStatusesEnum +) { + return status != null && invalidBatchTransactionsStates.includes(status); +} + +export function isBatchTransactionTimedOut( + status?: TransactionBatchStatusesEnum +) { + return status != null && timedOutBatchTransactionsStates.includes(status); +} + +export function isServerTransactionPending( + status?: TransactionServerStatusesEnum +) { + return status != null && pendingServerTransactionsStatuses.includes(status); +} +export function isServerTransactionSuccessful( + status: TransactionServerStatusesEnum +) { + return status != null && successServerTransactionsStates.includes(status); +} + +export function isServerTransactionFailed( + status: TransactionServerStatusesEnum +) { + return status != null && failServerTransactionsStates.includes(status); +} + +export function isServerTransactionNotExecuted( + status: TransactionServerStatusesEnum +) { + return status != null && notExecutedServerTransactionsStates.includes(status); +} diff --git a/src/store/selectors/toastsSelectors.ts b/src/store/selectors/toastsSelectors.ts new file mode 100644 index 0000000..2e4a1c7 --- /dev/null +++ b/src/store/selectors/toastsSelectors.ts @@ -0,0 +1,11 @@ +import { StoreType } from 'store/store.types'; + +export const networkSliceSelector = ({ network }: StoreType) => network; + +export const toastsSliceSelector = ({ toasts }: StoreType) => toasts; + +export const customToastsSelector = ({ toasts }: StoreType) => + toasts.customToasts; + +export const transactionToastsSelector = ({ toasts }: StoreType) => + toasts.transactionToasts; diff --git a/src/store/selectors/trackedTransactionsSelector.ts b/src/store/selectors/trackedTransactionsSelector.ts new file mode 100644 index 0000000..6a299f7 --- /dev/null +++ b/src/store/selectors/trackedTransactionsSelector.ts @@ -0,0 +1,40 @@ +import { TrackedTransactionsSliceType } from 'store/slices/trackedTransactions/trackedTransactionsSlice.types'; +import { StoreType } from 'store/store.types'; +import { TransactionServerStatusesEnum } from 'types/enums.types'; +import { SignedTransactionType } from 'types/transactions.types'; + +export const transactionsSliceSelector = ({ trackedTransactions }: StoreType) => + trackedTransactions; + +export const pendingTrackedSessionsSelector = ({ + trackedTransactions: state +}: StoreType): TrackedTransactionsSliceType => { + const pendingSessions: TrackedTransactionsSliceType = {}; + + Object.entries(state).forEach(([sessionId, data]) => { + const hasPendingTransactions = data.transactions.some( + ({ status }) => status === TransactionServerStatusesEnum.pending + ); + if (hasPendingTransactions && data.status === 'sent') { + pendingSessions[sessionId] = data; + } + }); + + return pendingSessions; +}; + +export const pendingTrackedTransactionsSelector = ({ + trackedTransactions: state +}: StoreType) => { + const pendingTransactions: SignedTransactionType[] = []; + + Object.values(state).forEach(({ transactions }) => { + transactions.forEach((transaction) => { + if (transaction.status === TransactionServerStatusesEnum.pending) { + pendingTransactions.push(transaction); + } + }); + }); + + return pendingTransactions; +}; diff --git a/src/store/slices/index.ts b/src/store/slices/index.ts index aea5138..c967642 100644 --- a/src/store/slices/index.ts +++ b/src/store/slices/index.ts @@ -3,3 +3,4 @@ export * from './network'; export * from './loginInfo'; export * from './config'; export * from './transactions'; +export * from './toast'; diff --git a/src/store/slices/toast/index.ts b/src/store/slices/toast/index.ts new file mode 100644 index 0000000..0f54e79 --- /dev/null +++ b/src/store/slices/toast/index.ts @@ -0,0 +1 @@ +export { toastSlice } from './toastSlice'; diff --git a/src/store/slices/toast/toastSlice.ts b/src/store/slices/toast/toastSlice.ts new file mode 100644 index 0000000..42973b2 --- /dev/null +++ b/src/store/slices/toast/toastSlice.ts @@ -0,0 +1,19 @@ +import { StateCreator } from 'zustand/vanilla'; +import { StoreType, MutatorsIn } from 'store/store.types'; +import { ToastsSliceState } from './toastSlice.types'; + +const initialState: ToastsSliceState = { + customToasts: [], + transactionToasts: [] +}; + +function getToastSlice(): StateCreator< + StoreType, + MutatorsIn, + [], + ToastsSliceState +> { + return () => initialState; +} + +export const toastSlice = getToastSlice(); diff --git a/src/store/slices/toast/toastSlice.types.ts b/src/store/slices/toast/toastSlice.types.ts new file mode 100644 index 0000000..0d0557d --- /dev/null +++ b/src/store/slices/toast/toastSlice.types.ts @@ -0,0 +1,82 @@ +import { JSX } from 'react'; +import { ServerTransactionType } from 'types/serverTransactions.types'; +import { SignedTransactionType } from 'types/transactions.types'; + +export interface ToastsSliceState { + customToasts: CustomToastType[]; + transactionToasts: TransactionToastType[]; +} + +interface SharedCustomToast { + toastId: string; + /** + * Duration in miliseconds + */ + duration?: number; + type?: string; + onClose?: () => void; +} +export interface MessageCustomToastType extends SharedCustomToast { + message: string; + icon?: never; + iconClassName?: never; + title?: never; + status?: never; + transaction?: never; + component?: never; +} +interface SharedIconToastType extends SharedCustomToast { + icon: string; + iconClassName?: string; + title: string; +} +export interface MessageIconToastType extends SharedIconToastType { + message: string; + /** + * Use `status` to display a row of information between `title` and `message` + */ + status?: string; + transaction?: never; + component?: never; +} + +export interface TransactionIconToastType extends SharedIconToastType { + transaction: ServerTransactionType; + component?: never; + message?: never; + status?: never; +} + +export interface ComponentIconToastType extends SharedIconToastType { + /** + * Use `component` to display a custom React compnent + * + * **⚠️ Warning**: Toasts with components will not be persisted on page reload because React components are not serializable + */ + component: (() => JSX.Element) | null; + transaction?: never; + message?: never; + status?: never; +} + +export type CustomToastType = + | MessageCustomToastType + | MessageIconToastType + | ComponentIconToastType + | TransactionIconToastType; + +export interface TransactionToastType { + duration?: number; + icon?: string; + iconClassName?: string; + startTimestamp: number; + title?: string; + toastId: string; + transaction?: SignedTransactionType; + type: string; +} + +export enum ToastsEnum { + custom = 'custom', + transaction = 'transaction' +} diff --git a/src/store/slices/trackedTransactions/index.ts b/src/store/slices/trackedTransactions/index.ts new file mode 100644 index 0000000..1a48a65 --- /dev/null +++ b/src/store/slices/trackedTransactions/index.ts @@ -0,0 +1 @@ +export { trackedTransactionsSlice } from './trackedTransactionsSlice'; diff --git a/src/store/slices/trackedTransactions/trackedTransactionsSlice.ts b/src/store/slices/trackedTransactions/trackedTransactionsSlice.ts new file mode 100644 index 0000000..dc84674 --- /dev/null +++ b/src/store/slices/trackedTransactions/trackedTransactionsSlice.ts @@ -0,0 +1,16 @@ +import { StateCreator } from 'zustand/vanilla'; +import { StoreType, MutatorsIn } from 'store/store.types'; +import { TrackedTransactionsSliceType } from './trackedTransactionsSlice.types'; + +const initialState: TrackedTransactionsSliceType = {}; + +function getTrackedTransactionsSlice(): StateCreator< + StoreType, + MutatorsIn, + [], + TrackedTransactionsSliceType +> { + return () => initialState; +} + +export const trackedTransactionsSlice = getTrackedTransactionsSlice(); diff --git a/src/store/slices/trackedTransactions/trackedTransactionsSlice.types.ts b/src/store/slices/trackedTransactions/trackedTransactionsSlice.types.ts new file mode 100644 index 0000000..a323499 --- /dev/null +++ b/src/store/slices/trackedTransactions/trackedTransactionsSlice.types.ts @@ -0,0 +1,11 @@ +import { TransactionBatchStatusesEnum } from 'types'; +import { SignedTransactionType } from 'types/transactions.types'; + +export interface TrackedTransactionsSliceType { + [sessionId: string]: { + transactions: SignedTransactionType[]; + status?: TransactionBatchStatusesEnum; + errorMessage?: string; + enableToasts?: boolean; + }; +} diff --git a/src/store/store.ts b/src/store/store.ts index 9ddab8e..2b3a616 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -6,9 +6,11 @@ import { networkSlice, accountSlice, loginInfoSlice, - transactionsSlice + transactionsSlice, + configSlice, + toastSlice } from './slices'; -import { configSlice } from './slices'; +import { trackedTransactionsSlice } from './slices/trackedTransactions'; import { InMemoryStorage, defaultStorageCallback, @@ -37,7 +39,9 @@ export const createDAppStore = (getStorageCallback: StorageCallback) => { transactions: transactionsSlice(...args), account: accountSlice(...args), loginInfo: loginInfoSlice(...args), - config: configSlice(...args) + config: configSlice(...args), + trackedTransactions: trackedTransactionsSlice(...args), + toasts: toastSlice(...args) })), { name: 'sdk-dapp-store', diff --git a/src/store/store.types.ts b/src/store/store.types.ts index e679a97..277f634 100644 --- a/src/store/store.types.ts +++ b/src/store/store.types.ts @@ -2,6 +2,8 @@ import { AccountSliceType } from './slices/account/account.types'; import { ConfigSliceType } from './slices/config/config.types'; import { LoginInfoSliceType } from './slices/loginInfo/loginInfo.types'; import { NetworkSliceType } from './slices/network/networkSlice.types'; +import { ToastsSliceState } from './slices/toast/toastSlice.types'; +import { TrackedTransactionsSliceType } from './slices/trackedTransactions/trackedTransactionsSlice.types'; import { TransactionsSliceType } from './slices/transactions/transacitionsSlice.types'; export type StoreType = { @@ -10,6 +12,8 @@ export type StoreType = { loginInfo: LoginInfoSliceType; config: ConfigSliceType; transactions: TransactionsSliceType; + toasts: ToastsSliceState; + trackedTransactions: TrackedTransactionsSliceType; }; export type MutatorsIn = [