From 9fc6afb104aac65fc6999c8d6fb52554d6a7717c Mon Sep 17 00:00:00 2001 From: Ciprian Draghici Date: Wed, 6 Sep 2023 17:46:56 +0300 Subject: [PATCH 1/7] Batch transactions improvements (#905) --- package.json | 2 +- src/apiCalls/transactions/index.ts | 1 + .../sendSignedBatchTransactions.ts | 60 +++++ .../BatchTransactionsSender.ts | 194 ---------------- .../BatchTransactionsSender/index.ts | 1 - .../BatchTransactionsTracker.tsx | 9 - .../TransactionSender/TransactionSender.ts | 164 +++++++------ .../types/transactionSender.types.ts | 15 ++ .../handleSendBatchTransactionsErrors.ts | 33 +++ .../utils/handleSendTransactionsErrors.ts | 23 ++ .../utils/invokeSendTransactions.ts | 127 +++++++++++ .../TransactionTracker.tsx | 14 +- .../useTransactionsTracker.ts | 9 +- .../tracker/useBatchTransactionsTracker.ts | 4 +- .../useCheckBatchesOnWsFailureFallback.ts | 4 +- .../tracker/useCheckHangingBatchesFallback.ts | 8 +- .../batch/tracker/useVerifyBatchStatus.ts | 14 +- .../batch/useSendBatchTransactions.ts | 24 +- .../useCheckTransactionStatus/checkBatch.ts | 36 ++- .../useCheckTransactionStatus.ts | 24 +- src/reduxStore/slices/transactionsSlice.ts | 21 +- .../sendBatchTransactions.spec.ts | 215 ++++++++++++++++++ .../transactions/sendBatchTransactions.ts | 76 ++++--- src/services/transactions/sendTransactions.ts | 20 +- src/services/transactions/signTransactions.ts | 6 +- .../transactions/updateSignedTransactions.ts | 11 +- .../utils/transformTransactionsToSign.ts | 26 +++ src/types/serverTransactions.types.ts | 7 +- src/types/transactions.types.ts | 18 +- src/types/transactionsTracker.types.ts | 7 + .../generateBatchTransactionsGrouping.spec.ts | 74 ++++++ .../generateBatchTransactionsGrouping.ts | 11 + .../DappProvider/CustomComponents.tsx | 50 +--- src/wrappers/DappProvider/DappProvider.tsx | 9 +- 34 files changed, 875 insertions(+), 442 deletions(-) create mode 100644 src/apiCalls/transactions/sendSignedBatchTransactions.ts delete mode 100644 src/components/BatchTransactionsSender/index.ts delete mode 100644 src/components/BatchTransactionsTracker/BatchTransactionsTracker.tsx create mode 100644 src/components/TransactionSender/types/transactionSender.types.ts create mode 100644 src/components/TransactionSender/utils/handleSendBatchTransactionsErrors.ts create mode 100644 src/components/TransactionSender/utils/handleSendTransactionsErrors.ts create mode 100644 src/components/TransactionSender/utils/invokeSendTransactions.ts create mode 100644 src/services/transactions/sendBatchTransactions.spec.ts create mode 100644 src/services/transactions/utils/transformTransactionsToSign.ts create mode 100644 src/types/transactionsTracker.types.ts create mode 100644 src/utils/transactions/batch/generateBatchTransactionsGrouping.spec.ts create mode 100644 src/utils/transactions/batch/generateBatchTransactionsGrouping.ts diff --git a/package.json b/package.json index 0353394a7..8fe9fcbf4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@multiversx/sdk-dapp", - "version": "2.19.9", + "version": "2.20.0-alpha.1", "description": "A library to hold the main logic for a dapp on the MultiversX blockchain", "author": "MultiversX", "license": "GPL-3.0-or-later", diff --git a/src/apiCalls/transactions/index.ts b/src/apiCalls/transactions/index.ts index 1570e9ab5..f3a3ff3d5 100644 --- a/src/apiCalls/transactions/index.ts +++ b/src/apiCalls/transactions/index.ts @@ -1,4 +1,5 @@ export * from './sendSignedTransactions'; +export * from './sendSignedBatchTransactions'; export * from './getTransactionsByHashes'; export * from './getTransactions'; export * from './getTransactionsCount'; diff --git a/src/apiCalls/transactions/sendSignedBatchTransactions.ts b/src/apiCalls/transactions/sendSignedBatchTransactions.ts new file mode 100644 index 000000000..5790bb3f5 --- /dev/null +++ b/src/apiCalls/transactions/sendSignedBatchTransactions.ts @@ -0,0 +1,60 @@ +import axios from 'axios'; +import { TIMEOUT } from 'constants/network'; +import { buildBatchId } from 'hooks/transactions/helpers/buildBatchId'; +import { addressSelector, networkSelector } from 'reduxStore/selectors'; +import { store } from 'reduxStore/store'; +import { + BatchTransactionsRequestType, + BatchTransactionsResponseType, + CustomTransactionInformation, + SignedTransactionType +} from 'types'; +import { TRANSACTIONS_BATCH } from '../endpoints'; + +export interface SendBatchTransactionsPropsType { + transactions: SignedTransactionType[][]; + sessionId: string; + customTransactionInformationOverrides?: Partial; +} + +export type SendSignedBatchTransactionsReturnType = { + error?: string | null; + batchId?: string | null; + data?: BatchTransactionsResponseType; +}; + +export async function sendSignedBatchTransactions({ + transactions, + sessionId +}: SendBatchTransactionsPropsType) { + const address = addressSelector(store.getState()); + const { apiAddress, apiTimeout } = networkSelector(store.getState()); + + try { + const batchId = buildBatchId({ + sessionId, + address + }); + + const payload: BatchTransactionsRequestType = { + transactions, + id: batchId + }; + + const response = await axios.post( + `${apiAddress}/${TRANSACTIONS_BATCH}`, + payload, + { + timeout: Number(apiTimeout ?? TIMEOUT) + } + ); + + return { batchId, data: response.data }; + } catch (err) { + console.error('error sending batch transactions', err); + return { + error: (err as any)?.message ?? 'error sending batch transactions', + batchId: null + }; + } +} diff --git a/src/components/BatchTransactionsSender/BatchTransactionsSender.ts b/src/components/BatchTransactionsSender/BatchTransactionsSender.ts index d75b565f6..e69de29bb 100644 --- a/src/components/BatchTransactionsSender/BatchTransactionsSender.ts +++ b/src/components/BatchTransactionsSender/BatchTransactionsSender.ts @@ -1,194 +0,0 @@ -import { useCallback, useEffect, useRef } from 'react'; -import { useDispatch, useSelector } from 'reduxStore/DappProviderContext'; -import { - accountSelector, - signedTransactionsSelector -} from 'reduxStore/selectors'; -import { - clearAllTransactionsToSign, - setTxSubmittedModal, - setBatchTransactions, - updateSignedTransactions -} from 'reduxStore/slices'; -import { sendBatchTransactions } from 'services/transactions/sendBatchTransactions'; -import { - TransactionBatchStatusesEnum, - TransactionServerStatusesEnum -} from 'types/enums.types'; -import { - SignedTransactionsBodyType, - SignedTransactionType -} from 'types/transactions.types'; -import { setNonce } from 'utils/account/setNonce'; -import { safeRedirect } from 'utils/redirect'; -import { sequentialToFlatArray } from 'utils/transactions/batch/sequentialToFlatArray'; -import { removeTransactionParamsFromUrl } from 'utils/transactions/removeTransactionParamsFromUrl'; - -/** - * Function used to redirect after sending because of Safari cancelling async requests on page change - */ -const optionalRedirect = (sessionInformation: SignedTransactionsBodyType) => { - const redirectRoute = sessionInformation.redirectRoute; - - if (redirectRoute) { - safeRedirect({ url: redirectRoute }); - } -}; - -export const BatchTransactionsSender = () => { - const dispatch = useDispatch(); - - const { address, nonce } = useSelector(accountSelector); - const signedTransactions = useSelector(signedTransactionsSelector); - - const sendingRef = useRef(false); - const sentSessionIds = useRef([]); - - const clearSignInfo = () => { - dispatch(clearAllTransactionsToSign()); - sendingRef.current = false; - }; - - const handleBatchErrors = ({ - errorMessage, - sessionId, - transactions - }: { - errorMessage: string; - sessionId: string; - transactions: SignedTransactionType[]; - }) => { - console.error('Unable to send transactions', errorMessage); - dispatch( - updateSignedTransactions({ - sessionId, - status: TransactionBatchStatusesEnum.fail, - errorMessage, - transactions: transactions.map((transaction) => ({ - ...transaction, - status: TransactionServerStatusesEnum.notExecuted - })) - }) - ); - clearSignInfo(); - }; - - const handleSendTransactions = useCallback(async () => { - const sessionIds = Object.keys(signedTransactions); - - for (const sessionId of sessionIds) { - const session = signedTransactions[sessionId]; - const skipSending = - session?.customTransactionInformation?.signWithoutSending; - - if (!session || !sessionId || skipSending) { - optionalRedirect(session); - continue; - } - - if (sentSessionIds.current.includes(sessionId)) { - continue; - } - - const { transactions } = session; - if (!transactions) { - continue; - } - - try { - const isSessionIdSigned = - session.status === TransactionBatchStatusesEnum.signed; - const shouldSendCurrentSession = - isSessionIdSigned && !sendingRef.current; - - if (!shouldSendCurrentSession) { - continue; - } - - sendingRef.current = true; - const indexes = [...Array(transactions.length).keys()]; - const defaultGrouping = [indexes]; - - const grouping = - session.customTransactionInformation?.sessionInformation?.grouping ?? - defaultGrouping; - const groupedTransactions = grouping?.map((item: number[]) => - item - .map((index) => transactions[index]) - .filter((transaction) => Boolean(transaction)) - ); - - if (!groupedTransactions) { - continue; - } - - sentSessionIds.current.push(sessionId); - const response = await sendBatchTransactions({ - transactions: groupedTransactions, - sessionId, - address - }); - - if (response?.error || !response?.data) { - handleBatchErrors({ - errorMessage: response?.error ?? 'Send batch error', - sessionId, - transactions - }); - continue; - } - - dispatch(setBatchTransactions(response.data)); - - const responseHashes = sequentialToFlatArray({ - transactions: response.data.transactions - }).map((tx) => tx.hash); - - const newStatus = TransactionServerStatusesEnum.pending; - const newTransactions = transactions.map((transaction) => { - if (responseHashes.includes(transaction.hash)) { - return { ...transaction, status: newStatus }; - } - - return transaction; - }); - - const submittedModalPayload = { - sessionId, - submittedMessage: 'submitted' - }; - - dispatch(setTxSubmittedModal(submittedModalPayload)); - dispatch( - updateSignedTransactions({ - sessionId, - status: TransactionBatchStatusesEnum.sent, - transactions: newTransactions - }) - ); - clearSignInfo(); - setNonce(nonce + transactions.length); - - optionalRedirect(session); - const [transaction] = transactions; - removeTransactionParamsFromUrl({ - transaction - }); - } catch (error) { - handleBatchErrors({ - errorMessage: (error as any).message, - sessionId, - transactions - }); - } finally { - sendingRef.current = false; - } - } - }, [signedTransactions, address, nonce]); - - useEffect(() => { - handleSendTransactions(); - }, [signedTransactions, address, handleSendTransactions]); - - return null; -}; diff --git a/src/components/BatchTransactionsSender/index.ts b/src/components/BatchTransactionsSender/index.ts deleted file mode 100644 index 04282c943..000000000 --- a/src/components/BatchTransactionsSender/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BatchTransactionsSender'; diff --git a/src/components/BatchTransactionsTracker/BatchTransactionsTracker.tsx b/src/components/BatchTransactionsTracker/BatchTransactionsTracker.tsx deleted file mode 100644 index 675df8595..000000000 --- a/src/components/BatchTransactionsTracker/BatchTransactionsTracker.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { - BatchTransactionsTrackerProps, - useBatchTransactionsTracker -} from 'hooks/transactions/batch/tracker/useBatchTransactionsTracker'; - -export function BatchTransactionsTracker(props: BatchTransactionsTrackerProps) { - useBatchTransactionsTracker(props); - return null; -} diff --git a/src/components/TransactionSender/TransactionSender.ts b/src/components/TransactionSender/TransactionSender.ts index 4f236757a..22ab92d53 100644 --- a/src/components/TransactionSender/TransactionSender.ts +++ b/src/components/TransactionSender/TransactionSender.ts @@ -1,12 +1,8 @@ -import { useEffect, useRef } from 'react'; -import { Transaction } from '@multiversx/sdk-core/out'; - -import { AxiosError } from 'axios'; +import { useCallback, useEffect, useRef } from 'react'; import { sendSignedTransactions as defaultSendSignedTxs, - SendSignedTransactionsReturnType + sendSignedBatchTransactions as defaultSendSignedBatchTxs } from 'apiCalls/transactions'; -import { newTransaction } from 'models/newTransaction'; import { useDispatch, useSelector } from 'reduxStore/DappProviderContext'; import { accountSelector, @@ -22,126 +18,118 @@ import { TransactionServerStatusesEnum } from 'types/enums.types'; import { SignedTransactionsBodyType } from 'types/transactions.types'; - import { setNonce } from 'utils/account/setNonce'; import { safeRedirect } from 'utils/redirect'; import { removeTransactionParamsFromUrl } from 'utils/transactions/removeTransactionParamsFromUrl'; - -export interface TransactionSenderType { - sendSignedTransactionsAsync?: ( - signedTransactions: Transaction[] - ) => Promise; -} +import { TransactionSenderType } from './types/transactionSender.types'; +import { invokeSendTransactions } from './utils/invokeSendTransactions'; /** * Function used to redirect after sending because of Safari cancelling async requests on page change */ const optionalRedirect = (sessionInformation: SignedTransactionsBodyType) => { const redirectRoute = sessionInformation.redirectRoute; - if (redirectRoute) { safeRedirect({ url: redirectRoute }); } }; export const TransactionSender = ({ - sendSignedTransactionsAsync = defaultSendSignedTxs + sendSignedTransactionsAsync = defaultSendSignedTxs, + sendSignedBatchTransactionsAsync = defaultSendSignedBatchTxs }: TransactionSenderType) => { - const account = useSelector(accountSelector); + const dispatch = useDispatch(); + + const { nonce } = useSelector(accountSelector); const signedTransactions = useSelector(signedTransactionsSelector); const sendingRef = useRef(false); - - const dispatch = useDispatch(); + const sentSessionIds = useRef([]); const clearSignInfo = () => { dispatch(clearAllTransactionsToSign()); sendingRef.current = false; }; - async function handleSendTransactions() { + + const handleSendTransactions = useCallback(async () => { const sessionIds = Object.keys(signedTransactions); + for (const sessionId of sessionIds) { - const sessionInformation = signedTransactions?.[sessionId]; + const session = signedTransactions[sessionId]; const skipSending = - sessionInformation?.customTransactionInformation?.signWithoutSending; + session?.customTransactionInformation?.signWithoutSending; - if (!sessionId || skipSending) { - optionalRedirect(sessionInformation); + if (!session || !sessionId || skipSending) { + optionalRedirect(session); continue; } - try { - const isSessionIdSigned = - signedTransactions[sessionId].status === - TransactionBatchStatusesEnum.signed; - const shouldSendCurrentSession = - isSessionIdSigned && !sendingRef.current; - if (!shouldSendCurrentSession) { - continue; - } - const { transactions } = signedTransactions[sessionId]; + if (sentSessionIds.current.includes(sessionId)) { + continue; + } - if (!transactions) { - continue; + const { transactions } = session; + if (!transactions) { + continue; + } + + const isSessionIdSigned = + session.status === TransactionBatchStatusesEnum.signed; + const shouldSendCurrentSession = isSessionIdSigned && !sendingRef.current; + + if (!shouldSendCurrentSession) { + continue; + } + + sendingRef.current = true; + sentSessionIds.current.push(sessionId); + + const responseHashes = await invokeSendTransactions({ + session, + sessionId, + clearSignInfo, + sendSignedTransactionsAsync, + sendSignedBatchTransactionsAsync + }); + + const newStatus = TransactionServerStatusesEnum.pending; + const newTransactions = transactions.map((transaction) => { + if (responseHashes?.includes(transaction.hash)) { + return { ...transaction, status: newStatus }; } - sendingRef.current = true; - const transactionsToSend = transactions.map((tx) => newTransaction(tx)); - const responseHashes = await sendSignedTransactionsAsync( - transactionsToSend - ); - - const newStatus = TransactionServerStatusesEnum.pending; - const newTransactions = transactions.map((transaction) => { - if (responseHashes.includes(transaction.hash)) { - return { ...transaction, status: newStatus }; - } - - return transaction; - }); - - const submittedModalPayload = { + + return transaction; + }); + + const submittedModalPayload = { + sessionId, + submittedMessage: 'submitted' + }; + + dispatch(setTxSubmittedModal(submittedModalPayload)); + dispatch( + updateSignedTransactions({ sessionId, - submittedMessage: 'submitted' - }; - - dispatch(setTxSubmittedModal(submittedModalPayload)); - dispatch( - updateSignedTransactions({ - sessionId, - status: TransactionBatchStatusesEnum.sent, - transactions: newTransactions - }) - ); - clearSignInfo(); - setNonce(account.nonce + transactions.length); - - optionalRedirect(sessionInformation); - const [transaction] = transactionsToSend; - removeTransactionParamsFromUrl({ - transaction - }); - } catch (error) { - const errorMessage = - (error as AxiosError).response?.data?.message ?? - (error as any).message; - - dispatch( - updateSignedTransactions({ - sessionId, - status: TransactionBatchStatusesEnum.fail, - errorMessage - }) - ); - clearSignInfo(); - } finally { - sendingRef.current = false; - } + status: TransactionBatchStatusesEnum.sent, + transactions: newTransactions + }) + ); + clearSignInfo(); + setNonce(nonce + transactions.length); + + optionalRedirect(session); + const [transaction] = transactions; + removeTransactionParamsFromUrl({ + transaction + }); + + sendingRef.current = false; } - } + }, [signedTransactions, nonce]); useEffect(() => { handleSendTransactions(); - }, [signedTransactions, account]); + }, [signedTransactions, handleSendTransactions]); return null; }; diff --git a/src/components/TransactionSender/types/transactionSender.types.ts b/src/components/TransactionSender/types/transactionSender.types.ts new file mode 100644 index 000000000..150d22d89 --- /dev/null +++ b/src/components/TransactionSender/types/transactionSender.types.ts @@ -0,0 +1,15 @@ +import { Transaction } from '@multiversx/sdk-core'; +import { + SendBatchTransactionsPropsType, + SendSignedBatchTransactionsReturnType, + SendSignedTransactionsReturnType +} from 'apiCalls/transactions'; + +export interface TransactionSenderType { + sendSignedTransactionsAsync?: ( + signedTransactions: Transaction[] + ) => Promise; + sendSignedBatchTransactionsAsync?: ( + props: SendBatchTransactionsPropsType + ) => Promise; +} diff --git a/src/components/TransactionSender/utils/handleSendBatchTransactionsErrors.ts b/src/components/TransactionSender/utils/handleSendBatchTransactionsErrors.ts new file mode 100644 index 000000000..16018bfdb --- /dev/null +++ b/src/components/TransactionSender/utils/handleSendBatchTransactionsErrors.ts @@ -0,0 +1,33 @@ +import { updateSignedTransactions } from 'reduxStore/slices'; +import { store } from 'reduxStore/store'; +import { + SignedTransactionType, + TransactionBatchStatusesEnum, + TransactionServerStatusesEnum +} from 'types'; + +export const handleSendBatchTransactionsErrors = ({ + errorMessage, + sessionId, + transactions, + clearSignInfo +}: { + errorMessage: string; + sessionId: string; + transactions: SignedTransactionType[]; + clearSignInfo?: () => void; +}) => { + console.error('Unable to send transactions', errorMessage); + store.dispatch( + updateSignedTransactions({ + sessionId, + status: TransactionBatchStatusesEnum.fail, + errorMessage, + transactions: transactions.map((transaction) => ({ + ...transaction, + status: TransactionServerStatusesEnum.notExecuted + })) + }) + ); + clearSignInfo?.(); +}; diff --git a/src/components/TransactionSender/utils/handleSendTransactionsErrors.ts b/src/components/TransactionSender/utils/handleSendTransactionsErrors.ts new file mode 100644 index 000000000..a4b0aed69 --- /dev/null +++ b/src/components/TransactionSender/utils/handleSendTransactionsErrors.ts @@ -0,0 +1,23 @@ +import { updateSignedTransactions } from 'reduxStore/slices'; +import { store } from 'reduxStore/store'; +import { TransactionBatchStatusesEnum } from 'types'; + +export const handleSendTransactionsErrors = ({ + errorMessage, + sessionId, + clearSignInfo +}: { + errorMessage: string; + sessionId: string; + clearSignInfo?: () => void; +}) => { + console.error('Unable to send transactions', errorMessage); + store.dispatch( + updateSignedTransactions({ + sessionId, + status: TransactionBatchStatusesEnum.fail, + errorMessage + }) + ); + clearSignInfo?.(); +}; diff --git a/src/components/TransactionSender/utils/invokeSendTransactions.ts b/src/components/TransactionSender/utils/invokeSendTransactions.ts new file mode 100644 index 000000000..66c536d9f --- /dev/null +++ b/src/components/TransactionSender/utils/invokeSendTransactions.ts @@ -0,0 +1,127 @@ +import { AxiosError } from 'axios'; +import { + SendBatchTransactionsPropsType, + SendSignedBatchTransactionsReturnType, + sendSignedTransactions +} from 'apiCalls/transactions'; +import { sendSignedBatchTransactions } from 'apiCalls/transactions/sendSignedBatchTransactions'; +import { newTransaction } from 'models/newTransaction'; +import { setBatchTransactions } from 'reduxStore/slices'; +import { store } from 'reduxStore/store'; +import { SignedTransactionsBodyType, SignedTransactionType } from 'types'; +import { sequentialToFlatArray } from 'utils/transactions/batch/sequentialToFlatArray'; +import { TransactionSenderType } from '../types/transactionSender.types'; +import { handleSendBatchTransactionsErrors } from './handleSendBatchTransactionsErrors'; +import { handleSendTransactionsErrors } from './handleSendTransactionsErrors'; + +const handleBatchSending = async ({ + session, + sessionId, + clearSignInfo, + sendSignedBatchTransactionsAsync +}: { + session: SignedTransactionsBodyType; + sessionId: string; + clearSignInfo?: () => void; + sendSignedBatchTransactionsAsync: ( + props: SendBatchTransactionsPropsType + ) => Promise; +}) => { + const { transactions } = session; + if (!transactions) { + return; + } + + const grouping = session.customTransactionInformation?.grouping; + if (!grouping) { + return; + } + + const groupedTransactions = transactions.reduce((acc, tx, index) => { + const groupIndex = grouping.findIndex((group) => group.includes(index)); + if (!acc[groupIndex]) { + acc[groupIndex] = []; + } + acc[groupIndex].push(tx); + return acc; + }, [] as SignedTransactionType[][]); + + if (groupedTransactions.length === 0) { + return; + } + + const response = await sendSignedBatchTransactionsAsync({ + transactions: groupedTransactions, + sessionId + }); + + const data = response?.data; + + if (response?.error || !data) { + handleSendBatchTransactionsErrors({ + errorMessage: response?.error ?? 'Send batch error', + sessionId, + transactions, + clearSignInfo + }); + return; + } + + if (data) { + store.dispatch(setBatchTransactions(data)); + } + + return sequentialToFlatArray({ + transactions: data?.transactions + }).map((tx) => tx.hash); +}; + +export const invokeSendTransactions = async ({ + session, + sessionId, + clearSignInfo, + sendSignedBatchTransactionsAsync = sendSignedBatchTransactions, + sendSignedTransactionsAsync = sendSignedTransactions +}: { + session: SignedTransactionsBodyType; + sessionId: string; + clearSignInfo?: () => void; +} & TransactionSenderType) => { + const { transactions } = session; + if (!transactions) { + return; + } + + const grouping = session.customTransactionInformation?.grouping; + + if (grouping) { + try { + return await handleBatchSending({ + session, + sessionId, + clearSignInfo, + sendSignedBatchTransactionsAsync + }); + } catch (error) { + handleSendBatchTransactionsErrors({ + errorMessage: (error as any).message, + sessionId, + transactions + }); + return null; + } + } + + try { + const transactionsToSend = transactions.map((tx) => newTransaction(tx)); + return await sendSignedTransactionsAsync(transactionsToSend); + } catch (error) { + handleSendTransactionsErrors({ + errorMessage: + (error as AxiosError).response?.data?.message ?? (error as any).message, + sessionId, + clearSignInfo + }); + return null; + } +}; diff --git a/src/components/TransactionsTracker/TransactionTracker.tsx b/src/components/TransactionsTracker/TransactionTracker.tsx index 6441007b6..faaeb4d93 100644 --- a/src/components/TransactionsTracker/TransactionTracker.tsx +++ b/src/components/TransactionsTracker/TransactionTracker.tsx @@ -1,11 +1,9 @@ -import { - useTransactionsTracker, - TransactionsTrackerType -} from './useTransactionsTracker'; +import { useBatchTransactionsTracker } from 'hooks/transactions/batch/tracker/useBatchTransactionsTracker'; +import { TransactionsTrackerType } from 'types/transactionsTracker.types'; +import { useTransactionsTracker } from './useTransactionsTracker'; -export function TransactionsTracker({ - getTransactionsByHash -}: TransactionsTrackerType) { - useTransactionsTracker({ getTransactionsByHash }); +export function TransactionsTracker(props: TransactionsTrackerType) { + useTransactionsTracker(props); + useBatchTransactionsTracker(props); return null; } diff --git a/src/components/TransactionsTracker/useTransactionsTracker.ts b/src/components/TransactionsTracker/useTransactionsTracker.ts index 53a1094a3..e1be91b71 100644 --- a/src/components/TransactionsTracker/useTransactionsTracker.ts +++ b/src/components/TransactionsTracker/useTransactionsTracker.ts @@ -1,11 +1,7 @@ import { useEffect } from 'react'; import { getTransactionsByHashes as defaultGetTxByHash } from 'apiCalls/transactions'; import { useCheckTransactionStatus, useRegisterWebsocketListener } from 'hooks'; -import { GetTransactionsByHashesType } from 'types/transactions.types'; - -export interface TransactionsTrackerType { - getTransactionsByHash?: GetTransactionsByHashesType; -} +import { TransactionsTrackerType } from 'types/transactionsTracker.types'; export function useTransactionsTracker(props?: TransactionsTrackerType) { const checkTransactionStatus = useCheckTransactionStatus(); @@ -16,7 +12,8 @@ export function useTransactionsTracker(props?: TransactionsTrackerType) { const onMessage = () => { checkTransactionStatus({ shouldRefreshBalance: true, - getTransactionsByHash + getTransactionsByHash, + ...props }); }; diff --git a/src/hooks/transactions/batch/tracker/useBatchTransactionsTracker.ts b/src/hooks/transactions/batch/tracker/useBatchTransactionsTracker.ts index b5af35e7b..e84492416 100644 --- a/src/hooks/transactions/batch/tracker/useBatchTransactionsTracker.ts +++ b/src/hooks/transactions/batch/tracker/useBatchTransactionsTracker.ts @@ -13,8 +13,8 @@ import { useCheckHangingBatchesFallback } from './useCheckHangingBatchesFallback import { useVerifyBatchStatus } from './useVerifyBatchStatus'; export type BatchTransactionsTrackerProps = { - onSuccess?: (batchId: string | null) => void; - onFail?: (batchId: string | null, errorMessage?: string) => void; + onSuccess?: (sessionId: string | null) => void; + onFail?: (sessionId: string | null, errorMessage?: string) => void; }; export const useBatchTransactionsTracker = ({ diff --git a/src/hooks/transactions/batch/tracker/useCheckBatchesOnWsFailureFallback.ts b/src/hooks/transactions/batch/tracker/useCheckBatchesOnWsFailureFallback.ts index 028471f6a..1b92e1a36 100644 --- a/src/hooks/transactions/batch/tracker/useCheckBatchesOnWsFailureFallback.ts +++ b/src/hooks/transactions/batch/tracker/useCheckBatchesOnWsFailureFallback.ts @@ -13,8 +13,8 @@ import { useVerifyBatchStatus } from './useVerifyBatchStatus'; * Resolves the toast by checking the status of each transaction in batch after a certain time (90seconds) * */ export const useCheckBatchesOnWsFailureFallback = (props?: { - onSuccess?: (batchId: string | null) => void; - onFail?: (batchId: string | null, errorMessage?: string) => void; + onSuccess?: (sessionId: string | null) => void; + onFail?: (sessionId: string | null, errorMessage?: string) => void; }) => { const { batchTransactionsArray } = useGetBatches(); const { verifyBatchStatus } = useVerifyBatchStatus(props); diff --git a/src/hooks/transactions/batch/tracker/useCheckHangingBatchesFallback.ts b/src/hooks/transactions/batch/tracker/useCheckHangingBatchesFallback.ts index 7325870ae..a2515f3df 100644 --- a/src/hooks/transactions/batch/tracker/useCheckHangingBatchesFallback.ts +++ b/src/hooks/transactions/batch/tracker/useCheckHangingBatchesFallback.ts @@ -16,8 +16,8 @@ import { useUpdateBatch } from './useUpdateBatch'; * Resolves the toast and set the status to failed for each transaction after a certain time (10minutes) * */ export const useCheckHangingBatchesFallback = (props?: { - onSuccess?: (batchId: string | null) => void; - onFail?: (batchId: string | null, errorMessage?: string) => void; + onSuccess?: (sessionId: string | null) => void; + onFail?: (sessionId: string | null, errorMessage?: string) => void; }) => { const { batchTransactionsArray } = useGetBatches(); const updateBatch = useUpdateBatch(); @@ -53,12 +53,12 @@ export const useCheckHangingBatchesFallback = (props?: { removeBatchTransactions(batchId); if (isSuccessful) { - onSuccess?.(batchId); + onSuccess?.(sessionId.toString()); } if (isFailed) { onFail?.( - batchId, + sessionId.toString(), 'Error processing batch transactions. Status: failed' ); } diff --git a/src/hooks/transactions/batch/tracker/useVerifyBatchStatus.ts b/src/hooks/transactions/batch/tracker/useVerifyBatchStatus.ts index 1a141493f..f5aca5524 100644 --- a/src/hooks/transactions/batch/tracker/useVerifyBatchStatus.ts +++ b/src/hooks/transactions/batch/tracker/useVerifyBatchStatus.ts @@ -8,8 +8,8 @@ import { useCheckBatch } from './useCheckBatch'; import { useUpdateBatch } from './useUpdateBatch'; export const useVerifyBatchStatus = (props?: { - onSuccess?: (batchId: string | null) => void; - onFail?: (batchId: string | null, errorMessage?: string) => void; + onSuccess?: (sessionId: string | null) => void; + onFail?: (sessionId: string | null, errorMessage?: string) => void; }) => { const dispatch = useDispatch(); const { signedTransactions } = useGetSignedTransactions(); @@ -30,6 +30,12 @@ export const useVerifyBatchStatus = (props?: { return; } + // If the grouping is missing then means the transactions were sent with the normal flow + const grouping = session.customTransactionInformation?.grouping; + if (!grouping) { + return; + } + const sessionTransactions = signedTransactions[sessionId]?.transactions ?? []; @@ -40,11 +46,11 @@ export const useVerifyBatchStatus = (props?: { if (completed) { if (isSuccessful) { - onSuccess?.(batchId); + onSuccess?.(sessionId); } if (isFailed) { - onFail?.(batchId, 'Error processing batch transactions.'); + onFail?.(sessionId, 'Error processing batch transactions.'); } } else { const data = await checkBatch({ batchId }); diff --git a/src/hooks/transactions/batch/useSendBatchTransactions.ts b/src/hooks/transactions/batch/useSendBatchTransactions.ts index df38c885f..2dc02ab5c 100644 --- a/src/hooks/transactions/batch/useSendBatchTransactions.ts +++ b/src/hooks/transactions/batch/useSendBatchTransactions.ts @@ -1,4 +1,8 @@ import { useCallback, useState } from 'react'; +import { + sendSignedBatchTransactions, + SendBatchTransactionsPropsType +} from 'apiCalls/transactions/sendSignedBatchTransactions'; import { useDispatch } from 'reduxStore/DappProviderContext'; import { setBatchTransactions, @@ -6,15 +10,13 @@ import { updateSignedTransactions } from 'reduxStore/slices'; import { removeBatchTransactions } from 'services/transactions'; -import { - sendBatchTransactions, - SendBatchTransactionsPropsType -} from 'services/transactions/sendBatchTransactions'; +import { updateSignedTransactionCustomTransactionInformationState } from 'services/transactions/updateSignedTransactions'; import { TransactionBatchStatusesEnum, TransactionServerStatusesEnum } from 'types/enums.types'; import { BatchTransactionStatus } from 'types/serverTransactions.types'; +import { generateBatchTransactionsGrouping } from 'utils/transactions/batch/generateBatchTransactionsGrouping'; import { sequentialToFlatArray } from 'utils/transactions/batch/sequentialToFlatArray'; export const useSendBatchTransactions = () => { @@ -23,7 +25,7 @@ export const useSendBatchTransactions = () => { const send = useCallback( async (params: SendBatchTransactionsPropsType) => { - const response = await sendBatchTransactions(params); + const response = await sendSignedBatchTransactions(params); const error = response?.error; const data = response?.data; @@ -44,6 +46,18 @@ export const useSendBatchTransactions = () => { status: TransactionServerStatusesEnum.pending })); + // Ensure the transaction custom information is updated with the desired values from the dApp when the transactions are send on demand + const grouping = generateBatchTransactionsGrouping(params.transactions); + + updateSignedTransactionCustomTransactionInformationState({ + sessionId: params.sessionId, + customTransactionInformationOverrides: { + ...(params.customTransactionInformationOverrides ?? {}), + // Mandatory override. Otherwise, the transactions will not be grouped and the transaction tracker will not work properly (doesn't know to differentiate between transactions sent in batch and the transactions sent using normal flow) + grouping + } + }); + dispatch( updateSignedTransactions({ sessionId: params.sessionId, diff --git a/src/hooks/transactions/useCheckTransactionStatus/checkBatch.ts b/src/hooks/transactions/useCheckTransactionStatus/checkBatch.ts index 3816998c9..6dff1cc8b 100644 --- a/src/hooks/transactions/useCheckTransactionStatus/checkBatch.ts +++ b/src/hooks/transactions/useCheckTransactionStatus/checkBatch.ts @@ -2,12 +2,12 @@ import { getTransactionsByHashes as defaultGetTxByHash } from 'apiCalls/transact import { updateSignedTransactionStatus } from 'reduxStore/slices'; import { store } from 'reduxStore/store'; import { - GetTransactionsByHashesReturnType, - GetTransactionsByHashesType, CustomTransactionInformation, + GetTransactionsByHashesReturnType, SignedTransactionsBodyType } from 'types'; import { TransactionServerStatusesEnum } from 'types/enums.types'; +import { TransactionsTrackerType } from 'types/transactionsTracker.types'; import { refreshAccount } from 'utils/account'; import { getIsTransactionFailed, @@ -18,10 +18,10 @@ import { getPendingTransactions } from './getPendingTransactions'; import { manageFailedTransactions } from './manageFailedTransactions'; import { manageTimedOutTransactions } from './manageTimedOutTransactions'; -export interface TransactionStatusTrackerPropsType { +export interface TransactionStatusTrackerPropsType + extends TransactionsTrackerType { sessionId: string; transactionBatch: SignedTransactionsBodyType; - getTransactionsByHash?: GetTransactionsByHashesType; shouldRefreshBalance?: boolean; isSequential?: boolean; } @@ -142,7 +142,9 @@ export async function checkBatch({ transactionBatch: { transactions, customTransactionInformation }, getTransactionsByHash = defaultGetTxByHash, shouldRefreshBalance, - isSequential + isSequential, + onSuccess, + onFail }: TransactionStatusTrackerPropsType) { try { if (transactions == null) { @@ -162,6 +164,30 @@ export async function checkBatch({ isSequential }); } + + const hasCompleted = serverTransactions.every( + (tx) => tx.status !== TransactionServerStatusesEnum.pending + ); + + // Call the onSuccess or onFail callback only if the transactions are sent normally (not using batch transactions mechanism). + // The batch transactions mechanism will call the callbacks separately. + if (hasCompleted && !customTransactionInformation?.grouping) { + const isSuccessful = serverTransactions.every( + (tx) => tx.status === TransactionServerStatusesEnum.success + ); + + if (isSuccessful) { + return onSuccess?.(sessionId); + } + + const isFailed = serverTransactions.some( + (tx) => tx.status === TransactionServerStatusesEnum.fail + ); + + if (isFailed) { + return onFail?.(sessionId); + } + } } catch (error) { console.error(error); } diff --git a/src/hooks/transactions/useCheckTransactionStatus/useCheckTransactionStatus.ts b/src/hooks/transactions/useCheckTransactionStatus/useCheckTransactionStatus.ts index b5daedac6..b655c9ff7 100644 --- a/src/hooks/transactions/useCheckTransactionStatus/useCheckTransactionStatus.ts +++ b/src/hooks/transactions/useCheckTransactionStatus/useCheckTransactionStatus.ts @@ -1,5 +1,5 @@ import { useGetPendingTransactions } from 'hooks/transactions/useGetPendingTransactions'; -import { GetTransactionsByHashesType } from 'types/transactions.types'; +import { TransactionsTrackerType } from 'types/transactionsTracker.types'; import { refreshAccount } from 'utils/account/refreshAccount'; import { getIsTransactionPending } from 'utils/transactions/transactionStateByStatus'; import { checkBatch } from './checkBatch'; @@ -7,17 +7,21 @@ import { checkBatch } from './checkBatch'; export function useCheckTransactionStatus() { const { pendingTransactionsArray } = useGetPendingTransactions(); - async function checkTransactionStatus(props: { - getTransactionsByHash?: GetTransactionsByHashesType; - shouldRefreshBalance?: boolean; - }) { - const pendingBatches = pendingTransactionsArray.filter( - ([sessionId, transactionBatch]) => { + async function checkTransactionStatus( + props: TransactionsTrackerType & { + shouldRefreshBalance?: boolean; + } + ) { + const pendingBatches = pendingTransactionsArray + .filter( + ([_, session]) => !session?.customTransactionInformation?.grouping + ) + .filter(([sessionId, session]) => { const isPending = - sessionId != null && getIsTransactionPending(transactionBatch.status); + sessionId != null && getIsTransactionPending(session.status); return isPending; - } - ); + }); + if (pendingBatches.length > 0) { for (const [sessionId, transactionBatch] of pendingBatches) { await checkBatch({ diff --git a/src/reduxStore/slices/transactionsSlice.ts b/src/reduxStore/slices/transactionsSlice.ts index 4a232cba3..bbfd4eae9 100644 --- a/src/reduxStore/slices/transactionsSlice.ts +++ b/src/reduxStore/slices/transactionsSlice.ts @@ -23,6 +23,7 @@ export interface UpdateSignedTransactionsPayloadType { status: TransactionBatchStatusesEnum; errorMessage?: string; transactions?: SignedTransactionType[]; + customTransactionInformationOverrides?: Partial; } export interface MoveTransactionsToSignedStatePayloadType @@ -221,6 +222,23 @@ export const transactionsSlice = createSlice({ action: PayloadAction ) => { state.signTransactionsCancelMessage = action.payload; + }, + updateCustomTransactionInformation: ( + state: TransactionsSliceStateType, + action: PayloadAction<{ + sessionId: string; + customTransactionInformationOverrides: Partial; + }> + ) => { + const { sessionId, customTransactionInformationOverrides } = + action.payload; + const session = state.signedTransactions[sessionId]; + if (session != null) { + state.signedTransactions[sessionId].customTransactionInformation = { + ...state.customTransactionInformationForSessionId[sessionId], + ...customTransactionInformationOverrides + }; + } } }, extraReducers: (builder) => { @@ -270,7 +288,8 @@ export const { clearTransactionToSign, setSignTransactionsError, setSignTransactionsCancelMessage, - moveTransactionsToSignedState + moveTransactionsToSignedState, + updateCustomTransactionInformation } = transactionsSlice.actions; export default transactionsSlice.reducer; diff --git a/src/services/transactions/sendBatchTransactions.spec.ts b/src/services/transactions/sendBatchTransactions.spec.ts new file mode 100644 index 000000000..4cd699482 --- /dev/null +++ b/src/services/transactions/sendBatchTransactions.spec.ts @@ -0,0 +1,215 @@ +import { sendBatchTransactions } from './sendBatchTransactions'; +import { addressSelector } from 'reduxStore/selectors'; +import { store } from 'reduxStore/store'; +import { getWindowLocation } from 'utils/window/getWindowLocation'; +import { signTransactions } from './signTransactions'; +import { transformTransactionsToSign } from './utils/transformTransactionsToSign'; + +jest.mock('reduxStore/selectors', () => ({ + addressSelector: jest.fn() +})); + +jest.mock('reduxStore/store', () => ({ + store: { + getState: jest.fn() + } +})); + +jest.mock('utils/window/getWindowLocation', () => ({ + getWindowLocation: jest.fn() +})); + +jest.mock('./signTransactions', () => ({ + signTransactions: jest.fn() +})); + +jest.mock('./utils/transformTransactionsToSign', () => ({ + transformTransactionsToSign: jest.fn() +})); + +describe('sendBatchTransactions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call all the dependencies and return the expected result', async () => { + const address = + 'erd1wh9c0sjr2xn8hzf02lwwcr4jk2s84tat9ud2kaq6zr7xzpvl9l5q8awmex'; + const sessionId = '12345'; + const transactions = [ + [ + { + receiver: address, + sender: address, + value: '0', + data: '1' + }, + { + receiver: address, + sender: address, + value: '0', + data: '2' + } + ], + [ + { + receiver: address, + sender: address, + value: '0', + data: '3' + } + ], + [ + { + receiver: address, + sender: address, + value: '0', + data: '4' + }, + { + receiver: address, + sender: address, + value: '0', + data: '5' + }, + { + receiver: address, + sender: address, + value: '0', + data: '6' + } + ] + ]; + const transactionsDisplayInfo = {}; + const callbackRoute = '/callback'; + const minGasLimit = 21000; + + // Mock the dependencies + (addressSelector as unknown as jest.Mock).mockReturnValue(address); + // eslint-disable-next-line @typescript-eslint/no-empty-function + (store.getState as unknown as jest.Mock).mockReturnValue(() => {}); + (getWindowLocation as unknown as jest.Mock).mockReturnValue({ + pathname: callbackRoute + }); + (signTransactions as unknown as jest.Mock).mockResolvedValue({ sessionId }); + (transformTransactionsToSign as unknown as jest.Mock).mockResolvedValue([]); + + const result = await sendBatchTransactions({ + transactions, + transactionsDisplayInfo, + minGasLimit + }); + + expect(addressSelector).toHaveBeenCalled(); + expect(store.getState).toHaveBeenCalled(); + expect(getWindowLocation).toHaveBeenCalled(); + expect(transformTransactionsToSign).toHaveBeenCalledWith({ + transactions: expect.any(Array), + minGasLimit + }); + expect(signTransactions).toHaveBeenCalledWith({ + transactions: expect.any(Array), + minGasLimit, + callbackRoute, + transactionsDisplayInfo, + customTransactionInformation: { + grouping: expect.any(Array), + redirectAfterSign: true, + completedTransactionsDelay: undefined, + sessionInformation: undefined, + skipGuardian: undefined, + signWithoutSending: false + } + }); + + expect(result).toEqual({ + error: undefined, + batchId: `${sessionId}-${address}` + }); + }); + + it('should prepare the grouping field with the indexes from the flat transactions array', async () => { + const address = + 'erd1wh9c0sjr2xn8hzf02lwwcr4jk2s84tat9ud2kaq6zr7xzpvl9l5q8awmex'; + const sessionId = '12345'; + const transactions = [ + [ + { + receiver: address, + sender: address, + value: '0', + data: '1' + }, + { + receiver: address, + sender: address, + value: '0', + data: '2' + } + ], + [ + { + receiver: address, + sender: address, + value: '0', + data: '3' + } + ], + [ + { + receiver: address, + sender: address, + value: '0', + data: '4' + }, + { + receiver: address, + sender: address, + value: '0', + data: '5' + }, + { + receiver: address, + sender: address, + value: '0', + data: '6' + } + ] + ]; + const transactionsDisplayInfo = {}; // Your test display info + const callbackRoute = '/callback'; + const minGasLimit = 21000; + + // Mock the dependencies + (addressSelector as unknown as jest.Mock).mockReturnValue(address); + (store.getState as unknown as jest.Mock).mockReturnValue({}); + (getWindowLocation as unknown as jest.Mock).mockReturnValue({ + pathname: callbackRoute + }); + (signTransactions as unknown as jest.Mock).mockResolvedValue({ sessionId }); + (transformTransactionsToSign as unknown as jest.Mock).mockResolvedValue( + transactions + ); + + await sendBatchTransactions({ + transactions, + transactionsDisplayInfo, + minGasLimit + }); + + expect(signTransactions).toHaveBeenCalledWith({ + transactions, + minGasLimit, + callbackRoute, + transactionsDisplayInfo, + customTransactionInformation: { + grouping: [[0, 1], [2], [3, 4, 5]], + redirectAfterSign: true, + completedTransactionsDelay: undefined, + sessionInformation: undefined, + skipGuardian: undefined, + signWithoutSending: false + } + }); + }); +}); diff --git a/src/services/transactions/sendBatchTransactions.ts b/src/services/transactions/sendBatchTransactions.ts index 300890173..284b07e9a 100644 --- a/src/services/transactions/sendBatchTransactions.ts +++ b/src/services/transactions/sendBatchTransactions.ts @@ -1,53 +1,59 @@ -import axios from 'axios'; -import { TRANSACTIONS_BATCH } from 'apiCalls'; -import { TIMEOUT } from 'constants/network'; -import { buildBatchId } from 'hooks/transactions/helpers/buildBatchId'; -import { networkSelector } from 'reduxStore/selectors'; +import { Transaction } from '@multiversx/sdk-core/out'; +import { addressSelector } from 'reduxStore/selectors'; import { store } from 'reduxStore/store'; import { - BatchTransactionsResponseType, SendBatchTransactionReturnType, - SignedTransactionType + SendBatchTransactionsPropsType, + SimpleTransactionType } from 'types'; - -export interface SendBatchTransactionsPropsType { - transactions: SignedTransactionType[][]; - address: string; - sessionId: string; -} +import { generateBatchTransactionsGrouping } from 'utils/transactions/batch/generateBatchTransactionsGrouping'; +import { getWindowLocation } from 'utils/window/getWindowLocation'; +import { signTransactions } from './signTransactions'; +import { transformTransactionsToSign } from './utils/transformTransactionsToSign'; export async function sendBatchTransactions({ transactions, - sessionId, - address + transactionsDisplayInfo, + redirectAfterSign = true, + callbackRoute = getWindowLocation().pathname, + signWithoutSending = false, + completedTransactionsDelay, + sessionInformation, + skipGuardian, + minGasLimit }: SendBatchTransactionsPropsType): Promise { - const { apiAddress, apiTimeout } = networkSelector(store.getState()); - try { - const batchId = buildBatchId({ - sessionId, - address + const address = addressSelector(store.getState()); + const transactionsPayload = transactions.flat(); + + const transactionsToSign = await transformTransactionsToSign({ + transactions: transactionsPayload as SimpleTransactionType[], + minGasLimit }); - const payload = { - transactions: transactions, - id: batchId - }; + const grouping = generateBatchTransactionsGrouping(transactions); - const response = await axios.post( - `${apiAddress}/${TRANSACTIONS_BATCH}`, - payload, - { - timeout: Number(apiTimeout ?? TIMEOUT) + const { sessionId, error } = await signTransactions({ + transactions: transactionsToSign as Transaction[], + minGasLimit, + callbackRoute, + transactionsDisplayInfo, + customTransactionInformation: { + grouping, + redirectAfterSign, + completedTransactionsDelay, + sessionInformation, + skipGuardian, + signWithoutSending } - ); + }); - return { batchId, data: response.data }; - } catch (err) { - console.error('error sending batch transactions', err); return { - error: (err as any)?.message ?? 'error sending batch transactions', - batchId: null + error, + batchId: `${sessionId}-${address}` }; + } catch (err) { + console.error('error signing transaction', err as any); + return { error: err as any, batchId: null }; } } diff --git a/src/services/transactions/sendTransactions.ts b/src/services/transactions/sendTransactions.ts index 6fc8d6dd3..c28ba7624 100644 --- a/src/services/transactions/sendTransactions.ts +++ b/src/services/transactions/sendTransactions.ts @@ -6,14 +6,14 @@ import { } from 'types'; import { getWindowLocation } from 'utils/window/getWindowLocation'; import { signTransactions } from './signTransactions'; -import { transformAndSignTransactions } from './transformAndSignTransactions'; +import { transformTransactionsToSign } from './utils/transformTransactionsToSign'; export async function sendTransactions({ transactions, transactionsDisplayInfo, redirectAfterSign = true, callbackRoute = getWindowLocation().pathname, - signWithoutSending, + signWithoutSending = false, completedTransactionsDelay, sessionInformation, skipGuardian, @@ -24,19 +24,13 @@ export async function sendTransactions({ ? transactions : [transactions]; - const areComplexTransactions = transactionsPayload.every( - (tx) => Object.getPrototypeOf(tx).toPlainObject != null - ); - let txToSign = transactionsPayload; - if (!areComplexTransactions) { - txToSign = await transformAndSignTransactions({ - transactions: transactionsPayload as SimpleTransactionType[], - minGasLimit - }); - } + const transactionsToSign = await transformTransactionsToSign({ + transactions: transactionsPayload as SimpleTransactionType[], + minGasLimit + }); return signTransactions({ - transactions: txToSign as Transaction[], + transactions: transactionsToSign as Transaction[], minGasLimit, callbackRoute, transactionsDisplayInfo, diff --git a/src/services/transactions/signTransactions.ts b/src/services/transactions/signTransactions.ts index 419559645..582633d4b 100644 --- a/src/services/transactions/signTransactions.ts +++ b/src/services/transactions/signTransactions.ts @@ -68,7 +68,11 @@ export async function signTransactions({ const signTransactionsPayload = { sessionId, callbackRoute, - customTransactionInformation, + customTransactionInformation: { + ...(customTransactionInformation ?? {}), + signWithoutSending: + customTransactionInformation?.signWithoutSending ?? true + }, transactions: transactionsPayload.map((tx) => { return { ...tx.toPlainObject(), diff --git a/src/services/transactions/updateSignedTransactions.ts b/src/services/transactions/updateSignedTransactions.ts index 721ab8789..0d9a6a588 100644 --- a/src/services/transactions/updateSignedTransactions.ts +++ b/src/services/transactions/updateSignedTransactions.ts @@ -6,9 +6,11 @@ import { UpdateSignedTransactionStatusPayloadType, MoveTransactionsToSignedStatePayloadType, setTransactionsDisplayInfo, - SetTransactionsInfoPayloadType + SetTransactionsInfoPayloadType, + updateCustomTransactionInformation } from 'reduxStore/slices'; import { store } from 'reduxStore/store'; +import { CustomTransactionInformation } from 'types'; export function setTransactionsToSignedState( payload: MoveTransactionsToSignedStatePayloadType @@ -33,3 +35,10 @@ export function setTransactionsDisplayInfoState( ) { store.dispatch(setTransactionsDisplayInfo(payload)); } + +export function updateSignedTransactionCustomTransactionInformationState(payload: { + sessionId: string; + customTransactionInformationOverrides: Partial; +}) { + store.dispatch(updateCustomTransactionInformation(payload)); +} diff --git a/src/services/transactions/utils/transformTransactionsToSign.ts b/src/services/transactions/utils/transformTransactionsToSign.ts new file mode 100644 index 000000000..296d66139 --- /dev/null +++ b/src/services/transactions/utils/transformTransactionsToSign.ts @@ -0,0 +1,26 @@ +import { Transaction } from '@multiversx/sdk-core'; +import { SimpleTransactionType } from 'types'; +import { transformAndSignTransactions } from '../transformAndSignTransactions'; + +export const transformTransactionsToSign = async ({ + transactions, + minGasLimit +}: { + transactions: (SimpleTransactionType | Transaction)[]; + minGasLimit?: number; +}) => { + const areComplexTransactions = transactions.every( + (tx) => Object.getPrototypeOf(tx).toPlainObject != null + ); + + let transactionsToSign = transactions; + + if (!areComplexTransactions) { + transactionsToSign = await transformAndSignTransactions({ + transactions: transactions as SimpleTransactionType[], + minGasLimit + }); + } + + return transactionsToSign; +}; diff --git a/src/types/serverTransactions.types.ts b/src/types/serverTransactions.types.ts index 7b5a79b25..d52be033c 100644 --- a/src/types/serverTransactions.types.ts +++ b/src/types/serverTransactions.types.ts @@ -320,10 +320,15 @@ export enum BatchTransactionStatus { fail = 'fail' } +export interface BatchTransactionsRequestType { + id: string; + transactions: SignedTransactionType[][]; +} + export interface BatchTransactionsResponseType { id: string; status: BatchTransactionStatus; - transactions: SignedTransactionType[] | SignedTransactionType[][]; + transactions: SignedTransactionType[][]; error?: string; message?: string; statusCode?: string; diff --git a/src/types/transactions.types.ts b/src/types/transactions.types.ts index be54754ea..b80d9b401 100644 --- a/src/types/transactions.types.ts +++ b/src/types/transactions.types.ts @@ -9,7 +9,6 @@ import { TransactionServerStatusesEnum, TransactionTypesEnum } from './enums.types'; -import { BatchTransactionsResponseType } from './serverTransactions.types'; export interface TransactionsToSignType { transactions: IPlainTransactionObject[]; @@ -142,6 +141,18 @@ export interface SendTransactionsPropsType { sessionInformation?: any; } +export interface SendBatchTransactionsPropsType { + transactions: (Transaction | SimpleTransactionType)[][]; + redirectAfterSign?: boolean; + signWithoutSending?: boolean; + skipGuardian?: boolean; + completedTransactionsDelay?: number; + callbackRoute?: string; + transactionsDisplayInfo: TransactionsDisplayInfoType; + minGasLimit?: number; + sessionInformation?: any; +} + export interface SignTransactionsPropsType { transactions: Transaction[] | Transaction; minGasLimit?: number; @@ -212,6 +223,10 @@ export interface CustomTransactionInformation { * If true, the change guardian action will not trigger transaction version update */ skipGuardian?: boolean; + /** + * Keeps indexes of transactions that should be grouped together. If not provided, all transactions will be grouped together. Used only for batch transactions. + */ + grouping?: number[][]; } export interface SendTransactionReturnType { @@ -222,7 +237,6 @@ export interface SendTransactionReturnType { export interface SendBatchTransactionReturnType { error?: string; batchId: string | null; - data?: BatchTransactionsResponseType | null; } export type GetTransactionsByHashesType = ( diff --git a/src/types/transactionsTracker.types.ts b/src/types/transactionsTracker.types.ts new file mode 100644 index 000000000..3583202ec --- /dev/null +++ b/src/types/transactionsTracker.types.ts @@ -0,0 +1,7 @@ +import { GetTransactionsByHashesType } from 'types/index'; + +export interface TransactionsTrackerType { + getTransactionsByHash?: GetTransactionsByHashesType; + onSuccess?: (sessionId: string | null) => void; + onFail?: (sessionId: string | null, errorMessage?: string) => void; +} diff --git a/src/utils/transactions/batch/generateBatchTransactionsGrouping.spec.ts b/src/utils/transactions/batch/generateBatchTransactionsGrouping.spec.ts new file mode 100644 index 000000000..118f3d8cc --- /dev/null +++ b/src/utils/transactions/batch/generateBatchTransactionsGrouping.spec.ts @@ -0,0 +1,74 @@ +import { generateBatchTransactionsGrouping } from './generateBatchTransactionsGrouping'; // Replace 'your-module' with the actual module path + +describe('generateBatchTransactionsGrouping', () => { + it('should generate batch transactions grouping correctly', () => { + const address = + 'erd1wh9c0sjr2xn8hzf02lwwcr4jk2s84tat9ud2kaq6zr7xzpvl9l5q8awmex'; + const transactions = [ + [ + { + receiver: address, + sender: address, + value: '0', + data: '1' + }, + { + receiver: address, + sender: address, + value: '0', + data: '2' + } + ], + [ + { + receiver: address, + sender: address, + value: '0', + data: '3' + } + ], + [ + { + receiver: address, + sender: address, + value: '0', + data: '4' + }, + { + receiver: address, + sender: address, + value: '0', + data: '5' + }, + { + receiver: address, + sender: address, + value: '0', + data: '6' + } + ] + ]; + + const expectedGrouping = [[0, 1], [2], [3, 4, 5]]; + + const result = generateBatchTransactionsGrouping(transactions); + + expect(result).toEqual(expectedGrouping); + }); + + it('should handle empty input', () => { + const transactions: any[][] = []; + + const result = generateBatchTransactionsGrouping(transactions); + + expect(result).toEqual([]); + }); + + it('should handle nested empty arrays', () => { + const transactions = [[], [], []]; + + const result = generateBatchTransactionsGrouping(transactions); + + expect(result).toEqual([[], [], []]); + }); +}); diff --git a/src/utils/transactions/batch/generateBatchTransactionsGrouping.ts b/src/utils/transactions/batch/generateBatchTransactionsGrouping.ts new file mode 100644 index 000000000..9d8acda68 --- /dev/null +++ b/src/utils/transactions/batch/generateBatchTransactionsGrouping.ts @@ -0,0 +1,11 @@ +import { Transaction } from '@multiversx/sdk-core/out'; +import { SimpleTransactionType } from 'types'; + +export const generateBatchTransactionsGrouping = ( + transactions: (Transaction | SimpleTransactionType)[][] +) => { + let indexInFlatArray = 0; + return transactions.map((group) => { + return group.map((_tx) => indexInFlatArray++); + }); +}; diff --git a/src/wrappers/DappProvider/CustomComponents.tsx b/src/wrappers/DappProvider/CustomComponents.tsx index 4811ca98b..826ccb4dd 100644 --- a/src/wrappers/DappProvider/CustomComponents.tsx +++ b/src/wrappers/DappProvider/CustomComponents.tsx @@ -1,16 +1,9 @@ import React from 'react'; -import { BatchTransactionsSender } from 'components/BatchTransactionsSender'; -import { BatchTransactionsTracker } from 'components/BatchTransactionsTracker/BatchTransactionsTracker'; import { LogoutListener } from 'components/LogoutListener'; -import { - TransactionSender, - TransactionSenderType -} from 'components/TransactionSender'; -import { - TransactionsTracker, - TransactionsTrackerType -} from 'components/TransactionsTracker'; -import { BatchTransactionsTrackerProps } from 'hooks/transactions/batch/tracker/useBatchTransactionsTracker'; +import { TransactionSender } from 'components/TransactionSender'; +import { TransactionSenderType } from 'components/TransactionSender/types/transactionSender.types'; +import { TransactionsTracker } from 'components/TransactionsTracker'; +import { TransactionsTrackerType } from 'types/transactionsTracker.types'; export interface CustomComponentsType { transactionSender?: { @@ -18,53 +11,26 @@ export interface CustomComponentsType { props?: TransactionSenderType; }; transactionTracker?: { - component: typeof TransactionsTracker; + component?: typeof TransactionsTracker; props?: TransactionsTrackerType; }; - batchTransactionsSender?: { - component: typeof BatchTransactionsSender; - props?: object; - }; - batchTransactionsTracker?: { - component: typeof BatchTransactionsTracker; - props?: BatchTransactionsTrackerProps; - }; } export function CustomComponents({ - customComponents, - enableBatchTransactions + customComponents }: { customComponents?: CustomComponentsType; - enableBatchTransactions?: boolean; }) { const transactionSender = customComponents?.transactionSender; const transactionTracker = customComponents?.transactionTracker; - const batchTransactionsSender = customComponents?.batchTransactionsSender; - const batchTransactionsTracker = customComponents?.batchTransactionsTracker; const TxSender = transactionSender?.component ?? TransactionSender; const TxTracker = transactionTracker?.component ?? TransactionsTracker; - const BatchTxsSender = - batchTransactionsSender?.component ?? BatchTransactionsSender; - const BatchTxsTracker = - batchTransactionsTracker?.component ?? BatchTransactionsTracker; return ( <> - {!Boolean(enableBatchTransactions) && ( - <> - - - - )} - - {Boolean(enableBatchTransactions) && ( - <> - - - - )} + + diff --git a/src/wrappers/DappProvider/DappProvider.tsx b/src/wrappers/DappProvider/DappProvider.tsx index 55426c3cc..e2250d52d 100644 --- a/src/wrappers/DappProvider/DappProvider.tsx +++ b/src/wrappers/DappProvider/DappProvider.tsx @@ -21,7 +21,6 @@ export interface DappProviderPropsType { environment: 'testnet' | 'mainnet' | 'devnet' | EnvironmentsEnum; customComponents?: CustomComponentsType; dappConfig?: DappConfigType; - enableBatchTransactions?: boolean; } export const DappProvider = ({ @@ -30,8 +29,7 @@ export const DappProvider = ({ externalProvider, environment, customComponents, - dappConfig, - enableBatchTransactions + dappConfig }: DappProviderPropsType) => { if (!environment) { //throw if the user tries to initialize the app without a valid environment @@ -54,10 +52,7 @@ export const DappProvider = ({ dappConfig={dappConfig} > - + {children} )} From 54080401887e5e3b7433d69db4ca41acaaaf9fb0 Mon Sep 17 00:00:00 2001 From: cipriandraghici Date: Wed, 6 Sep 2023 17:49:14 +0300 Subject: [PATCH 2/7] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ff9bab20..8be484724 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- [Batch transactions improvements](https://github.com/multiversx/mx-sdk-dapp/pull/905) + ## [[v2.19.9]](https://github.com/multiversx/mx-sdk-dapp/pull/908)] - 2023-09-06 - [Changed `safeRedirect` method to force page reload on logout to ensure fresh states](https://github.com/multiversx/mx-sdk-dapp/pull/907) From dcb3723d84432279e5fe2c6813283acf9bb9acda Mon Sep 17 00:00:00 2001 From: cipriandraghici Date: Wed, 6 Sep 2023 17:49:28 +0300 Subject: [PATCH 3/7] 2.20.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8fe9fcbf4..a64126240 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@multiversx/sdk-dapp", - "version": "2.20.0-alpha.1", + "version": "2.20.0", "description": "A library to hold the main logic for a dapp on the MultiversX blockchain", "author": "MultiversX", "license": "GPL-3.0-or-later", From d2bc9516103409199cb1f8741df0680726211818 Mon Sep 17 00:00:00 2001 From: Ciprian Draghici Date: Thu, 7 Sep 2023 16:58:39 +0300 Subject: [PATCH 4/7] Add batch transactions documentation (#909) * add batch transactions documentation * update changelog * small update * updates after review * updates after review --- CHANGELOG.md | 3 +- README.md | 198 +++++++++++++++++- .../TransactionTracker.tsx | 2 +- src/components/TransactionsTracker/index.ts | 1 - .../transactions}/useTransactionsTracker.ts | 0 .../useClosureRef.ts | 0 src/web/hooks/useIdleTimer.tsx | 2 +- 7 files changed, 199 insertions(+), 7 deletions(-) rename src/{components/TransactionsTracker => hooks/transactions}/useTransactionsTracker.ts (100%) rename src/{components/TransactionsTracker => hooks}/useClosureRef.ts (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8be484724..0ac8fc009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- [Add batch transactions documentation](https://github.com/multiversx/mx-sdk-dapp/pull/909) - [Batch transactions improvements](https://github.com/multiversx/mx-sdk-dapp/pull/905) - +- ## [[v2.19.9]](https://github.com/multiversx/mx-sdk-dapp/pull/908)] - 2023-09-06 - [Changed `safeRedirect` method to force page reload on logout to ensure fresh states](https://github.com/multiversx/mx-sdk-dapp/pull/907) diff --git a/README.md b/README.md index 87c9d857f..edefdcc13 100644 --- a/README.md +++ b/README.md @@ -532,9 +532,125 @@ or the `useSignTransactions` hook defined below. If you don't use one of these, -
+
+ +Sending sync transactions in batches (batch transactions mechanism) + + +### Sending Transactions + +The API for sending sync transactions is a function called **sendBatchTransactions**: + +`import { sendBatchTransactions } from "@multiversx/sdk-dapp/services/transactions/sendBatchTransactions";` + +It can be used to send a group of transactions (that ca be synchronized) with minimum information: + + +```typescript +const { sessionId, error } = await sendBatchTransactions({ + transactions: [ + [ + { + value: '1000000000000000000', + data: 'tx1', + receiver: receiverAddress + }, + ], + [ + { + value: '1000000000000000000', + data: 'tx2', + receiver: receiverAddress + }, + { + value: '1000000000000000000', + data: 'tx3', + receiver: receiverAddress + }, + ] + ], + /** + * the route to be redirected to after signing. Will not redirect if the user is already on the specified route + * @default window.location.pathname + */ + callbackRoute?: string; + /** + * custom message for toasts texts + * @default null + */ + transactionsDisplayInfo: TransactionsDisplayInfoType; + /** + * Minimum amount of gas in order to process the transaction. + * @default 50_000 + */ + minGasLimit?: number (optional, defaults to 50_000); + /** + * Contains extra sessionInformation that will be passed back via getSignedTransactions hook + */ + sessionInformation?: any (optional, defaults to null) + /** + * The transactions will be signed without being sent to the blockchain + */ + signWithoutSending?: boolean; + /** + * Delay the transaction status from going into "successful" state + */ + completedTransactionsDelay?: number; + /** + * If true, redirects to the provided callbackRoute + */ + redirectAfterSigning?: boolean; + }); +``` + +It returns a Promise that will be fulfilled with `{error?: string; sessionId: string | null;}` + +- `sessionId` is the transactions' session id used to compose the `batchId` which will be used to send the batch to the batch service and to track the transactions status and react to it. +- `error` is the error that can appear during the signing/sending process. + +### How to synchronize transactions ? +`sendBatchTransactions` accepts an argument `transactions` which is an array of transactions arrays. +Each transaction array will be sent to the blockchain in the order they are provided. +Having the example above, the transactions will be sent in the following order: +- `tx1` +- `tx2, tx3` + +`tx1` will be sent first, waits until is completed, then `tx2` and `tx3` will be sent in parallel. This means that the groups are sent synchronously, but the transactions inside a group are sent in parallel. + +** Important! This function will send the transactions automatically in batches, based on the provided transactions array, immediately after signing. +If you do not want them to be sent automatically, but on demand, then you should use send callback exposed by the `useSendBatchTransactions` hook. +Be sure to save the `sessionId` passed to the batch. We recommend to generate a new sessionId like this: `Date.now().toString();` ** + +```typescript +import { sendBatchTransactions } from '@multiversx/sdk-dapp/services/transactions/sendBatchTransactions'; +import { useSendBatchTransactions } from '@multiversx/sdk-dapp/hooks/transactions/batch/useSendBatchTransactions'; + + +const { send: sendBatchToBlockchain } = useSendBatchTransactions(); + +// Use signWithoutSending: true to sign the transactions without sending them to the blockchain +sendBatchTransactions({ + transactions, + signWithoutSending: true, + callbackRoute: window.location.pathname, + customTransactionInformation: { redirectAfterSign: true } +}); + +const { error, batchId, data } = await sendBatchToBlockchain({ + transactions, + sessionId +}); +``` + +**Important! For the transaction to be signed, you will have to use either `SignTransactionsModals` defined above, in the `Prerequisites` section, +or the `useSignTransactions` hook defined below. If you don't use one of these, the transactions won't be signed** + +
+ +
+ Transaction Signing Flow - +
### Transaction Signing Flow @@ -619,7 +735,83 @@ Tracking a transaction ### Tracking a transaction -The library exposes a hook called useTrackTransactionStatus; +The library has a built-in implementation for tracking the transactions sent normally of synchronously via batch transactions. +Also, exposes a hook called `useTrackTransactionStatus`; + +### 1. Built-in tracking + +There is a `TransactionTracker` component that is used to track transactions. It is used by default in the `DappProvider` component. +This component is using the `useTransactionsTracker` and `useBatchTransactionsTracker` hooks to track the transactions. + +`useTransactionsTracker` - track transactions sent normally +`useBatchTransactionsTracker` track batch transactions + +The developers are be able to create their own implementation for the transaction tracking component (using these hooks or creating their own logic) and pass it to the `DappProvider` component through `customComponents` field. + +```jsx +import { TransactionsTracker } from "your/module"; + { + console.log(`Session ${sessionId} successfully completed`); + }, + onFail: (sessionId, errorMessage) => { + if (errorMessage) { + console.log(`Session ${sessionId} failed, ${errorMessage}`); + return; + } + + console.log(`Session ${sessionId} failed`); + } + } + } + }} +> +```` +The props passed to the `TransactionsTracker` component are: +```typescript +export interface TransactionsTrackerType { + getTransactionsByHash?: GetTransactionsByHashesType; + onSuccess?: (sessionId: string | null) => void; + onFail?: (sessionId: string | null, errorMessage?: string) => void; +} +``` +Also, the same props can be passed to the `useTransactionsTracker` and `useBatchTransactionsTracker` hooks. + +```typescript +import { useBatchTransactionsTracker } from 'hooks/transactions/batch/tracker/useBatchTransactionsTracker'; +import { useTransactionsTracker } from 'hooks/transactions/useTransactionsTracker'; + +const props = { + onSuccess: (sessionId) => { + console.log(`Session ${sessionId} successfully completed`); + }, + onFail: (sessionId, errorMessage) => { + if (errorMessage) { + console.log(`Session ${sessionId} failed, ${errorMessage}`); + return; + } + + console.log(`Session ${sessionId} failed`); + } +}; + +useTransactionsTracker(props); +useBatchTransactionsTracker(props); +``` + +The transactions trackers will automatically update the transactions statuses in the store. This functionality is used by the `TransactionToastList` component to display the transactions statuses. + +### 2. Tracking transaction status ```typescript import {useTrackTransactionStatus} from @multiversx/sdk-dapp/hooks; diff --git a/src/components/TransactionsTracker/TransactionTracker.tsx b/src/components/TransactionsTracker/TransactionTracker.tsx index faaeb4d93..9383110ef 100644 --- a/src/components/TransactionsTracker/TransactionTracker.tsx +++ b/src/components/TransactionsTracker/TransactionTracker.tsx @@ -1,6 +1,6 @@ import { useBatchTransactionsTracker } from 'hooks/transactions/batch/tracker/useBatchTransactionsTracker'; +import { useTransactionsTracker } from 'hooks/transactions/useTransactionsTracker'; import { TransactionsTrackerType } from 'types/transactionsTracker.types'; -import { useTransactionsTracker } from './useTransactionsTracker'; export function TransactionsTracker(props: TransactionsTrackerType) { useTransactionsTracker(props); diff --git a/src/components/TransactionsTracker/index.ts b/src/components/TransactionsTracker/index.ts index 196aa9def..c4c4a9746 100644 --- a/src/components/TransactionsTracker/index.ts +++ b/src/components/TransactionsTracker/index.ts @@ -1,2 +1 @@ export * from './TransactionTracker'; -export * from './useTransactionsTracker'; diff --git a/src/components/TransactionsTracker/useTransactionsTracker.ts b/src/hooks/transactions/useTransactionsTracker.ts similarity index 100% rename from src/components/TransactionsTracker/useTransactionsTracker.ts rename to src/hooks/transactions/useTransactionsTracker.ts diff --git a/src/components/TransactionsTracker/useClosureRef.ts b/src/hooks/useClosureRef.ts similarity index 100% rename from src/components/TransactionsTracker/useClosureRef.ts rename to src/hooks/useClosureRef.ts diff --git a/src/web/hooks/useIdleTimer.tsx b/src/web/hooks/useIdleTimer.tsx index 5e3edd106..64a2474e4 100644 --- a/src/web/hooks/useIdleTimer.tsx +++ b/src/web/hooks/useIdleTimer.tsx @@ -1,5 +1,5 @@ import { useIdleTimer as useReactIdleTimer } from 'react-idle-timer'; -import { useClosureRef } from 'components/TransactionsTracker/useClosureRef'; +import { useClosureRef } from 'hooks/useClosureRef'; import { useGetIsLoggedIn } from 'hooks/account/useGetIsLoggedIn'; import { useSelector } from 'reduxStore/DappProviderContext'; import { logoutRouteSelector } from 'reduxStore/selectors'; From 67ed6a6cc352fa4b984be36aece4f40007442039 Mon Sep 17 00:00:00 2001 From: cipriandraghici Date: Thu, 7 Sep 2023 17:00:33 +0300 Subject: [PATCH 5/7] update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ac8fc009..3748861e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [[v2.20.0]](https://github.com/multiversx/mx-sdk-dapp/pull/911)] - 2023-09-07 + - [Add batch transactions documentation](https://github.com/multiversx/mx-sdk-dapp/pull/909) - [Batch transactions improvements](https://github.com/multiversx/mx-sdk-dapp/pull/905) -- + ## [[v2.19.9]](https://github.com/multiversx/mx-sdk-dapp/pull/908)] - 2023-09-06 - [Changed `safeRedirect` method to force page reload on logout to ensure fresh states](https://github.com/multiversx/mx-sdk-dapp/pull/907) From e65920ba6c610949f80abaec006e726a3a7b7bfb Mon Sep 17 00:00:00 2001 From: cipriandraghici Date: Thu, 7 Sep 2023 17:20:31 +0300 Subject: [PATCH 6/7] updates after review --- ...endBatchTransactions.spec.ts => sendBatchTransactions.test.ts} | 0 ...Grouping.spec.ts => generateBatchTransactionsGrouping.test.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/services/transactions/{sendBatchTransactions.spec.ts => sendBatchTransactions.test.ts} (100%) rename src/utils/transactions/batch/{generateBatchTransactionsGrouping.spec.ts => generateBatchTransactionsGrouping.test.ts} (100%) diff --git a/src/services/transactions/sendBatchTransactions.spec.ts b/src/services/transactions/sendBatchTransactions.test.ts similarity index 100% rename from src/services/transactions/sendBatchTransactions.spec.ts rename to src/services/transactions/sendBatchTransactions.test.ts diff --git a/src/utils/transactions/batch/generateBatchTransactionsGrouping.spec.ts b/src/utils/transactions/batch/generateBatchTransactionsGrouping.test.ts similarity index 100% rename from src/utils/transactions/batch/generateBatchTransactionsGrouping.spec.ts rename to src/utils/transactions/batch/generateBatchTransactionsGrouping.test.ts From e7c3361cd6aab59a25b24b343407ec7a52d1e928 Mon Sep 17 00:00:00 2001 From: cipriandraghici Date: Thu, 7 Sep 2023 17:21:28 +0300 Subject: [PATCH 7/7] updates after review --- src/apiCalls/transactions/sendSignedBatchTransactions.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/apiCalls/transactions/sendSignedBatchTransactions.ts b/src/apiCalls/transactions/sendSignedBatchTransactions.ts index 5790bb3f5..1aafc584d 100644 --- a/src/apiCalls/transactions/sendSignedBatchTransactions.ts +++ b/src/apiCalls/transactions/sendSignedBatchTransactions.ts @@ -30,6 +30,14 @@ export async function sendSignedBatchTransactions({ const address = addressSelector(store.getState()); const { apiAddress, apiTimeout } = networkSelector(store.getState()); + if (!address) { + return { + error: + 'Invalid address provided. You need to be logged in to send transactions', + batchId: null + }; + } + try { const batchId = buildBatchId({ sessionId,