Skip to content

Commit

Permalink
Added transaction manager (#41)
Browse files Browse the repository at this point in the history
* added transaction manager

* update changelog

* updates after review

* revert back to singleton

* fixies after review
  • Loading branch information
DanutIlie authored Dec 9, 2024
1 parent 6c5da6e commit 37cc8fd
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 63 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/constants/transactions.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '-';
150 changes: 150 additions & 0 deletions src/core/managers/TransactionManager.ts
Original file line number Diff line number Diff line change
@@ -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<string[]> => {
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<string[]> => {
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<BatchTransactionsResponseType>(
`${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);
}

This file was deleted.

26 changes: 0 additions & 26 deletions src/core/methods/sendTransactions/sendTransactions.ts

This file was deleted.

6 changes: 6 additions & 0 deletions src/types/enums.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,9 @@ export enum ESDTTransferTypesEnum {
ESDTWipe = 'ESDTWipe',
ESDTFreeze = 'ESDTFreeze'
}

export enum GuardianActionsEnum {
SetGuardian = 'SetGuardian',
GuardAccount = 'GuardAccount',
UnGuardAccount = 'UnGuardAccount'
}

0 comments on commit 37cc8fd

Please sign in to comment.