From 37cc8fd50e5df705eaa7e67bbb16599c08880c6a Mon Sep 17 00:00:00 2001 From: DanutIlie <42973343+DanutIlie@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:35:50 +0200 Subject: [PATCH] Added transaction manager (#41) * added transaction manager * update changelog * updates after review * revert back to singleton * fixies after review --- CHANGELOG.md | 1 + src/constants/transactions.constants.ts | 1 + src/core/managers/TransactionManager.ts | 150 ++++++++++++++++++ .../helpers/sendSignedTransactions.ts | 37 ----- .../sendTransactions/sendTransactions.ts | 26 --- src/types/enums.types.ts | 6 + 6 files changed, 158 insertions(+), 63 deletions(-) create mode 100644 src/core/managers/TransactionManager.ts delete mode 100644 src/core/methods/sendTransactions/helpers/sendSignedTransactions.ts delete mode 100644 src/core/methods/sendTransactions/sendTransactions.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bc0405..1b7f4ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- [Added transaction manager](https://github.com/multiversx/mx-sdk-dapp-core/pull/41) - [Added custom web socket url support](https://github.com/multiversx/mx-sdk-dapp-core/pull/35) - [Metamask integration](https://github.com/multiversx/mx-sdk-dapp-core/pull/27) - [Extension integration](https://github.com/multiversx/mx-sdk-dapp-core/pull/26) diff --git a/src/constants/transactions.constants.ts b/src/constants/transactions.constants.ts index 7db2b66..625c86d 100644 --- a/src/constants/transactions.constants.ts +++ b/src/constants/transactions.constants.ts @@ -4,3 +4,4 @@ export const CROSS_SHARD_ROUNDS = 5; export const TRANSACTIONS_STATUS_POLLING_INTERVAL_MS = 90 * 1000; // 90sec export const TRANSACTIONS_STATUS_DROP_INTERVAL_MS = 10 * 60 * 1000; // 10min export const CANCEL_TRANSACTION_TOAST_DEFAULT_DURATION = 20000; +export const BATCH_TRANSACTIONS_ID_SEPARATOR = '-'; diff --git a/src/core/managers/TransactionManager.ts b/src/core/managers/TransactionManager.ts new file mode 100644 index 0000000..599dbcb --- /dev/null +++ b/src/core/managers/TransactionManager.ts @@ -0,0 +1,150 @@ +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 { networkSelector } from 'store/selectors'; +import { getState } from 'store/store'; +import { GuardianActionsEnum } from 'types'; +import { BatchTransactionsResponseType } from 'types/serverTransactions.types'; +import { SignedTransactionType } from 'types/transactions.types'; + +export class TransactionManager { + private static instance: TransactionManager | null = null; + + private constructor() {} + + public static getInstance(): TransactionManager { + if (!TransactionManager.instance) { + TransactionManager.instance = new TransactionManager(); + } + return TransactionManager.instance; + } + + public send = async ( + signedTransactions: Transaction[] | Transaction[][] + ): Promise => { + if (signedTransactions.length === 0) { + throw new Error('No transactions to send'); + } + + try { + if (!this.isBatchTransaction(signedTransactions)) { + const hashes = await this.sendSignedTransactions(signedTransactions); + return hashes; + } + + const sentTransactions = + await this.sendSignedBatchTransactions(signedTransactions); + + if (!sentTransactions.data || sentTransactions.data.error) { + throw new Error( + sentTransactions.data?.error || 'Failed to send transactions' + ); + } + + const flatSentTransactions = this.sequentialToFlatArray( + sentTransactions.data.transactions + ); + + return flatSentTransactions.map((transaction) => transaction.hash); + } catch (error) { + const responseData = <{ message: string }>( + (error as AxiosError).response?.data + ); + throw responseData?.message ?? (error as any).message; + } + }; + + private sendSignedTransactions = async ( + signedTransactions: Transaction[] + ): Promise => { + const { apiAddress, apiTimeout } = networkSelector(getState()); + + const promises = signedTransactions.map((transaction) => + axios.post(`${apiAddress}/transactions`, transaction.toPlainObject(), { + timeout: Number(apiTimeout) + }) + ); + + const response = await Promise.all(promises); + + return response.map(({ data }) => data.txHash); + }; + + private sendSignedBatchTransactions = async ( + signedTransactions: Transaction[][] + ) => { + const { address } = getAccount(); + const { apiAddress, apiTimeout } = networkSelector(getState()); + + if (!address) { + return { + error: + 'Invalid address provided. You need to be logged in to send transactions' + }; + } + + const batchId = this.buildBatchId(address); + const parsedTransactions = signedTransactions.map((transactions) => + transactions.map((transaction) => + this.parseSignedTransaction(transaction) + ) + ); + + const payload = { + transactions: parsedTransactions, + id: batchId + }; + + const { data } = await axios.post( + `${apiAddress}/batch`, + payload, + { + timeout: Number(apiTimeout) + } + ); + + return { data }; + }; + + private buildBatchId = (address: string) => { + const sessionId = Date.now().toString(); + return `${sessionId}${BATCH_TRANSACTIONS_ID_SEPARATOR}${address}`; + }; + + private sequentialToFlatArray = ( + transactions: SignedTransactionType[] | SignedTransactionType[][] = [] + ) => + this.getIsSequential(transactions) + ? transactions.flat() + : (transactions as SignedTransactionType[]); + + private getIsSequential = ( + transactions?: SignedTransactionType[] | SignedTransactionType[][] + ) => transactions?.every((transaction) => Array.isArray(transaction)); + + private isBatchTransaction = ( + transactions: Transaction[] | Transaction[][] + ): transactions is Transaction[][] => { + return Array.isArray(transactions[0]); + }; + + private parseSignedTransaction = (signedTransaction: Transaction) => { + const parsedTransaction = { + ...signedTransaction.toPlainObject(), + hash: signedTransaction.getHash().hex() + }; + + // TODO: Remove when the protocol supports usernames for guardian transactions + if (this.isGuardianTx(parsedTransaction.data)) { + delete parsedTransaction.senderUsername; + delete parsedTransaction.receiverUsername; + } + + return parsedTransaction; + }; + + private isGuardianTx = (transactionData?: string) => + transactionData && + transactionData.startsWith(GuardianActionsEnum.SetGuardian); +} diff --git a/src/core/methods/sendTransactions/helpers/sendSignedTransactions.ts b/src/core/methods/sendTransactions/helpers/sendSignedTransactions.ts deleted file mode 100644 index f1d4c8d..0000000 --- a/src/core/methods/sendTransactions/helpers/sendSignedTransactions.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Transaction } from '@multiversx/sdk-core'; -import axios from 'axios'; -import { networkSelector } from 'store/selectors'; -import { getState } from 'store/store'; -import { TransactionServerStatusesEnum } from 'types'; -import { SignedTransactionType } from 'types/transactions.types'; - -export async function sendSignedTransactions( - signedTransactions: Transaction[] -): Promise { - const { apiAddress, apiTimeout } = networkSelector(getState()); - - const promises = signedTransactions.map((transaction) => { - return axios.post( - `${apiAddress}/transactions`, - transaction.toPlainObject(), - { timeout: parseInt(apiTimeout) } - ); - }); - - const response = await Promise.all(promises); - - const sentTransactions: SignedTransactionType[] = []; - - response.forEach(({ data }, i) => { - const currentTransaction = signedTransactions[i]; - if (currentTransaction.getHash().hex() === data.txHash) { - sentTransactions.push({ - ...currentTransaction.toPlainObject(), - hash: data.txHash, - status: TransactionServerStatusesEnum.pending - }); - } - }); - - return sentTransactions; -} diff --git a/src/core/methods/sendTransactions/sendTransactions.ts b/src/core/methods/sendTransactions/sendTransactions.ts deleted file mode 100644 index d1a5bc3..0000000 --- a/src/core/methods/sendTransactions/sendTransactions.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Transaction } from '@multiversx/sdk-core/out'; -import { AxiosError } from 'axios'; -import { createTransactionsSession } from 'store/actions/transactions/transactionsActions'; -import { sendSignedTransactions } from './helpers/sendSignedTransactions'; - -export async function sendTransactions( - transactions: Transaction[] = [] -): Promise { - if (transactions.length === 0) { - return null; - } - - try { - const sentTransactions = await sendSignedTransactions(transactions); - const sessionId = createTransactionsSession({ - transactions: sentTransactions - }); - - return sessionId; - } catch (error) { - const responseData = <{ message: string }>( - (error as AxiosError).response?.data - ); - throw responseData?.message ?? (error as any).message; - } -} diff --git a/src/types/enums.types.ts b/src/types/enums.types.ts index 4091e71..da0044c 100644 --- a/src/types/enums.types.ts +++ b/src/types/enums.types.ts @@ -42,3 +42,9 @@ export enum ESDTTransferTypesEnum { ESDTWipe = 'ESDTWipe', ESDTFreeze = 'ESDTFreeze' } + +export enum GuardianActionsEnum { + SetGuardian = 'SetGuardian', + GuardAccount = 'GuardAccount', + UnGuardAccount = 'UnGuardAccount' +}