From b89cc1eb1daa30a54037cc00b45994ae012378ff Mon Sep 17 00:00:00 2001 From: Tudor Morar Date: Mon, 16 Sep 2024 11:17:46 +0300 Subject: [PATCH] Sign, send and track transactions (#21) * Add transactions tracking --- CHANGELOG.md | 1 + package.json | 11 +- .../getAccountFromApi.ts | 0 src/apiCalls/{accounts => account}/index.ts | 0 .../transactions/getTransactionByHash.ts | 16 + .../transactions/getTransactionsByHashes.ts | 44 +++ src/apiCalls/websocket/getWebsocketUrl.ts | 13 + src/apiCalls/websocket/index.ts | 1 + src/constants/index.ts | 2 +- src/constants/ledger.constants.ts | 1 - src/constants/mvx.constants.ts | 14 + src/constants/network.constants.ts | 9 +- src/constants/placeholders.ts | 8 - src/constants/transactions.constants.ts | 6 + src/core/methods/account/getAccount.ts | 4 +- src/core/methods/initApp/initApp.ts | 10 + src/core/methods/initApp/initApp.types.ts | 4 + .../initializeWebsocketConnection.ts | 113 ++++++ .../initApp/websocket/registerWebsocket.ts | 25 ++ .../initApp/websocket/websocket.constants.ts | 21 ++ .../login/helpers/getAccountFromToken.ts | 4 +- src/core/methods/login/login.ts | 20 +- .../helpers/sendSignedTransactions.ts | 37 ++ .../sendTransactions/sendTransactions.ts | 27 ++ .../signTransactions/signTransactions.ts | 35 ++ .../checkTransactionStatus/checkBatch.ts | 190 ++++++++++ .../checkTransactionStatus.ts | 27 ++ .../getPendingTransactions.ts | 30 ++ .../helpers/checkTransactionStatus/index.ts | 1 + .../manageFailedTransactions.ts | 39 ++ .../helpers/getPendingStoreTransactions.ts | 25 ++ .../helpers/getPollingInterval.ts | 14 + .../trackTransactions/trackTransactions.ts | 73 ++++ .../trackTransactions.types.ts | 7 + src/store/actions/account/accountActions.ts | 3 +- .../actions/sharedActions/sharedActions.ts | 5 +- .../transactions/transactionStateByStatus.ts | 143 ++++++++ .../transactions/transactionsActions.ts | 104 ++++++ src/store/selectors/accountSelectors.ts | 4 + src/store/selectors/networkSelectors.ts | 4 + src/store/selectors/transactionsSelector.ts | 40 ++ src/store/slices/account/emptyAccount.ts | 2 +- src/store/slices/index.ts | 1 + src/store/slices/network/emptyNetwork.ts | 3 +- src/store/slices/transactions/index.ts | 1 + .../slices/transactions/transacitionsSlice.ts | 16 + .../transactions/transacitionsSlice.types.ts | 10 + src/store/store.ts | 8 +- src/store/store.types.ts | 2 + src/types/enums.types.ts | 20 + src/types/network.types.ts | 2 + src/types/serverTransactions.types.ts | 347 ++++++++++++++++++ src/types/tokens.types.ts | 136 +++++++ src/types/transactions.types.ts | 50 +++ src/utils/account/fetchAccount.ts | 3 + src/utils/account/getAccount.ts | 3 - src/utils/account/index.ts | 3 +- src/utils/account/refreshAccount.ts | 58 +++ yarn.lock | 146 ++++---- 59 files changed, 1833 insertions(+), 113 deletions(-) rename src/apiCalls/{accounts => account}/getAccountFromApi.ts (100%) rename src/apiCalls/{accounts => account}/index.ts (100%) create mode 100644 src/apiCalls/transactions/getTransactionByHash.ts create mode 100644 src/apiCalls/transactions/getTransactionsByHashes.ts create mode 100644 src/apiCalls/websocket/getWebsocketUrl.ts create mode 100644 src/apiCalls/websocket/index.ts delete mode 100644 src/constants/ledger.constants.ts create mode 100644 src/constants/mvx.constants.ts delete mode 100644 src/constants/placeholders.ts create mode 100644 src/constants/transactions.constants.ts create mode 100644 src/core/methods/initApp/websocket/initializeWebsocketConnection.ts create mode 100644 src/core/methods/initApp/websocket/registerWebsocket.ts create mode 100644 src/core/methods/initApp/websocket/websocket.constants.ts create mode 100644 src/core/methods/sendTransactions/helpers/sendSignedTransactions.ts create mode 100644 src/core/methods/sendTransactions/sendTransactions.ts create mode 100644 src/core/methods/signTransactions/signTransactions.ts create mode 100644 src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkBatch.ts create mode 100644 src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkTransactionStatus.ts create mode 100644 src/core/methods/trackTransactions/helpers/checkTransactionStatus/getPendingTransactions.ts create mode 100644 src/core/methods/trackTransactions/helpers/checkTransactionStatus/index.ts create mode 100644 src/core/methods/trackTransactions/helpers/checkTransactionStatus/manageFailedTransactions.ts create mode 100644 src/core/methods/trackTransactions/helpers/getPendingStoreTransactions.ts create mode 100644 src/core/methods/trackTransactions/helpers/getPollingInterval.ts create mode 100644 src/core/methods/trackTransactions/trackTransactions.ts create mode 100644 src/core/methods/trackTransactions/trackTransactions.types.ts create mode 100644 src/store/actions/transactions/transactionStateByStatus.ts create mode 100644 src/store/actions/transactions/transactionsActions.ts create mode 100644 src/store/selectors/transactionsSelector.ts create mode 100644 src/store/slices/transactions/index.ts create mode 100644 src/store/slices/transactions/transacitionsSlice.ts create mode 100644 src/store/slices/transactions/transacitionsSlice.types.ts create mode 100644 src/types/serverTransactions.types.ts create mode 100644 src/types/tokens.types.ts create mode 100644 src/types/transactions.types.ts create mode 100644 src/utils/account/fetchAccount.ts delete mode 100644 src/utils/account/getAccount.ts create mode 100644 src/utils/account/refreshAccount.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b8669..b1f159d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- [Added sign, send, & track transactions with websocket connection](https://github.com/multiversx/mx-sdk-dapp-core/pull/21) - [Added restore provider after page reload](https://github.com/multiversx/mx-sdk-dapp-core/pull/19) - [Added signMessage](https://github.com/multiversx/mx-sdk-dapp-core/pull/18) diff --git a/package.json b/package.json index a15d07c..94b49ab 100644 --- a/package.json +++ b/package.json @@ -33,16 +33,16 @@ "@lifeomic/axios-fetch": "3.0.1", "@multiversx/sdk-extension-provider": "3.0.0", "@multiversx/sdk-hw-provider": "6.4.0", - "@multiversx/sdk-metamask-provider": "0.0.5", + "@multiversx/sdk-metamask-provider": "0.0.7", "@multiversx/sdk-native-auth-client": "^1.0.8", "@multiversx/sdk-opera-provider": "1.0.0-alpha.1", "@multiversx/sdk-wallet": "4.5.1", "@multiversx/sdk-wallet-connect-provider": "4.1.2", "@multiversx/sdk-web-wallet-provider": "3.2.1", - "@types/lodash": "^4.17.4", - "isomorphic-fetch": "^3.0.0", - "lodash": "^4.17.21", - "zustand": "^4.4.7" + "isomorphic-fetch": "3.0.0", + "lodash": "4.17.21", + "socket.io-client": "4.7.5", + "zustand": "4.4.7" }, "peerDependencies": { "@multiversx/sdk-core": ">= 13.0.0", @@ -55,6 +55,7 @@ "string-width": "4.1.0" }, "devDependencies": { + "@types/lodash": "4.17.4", "@multiversx/sdk-core": ">= 13.0.0", "@multiversx/sdk-dapp-utils": ">= 0.1.0", "@multiversx/sdk-web-wallet-cross-window-provider": ">= 1.0.0", diff --git a/src/apiCalls/accounts/getAccountFromApi.ts b/src/apiCalls/account/getAccountFromApi.ts similarity index 100% rename from src/apiCalls/accounts/getAccountFromApi.ts rename to src/apiCalls/account/getAccountFromApi.ts diff --git a/src/apiCalls/accounts/index.ts b/src/apiCalls/account/index.ts similarity index 100% rename from src/apiCalls/accounts/index.ts rename to src/apiCalls/account/index.ts diff --git a/src/apiCalls/transactions/getTransactionByHash.ts b/src/apiCalls/transactions/getTransactionByHash.ts new file mode 100644 index 0000000..6d8d591 --- /dev/null +++ b/src/apiCalls/transactions/getTransactionByHash.ts @@ -0,0 +1,16 @@ +import axios from 'axios'; +import { TRANSACTIONS_ENDPOINT } from 'apiCalls/endpoints'; +import { getState } from 'store/store'; +import { networkSelector } from 'store/selectors'; +import { ServerTransactionType } from 'types/serverTransactions.types'; + +export const getTransactionByHash = (hash: string) => { + const { apiAddress } = networkSelector(getState()); + + return axios.get( + `${apiAddress}/${TRANSACTIONS_ENDPOINT}/${hash}`, + { + timeout: 10000 // 10sec + } + ); +}; diff --git a/src/apiCalls/transactions/getTransactionsByHashes.ts b/src/apiCalls/transactions/getTransactionsByHashes.ts new file mode 100644 index 0000000..4df726f --- /dev/null +++ b/src/apiCalls/transactions/getTransactionsByHashes.ts @@ -0,0 +1,44 @@ +import axios from 'axios'; +import { TRANSACTIONS_ENDPOINT } from 'apiCalls/endpoints'; + +import { getState } from 'store/store'; +import { networkSelector } from 'store/selectors'; +import { + GetTransactionsByHashesReturnType, + PendingTransactionsType +} from 'types/transactions.types'; + +export const getTransactionsByHashes = async ( + pendingTransactions: PendingTransactionsType +): Promise => { + const { apiAddress } = networkSelector(getState()); + const hashes = pendingTransactions.map((tx) => tx.hash); + + const { data: responseData } = await axios.get( + `${apiAddress}/${TRANSACTIONS_ENDPOINT}`, + { + params: { + hashes: hashes.join(','), + withScResults: true + } + } + ); + + return pendingTransactions.map(({ hash, previousStatus }) => { + const txOnNetwork = responseData.find( + (txResponse: any) => txResponse?.txHash === hash + ); + + return { + hash, + data: txOnNetwork?.data, + invalidTransaction: txOnNetwork == null, + status: txOnNetwork?.status, + results: txOnNetwork?.results, + sender: txOnNetwork?.sender, + receiver: txOnNetwork?.receiver, + previousStatus, + hasStatusChanged: txOnNetwork && txOnNetwork.status !== previousStatus + }; + }); +}; diff --git a/src/apiCalls/websocket/getWebsocketUrl.ts b/src/apiCalls/websocket/getWebsocketUrl.ts new file mode 100644 index 0000000..58bb9bc --- /dev/null +++ b/src/apiCalls/websocket/getWebsocketUrl.ts @@ -0,0 +1,13 @@ +import axios from 'axios'; + +export async function getWebsocketUrl(apiAddress: string) { + try { + const { data } = await axios.get<{ url: string }>( + `${apiAddress}/websocket/config` + ); + return `wss://${data.url}`; + } catch (err) { + console.error(err); + throw new Error('Can not get websocket url'); + } +} diff --git a/src/apiCalls/websocket/index.ts b/src/apiCalls/websocket/index.ts new file mode 100644 index 0000000..a1842b1 --- /dev/null +++ b/src/apiCalls/websocket/index.ts @@ -0,0 +1 @@ +export * from './getWebsocketUrl'; diff --git a/src/constants/index.ts b/src/constants/index.ts index 9ebad72..6047bdd 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -4,4 +4,4 @@ export * from './storage.constants'; export * from './window.constants'; export * from './browser.constants'; export * from './errorMessages.constants'; -export * from './ledger.constants'; +export * from './mvx.constants'; diff --git a/src/constants/ledger.constants.ts b/src/constants/ledger.constants.ts deleted file mode 100644 index 7941247..0000000 --- a/src/constants/ledger.constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const LEDGER_CONTRACT_DATA_ENABLED_VALUE = 1; diff --git a/src/constants/mvx.constants.ts b/src/constants/mvx.constants.ts new file mode 100644 index 0000000..c983a41 --- /dev/null +++ b/src/constants/mvx.constants.ts @@ -0,0 +1,14 @@ +export const GAS_PRICE_MODIFIER = 0.01; +export const GAS_PER_DATA_BYTE = 1500; +export const GAS_LIMIT = 50000; +/** + * Extra gas limit for guarded transactions + */ +export const EXTRA_GAS_LIMIT_GUARDED_TX = 50000; +export const GAS_PRICE = 1000000000; +export const DECIMALS = 18; +export const DIGITS = 4; +export const VERSION = 1; +export const LEDGER_CONTRACT_DATA_ENABLED_VALUE = 1; +export const METACHAIN_SHARD_ID = 4294967295; +export const ALL_SHARDS_SHARD_ID = 4294967280; diff --git a/src/constants/network.constants.ts b/src/constants/network.constants.ts index d8b5490..062f942 100644 --- a/src/constants/network.constants.ts +++ b/src/constants/network.constants.ts @@ -21,7 +21,8 @@ export const fallbackNetworkConfigurations: Record< xAliasAddress: 'https://devnet.xalias.com', apiAddress: 'https://devnet-api.multiversx.com', explorerAddress: 'http://devnet-explorer.multiversx.com', - apiTimeout: '4000' + apiTimeout: '4000', + roundDuration: 6000 }, testnet: { id: 'testnet', @@ -39,7 +40,8 @@ export const fallbackNetworkConfigurations: Record< xAliasAddress: 'https://testnet.xalias.com', apiAddress: 'https://testnet-api.multiversx.com', explorerAddress: 'http://testnet-explorer.multiversx.com', - apiTimeout: '4000' + apiTimeout: '4000', + roundDuration: 6000 }, mainnet: { id: 'mainnet', @@ -57,7 +59,8 @@ export const fallbackNetworkConfigurations: Record< xAliasAddress: 'https://xalias.com', apiAddress: 'https://api.multiversx.com', explorerAddress: 'https://explorer.multiversx.com', - apiTimeout: '4000' + apiTimeout: '4000', + roundDuration: 6000 } }; diff --git a/src/constants/placeholders.ts b/src/constants/placeholders.ts deleted file mode 100644 index 70dd268..0000000 --- a/src/constants/placeholders.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Not Applicable - * @value N/A - */ -export const N_A = 'N/A'; - -export const ZERO = '0'; -export const ELLIPSIS = '...'; diff --git a/src/constants/transactions.constants.ts b/src/constants/transactions.constants.ts new file mode 100644 index 0000000..7db2b66 --- /dev/null +++ b/src/constants/transactions.constants.ts @@ -0,0 +1,6 @@ +export const CANCEL_TRANSACTION_TOAST_ID = 'cancel-transaction-toast'; +export const AVERAGE_TX_DURATION_MS = 6000; +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; diff --git a/src/core/methods/account/getAccount.ts b/src/core/methods/account/getAccount.ts index 341e426..7fcc837 100644 --- a/src/core/methods/account/getAccount.ts +++ b/src/core/methods/account/getAccount.ts @@ -1,6 +1,6 @@ import { accountSelector } from 'store/selectors'; import { getState } from 'store/store'; -export function getAccount() { - return accountSelector(getState()); +export function getAccount(state = getState()) { + return accountSelector(state); } diff --git a/src/core/methods/initApp/initApp.ts b/src/core/methods/initApp/initApp.ts index e3863db..d717fdf 100644 --- a/src/core/methods/initApp/initApp.ts +++ b/src/core/methods/initApp/initApp.ts @@ -7,6 +7,8 @@ import { getDefaultNativeAuthConfig } from 'services/nativeAuth/methods/getDefau import { InitAppType } from './initApp.types'; import { getIsLoggedIn } from '../account/getIsLoggedIn'; import { restoreProvider } from 'core/providers/helpers/restoreProvider'; +import { registerWebsocketListener } from './websocket/registerWebsocket'; +import { trackTransactions } from '../trackTransactions/trackTransactions'; const defaultInitAppProps = { storage: { @@ -32,6 +34,9 @@ export const initApp = async ({ }: InitAppType) => { initStore(storage.getStorageCallback); + const shouldEnableTransactionTracker = + dAppConfig.enableTansactionTracker !== false; + if (dAppConfig?.nativeAuth) { const nativeAuthConfig: NativeAuthConfigType = typeof dAppConfig.nativeAuth === 'boolean' @@ -46,9 +51,14 @@ export const initApp = async ({ environment: dAppConfig.environment }); + if (shouldEnableTransactionTracker) { + trackTransactions(); + } + const isLoggedIn = getIsLoggedIn(); if (isLoggedIn) { await restoreProvider(); + await registerWebsocketListener(); } }; diff --git a/src/core/methods/initApp/initApp.types.ts b/src/core/methods/initApp/initApp.types.ts index 875c84d..f4121e7 100644 --- a/src/core/methods/initApp/initApp.types.ts +++ b/src/core/methods/initApp/initApp.types.ts @@ -11,6 +11,10 @@ type BaseDappConfigType = { * If set to `NativeAuthConfigType`, will set the native auth configuration. */ nativeAuth?: boolean | NativeAuthConfigType; + /** + * default: `true` + */ + enableTansactionTracker?: boolean; }; export type EnvironmentDappConfigType = BaseDappConfigType & { diff --git a/src/core/methods/initApp/websocket/initializeWebsocketConnection.ts b/src/core/methods/initApp/websocket/initializeWebsocketConnection.ts new file mode 100644 index 0000000..ddb2684 --- /dev/null +++ b/src/core/methods/initApp/websocket/initializeWebsocketConnection.ts @@ -0,0 +1,113 @@ +import { io } from 'socket.io-client'; +import { retryMultipleTimes } from 'utils/retryMultipleTimes'; +import { + BatchTransactionsWSResponseType, + websocketConnection, + WebsocketConnectionStatusEnum +} from './websocket.constants'; +import { getWebsocketUrl } from 'apiCalls/websocket'; +import { getStore } from 'store/store'; +import { getAccount } from 'core/methods/account/getAccount'; +import { networkSelector } from 'store/selectors'; +import { + setWebsocketBatchEvent, + setWebsocketEvent +} from 'store/actions/account/accountActions'; + +const TIMEOUT = 3000; +const RECONNECTION_ATTEMPTS = 3; +const RETRY_INTERVAL = 500; +const MESSAGE_DELAY = 1000; +const BATCH_UPDATED_EVENT = 'batchUpdated'; +const CONNECT = 'connect'; +const DISCONNECT = 'disconnect'; + +export async function initializeWebsocketConnection() { + const { address } = getAccount(); + const { apiAddress } = networkSelector(getStore().getState()); + + let messageTimeout: NodeJS.Timeout | null = null; + let batchTimeout: NodeJS.Timeout | null = null; + + const handleMessageReceived = (message: string) => { + if (messageTimeout) { + clearTimeout(messageTimeout); + } + messageTimeout = setTimeout(() => { + setWebsocketEvent(message); + }, MESSAGE_DELAY); + }; + + const handleBatchUpdate = (data: BatchTransactionsWSResponseType) => { + if (batchTimeout) { + clearTimeout(batchTimeout); + } + batchTimeout = setTimeout(() => { + setWebsocketBatchEvent(data); + }, MESSAGE_DELAY); + }; + + const initializeConnection = retryMultipleTimes( + async () => { + // To avoid multiple connections to the same endpoint + websocketConnection.status = WebsocketConnectionStatusEnum.PENDING; + + const websocketUrl = await getWebsocketUrl(apiAddress); + + if (!websocketUrl) { + console.warn('Cannot get websocket URL'); + return; + } + + websocketConnection.instance = io(websocketUrl, { + forceNew: true, + reconnectionAttempts: RECONNECTION_ATTEMPTS, + timeout: TIMEOUT, + query: { address } + }); + + websocketConnection.status = WebsocketConnectionStatusEnum.COMPLETED; + + websocketConnection.instance.onAny(handleMessageReceived); + + websocketConnection.instance.on(BATCH_UPDATED_EVENT, handleBatchUpdate); + + websocketConnection.instance.on(CONNECT, () => { + console.log('Websocket connected.'); + }); + + websocketConnection.instance.on(DISCONNECT, () => { + console.warn('Websocket disconnected. Trying to reconnect...'); + setTimeout(() => { + console.log('Websocket reconnecting...'); + websocketConnection.instance?.connect(); + }, RETRY_INTERVAL); + }); + }, + { retries: 2, delay: RETRY_INTERVAL } + ); + + const closeConnection = () => { + websocketConnection.instance?.close(); + websocketConnection.status = WebsocketConnectionStatusEnum.NOT_INITIALIZED; + if (messageTimeout) { + clearTimeout(messageTimeout); + } + if (batchTimeout) { + clearTimeout(batchTimeout); + } + }; + + if ( + address && + websocketConnection.status === + WebsocketConnectionStatusEnum.NOT_INITIALIZED && + !websocketConnection.instance?.active + ) { + await initializeConnection(); + } + + return { + closeConnection + }; +} diff --git a/src/core/methods/initApp/websocket/registerWebsocket.ts b/src/core/methods/initApp/websocket/registerWebsocket.ts new file mode 100644 index 0000000..f3e2c59 --- /dev/null +++ b/src/core/methods/initApp/websocket/registerWebsocket.ts @@ -0,0 +1,25 @@ +import { initializeWebsocketConnection } from './initializeWebsocketConnection'; +import { getStore } from 'store/store'; +import { getAccount } from 'core/methods/account/getAccount'; + +let localAddress = ''; +let closeConnectionRef: () => void; + +export const registerWebsocketListener = async () => { + const store = getStore(); + const account = getAccount(); + localAddress = account.address; + + // Initialize the websocket connection + const data = await initializeWebsocketConnection(); + closeConnectionRef = data.closeConnection; + + store.subscribe(async ({ account: { address } }) => { + if (localAddress && address !== localAddress) { + closeConnectionRef(); + localAddress = address; + const { closeConnection } = await initializeWebsocketConnection(); + closeConnectionRef = closeConnection; + } + }); +}; diff --git a/src/core/methods/initApp/websocket/websocket.constants.ts b/src/core/methods/initApp/websocket/websocket.constants.ts new file mode 100644 index 0000000..c79cbac --- /dev/null +++ b/src/core/methods/initApp/websocket/websocket.constants.ts @@ -0,0 +1,21 @@ +import { Socket } from 'socket.io-client'; + +export type BatchTransactionsWSResponseType = { + batchId: string; + txHashes: string[]; +}; + +export enum WebsocketConnectionStatusEnum { + NOT_INITIALIZED = 'not_initialized', + PENDING = 'pending', + COMPLETED = 'completed' +} + +export const websocketConnection: { + instance: Socket | null; + // Use the connection status to avoid multiple websocket connections + status: WebsocketConnectionStatusEnum; +} = { + instance: null, + status: WebsocketConnectionStatusEnum.NOT_INITIALIZED +}; diff --git a/src/core/methods/login/helpers/getAccountFromToken.ts b/src/core/methods/login/helpers/getAccountFromToken.ts index 8c1b67e..2b83852 100644 --- a/src/core/methods/login/helpers/getAccountFromToken.ts +++ b/src/core/methods/login/helpers/getAccountFromToken.ts @@ -1,4 +1,4 @@ -import { getAccount } from 'utils/account/getAccount'; +import { fetchAccount } from 'utils/account/fetchAccount'; import { getModifiedLoginToken } from './getModifiedLoginToken'; interface GetAccountFromTokenType { @@ -25,7 +25,7 @@ export const getAccountFromToken = async ({ const accountAddress = modifiedLoginToken != null ? tokenAddress : address; - const account = await getAccount(accountAddress); + const account = await fetchAccount(accountAddress); return { account, diff --git a/src/core/methods/login/login.ts b/src/core/methods/login/login.ts index b8586b3..12a3adf 100644 --- a/src/core/methods/login/login.ts +++ b/src/core/methods/login/login.ts @@ -15,10 +15,11 @@ import { getState } from 'store/store'; import { NativeAuthConfigType } from 'services/nativeAuth/nativeAuth.types'; import { getIsLoggedIn } from 'core/methods/account/getIsLoggedIn'; import { getAddress } from 'core/methods/account/getAddress'; -import { loginAction, logoutAction } from 'store/actions'; +import { logoutAction } from 'store/actions'; import { extractAccountFromToken } from './helpers/extractAccountFromToken'; import { SECOND_LOGIN_ATTEMPT_ERROR } from 'constants/errorMessages.constants'; import { getCallbackUrl } from './helpers/getCallbackUrl'; +import { registerWebsocketListener } from '../initApp/websocket/registerWebsocket'; async function loginWithoutNativeToken(provider: IProvider) { await provider.login?.({ @@ -83,11 +84,6 @@ async function loginWithNativeToken( nativeAuthToken }); - loginAction({ - address, - providerType: provider.getType() - }); - const accountDetails = await extractAccountFromToken({ loginToken, extraInfoData: { @@ -98,14 +94,14 @@ async function loginWithNativeToken( provider }); - if (accountDetails.account) { - setAccount(accountDetails.account); - } else { + if (!accountDetails.account) { logoutAction(); console.error('Failed to fetch account'); throw new Error('Failed to fetch account'); } + await registerWebsocketListener(); + return { address: accountDetails?.address || address, signature, @@ -144,5 +140,9 @@ export const login = async ({ return await loginWithNativeToken(provider, nativeAuthConfig); } - return await loginWithoutNativeToken(provider); + const data = await loginWithoutNativeToken(provider); + + await registerWebsocketListener(); + + return data; }; diff --git a/src/core/methods/sendTransactions/helpers/sendSignedTransactions.ts b/src/core/methods/sendTransactions/helpers/sendSignedTransactions.ts new file mode 100644 index 0000000..f1d4c8d --- /dev/null +++ b/src/core/methods/sendTransactions/helpers/sendSignedTransactions.ts @@ -0,0 +1,37 @@ +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 new file mode 100644 index 0000000..f043020 --- /dev/null +++ b/src/core/methods/sendTransactions/sendTransactions.ts @@ -0,0 +1,27 @@ +import { Transaction } from '@multiversx/sdk-core/out'; +import { AxiosError } from 'axios'; +import { sendSignedTransactions } from './helpers/sendSignedTransactions'; +import { SignedTransactionType } from 'types/transactions.types'; +import { createTransactionsSession } from 'store/actions/transactions/transactionsActions'; + +export const sendTransactions = async ( + 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/core/methods/signTransactions/signTransactions.ts b/src/core/methods/signTransactions/signTransactions.ts new file mode 100644 index 0000000..3ced0d2 --- /dev/null +++ b/src/core/methods/signTransactions/signTransactions.ts @@ -0,0 +1,35 @@ +import { + Transaction, + TransactionOptions, + TransactionVersion +} from '@multiversx/sdk-core/out'; +import { getAccountProvider } from 'core/providers'; +import { getAccount } from '../account/getAccount'; + +type SignTransactionsOptionsType = { + skipGuardian?: boolean; +}; + +export const signTransactions = async ( + transactions: Transaction[], + options: SignTransactionsOptionsType = {} +): Promise => { + const provider = getAccountProvider(); + const { isGuarded } = getAccount(); + + const transacitonsToSign = + isGuarded && !options.skipGuardian + ? transactions?.map((transaction) => { + transaction.setVersion(TransactionVersion.withTxOptions()); + transaction.setOptions( + TransactionOptions.withOptions({ guarded: true }) + ); + return transaction; + }) + : transactions; + + const signedTransactions: Transaction[] = + (await provider.signTransactions(transacitonsToSign)) ?? []; + + return signedTransactions; +}; diff --git a/src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkBatch.ts b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkBatch.ts new file mode 100644 index 0000000..cfb0727 --- /dev/null +++ b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkBatch.ts @@ -0,0 +1,190 @@ +import { + TransactionBatchStatusesEnum, + TransactionServerStatusesEnum +} from 'types/enums.types'; +import { refreshAccount } from 'utils/account'; + +import { getPendingTransactions } from './getPendingTransactions'; +import { manageFailedTransactions } from './manageFailedTransactions'; +import { TransactionsTrackerType } from '../../trackTransactions.types'; +import { + GetTransactionsByHashesReturnType, + SignedTransactionType +} from 'types/transactions.types'; +import { + BatchTransactionStatus, + ServerTransactionType +} from 'types/serverTransactions.types'; +import { + updateSignedTransactionStatus, + updateTransactionsSession +} from 'store/actions/transactions/transactionsActions'; +import { + getIsTransactionFailed, + getIsTransactionSuccessful +} from 'store/actions/transactions/transactionStateByStatus'; +import { getTransactionsByHashes } from 'apiCalls/transactions/getTransactionsByHashes'; + +export interface TransactionStatusTrackerPropsType + extends TransactionsTrackerType { + sessionId: string; + transactionBatch: SignedTransactionType[]; + shouldRefreshBalance?: boolean; + isSequential?: boolean; +} + +interface RetriesType { + [hash: string]: number; +} + +const retries: RetriesType = {}; +const timeouts: string[] = []; + +interface ManageTransactionType { + serverTransaction: GetTransactionsByHashesReturnType[0]; + sessionId: string; + shouldRefreshBalance?: boolean; + isSequential?: boolean; +} + +function manageTransaction({ + serverTransaction, + sessionId, + shouldRefreshBalance, + isSequential +}: ManageTransactionType) { + const { + hash, + status, + inTransit, + results, + invalidTransaction, + hasStatusChanged + } = serverTransaction; + try { + if (timeouts.includes(hash)) { + return; + } + + const retriesForThisHash = retries[hash]; + if (retriesForThisHash > 30) { + // consider transaction as stuck after 1 minute + updateTransactionsSession({ + sessionId, + status: TransactionBatchStatusesEnum.timedOut + }); + return; + } + + if ( + (invalidTransaction && !isSequential) || + status === TransactionBatchStatusesEnum.sent + ) { + retries[hash] = retries[hash] ? retries[hash] + 1 : 1; + return; + } + + // 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({ + sessionId, + status, + transactionHash: hash, + inTransit, + serverTransaction: serverTransaction as unknown as ServerTransactionType + }); + return; + } + + if (hasStatusChanged) { + updateSignedTransactionStatus({ + sessionId, + status, + transactionHash: hash, + inTransit, + serverTransaction: serverTransaction as unknown as ServerTransactionType + }); + } + + // if set to true will trigger a balance refresh after each iteration + if (!shouldRefreshBalance) { + refreshAccount(); + } + + if (getIsTransactionFailed(status)) { + manageFailedTransactions({ sessionId, hash, results }); + } + } catch (error) { + console.error(error); + updateTransactionsSession({ + sessionId, + status: TransactionBatchStatusesEnum.timedOut + }); + } +} + +export async function checkBatch({ + sessionId, + transactionBatch: transactions, + getTransactionsByHash = getTransactionsByHashes, + shouldRefreshBalance, + isSequential, + onSuccess, + onFail +}: TransactionStatusTrackerPropsType) { + try { + if (transactions == null) { + return; + } + + const pendingTransactions = getPendingTransactions(transactions, timeouts); + + const serverTransactions = await getTransactionsByHash(pendingTransactions); + + for (const serverTransaction of serverTransactions) { + manageTransaction({ + serverTransaction, + sessionId, + shouldRefreshBalance, + 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. + + // TODO: check grouping and sequential transactions + if (hasCompleted /* && !customTransactionInformation?.grouping */) { + const isSuccessful = serverTransactions.every( + (tx) => tx.status === TransactionServerStatusesEnum.success + ); + + if (isSuccessful) { + updateTransactionsSession({ + sessionId, + status: TransactionBatchStatusesEnum.success + }); + return onSuccess?.(sessionId); + } + + const isFailed = serverTransactions.some( + (tx) => tx.status === TransactionServerStatusesEnum.fail + ); + + if (isFailed) { + updateTransactionsSession({ + sessionId, + status: TransactionBatchStatusesEnum.fail + }); + return onFail?.(sessionId); + } + } + } catch (error) { + console.error(error); + } +} diff --git a/src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkTransactionStatus.ts b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkTransactionStatus.ts new file mode 100644 index 0000000..7fe219d --- /dev/null +++ b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/checkTransactionStatus.ts @@ -0,0 +1,27 @@ +import { refreshAccount } from 'utils/account'; +import { checkBatch } from './checkBatch'; +import { getPendingStoreTransactions } from '../getPendingStoreTransactions'; +import { TransactionsTrackerType } from '../../trackTransactions.types'; + +export async function checkTransactionStatus( + props: TransactionsTrackerType & { + shouldRefreshBalance?: boolean; + } +) { + const { pendingSessions } = getPendingStoreTransactions(); + if (Object.keys(pendingSessions).length > 0) { + for (const [sessionId, { transactions }] of Object.entries( + pendingSessions + )) { + await checkBatch({ + sessionId, + transactionBatch: transactions, + ...props + }); + } + } + + if (props.shouldRefreshBalance) { + await refreshAccount(); + } +} diff --git a/src/core/methods/trackTransactions/helpers/checkTransactionStatus/getPendingTransactions.ts b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/getPendingTransactions.ts new file mode 100644 index 0000000..a7f1747 --- /dev/null +++ b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/getPendingTransactions.ts @@ -0,0 +1,30 @@ +import { getIsTransactionPending } from 'store/actions/transactions/transactionStateByStatus'; +import { SignedTransactionType } from 'types/transactions.types'; + +export interface PendingTransactionType { + hash: string; + previousStatus: string; +} + +export function getPendingTransactions( + transactions: SignedTransactionType[], + timedOutHashes: string[] +): PendingTransactionType[] { + const pendingTransactions = transactions.reduce( + (acc: PendingTransactionType[], { status, hash }) => { + if ( + hash != null && + !timedOutHashes.includes(hash) && + getIsTransactionPending(status) + ) { + acc.push({ + hash, + previousStatus: status + }); + } + return acc; + }, + [] + ); + return pendingTransactions; +} diff --git a/src/core/methods/trackTransactions/helpers/checkTransactionStatus/index.ts b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/index.ts new file mode 100644 index 0000000..11b692e --- /dev/null +++ b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/index.ts @@ -0,0 +1 @@ +export * from './checkTransactionStatus'; diff --git a/src/core/methods/trackTransactions/helpers/checkTransactionStatus/manageFailedTransactions.ts b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/manageFailedTransactions.ts new file mode 100644 index 0000000..9340877 --- /dev/null +++ b/src/core/methods/trackTransactions/helpers/checkTransactionStatus/manageFailedTransactions.ts @@ -0,0 +1,39 @@ +import { + updateSignedTransactionStatus, + updateTransactionsSession +} from 'store/actions/transactions/transactionsActions'; +import { + TransactionBatchStatusesEnum, + TransactionServerStatusesEnum +} from 'types/enums.types'; +import { ServerTransactionType } from 'types/serverTransactions.types'; +import { SmartContractResult } from 'types/transactions.types'; + +export function manageFailedTransactions({ + results, + hash, + sessionId +}: { + results: SmartContractResult[]; + hash: string; + sessionId: string; +}) { + const resultWithError = results?.find( + (scResult) => scResult?.returnMessage !== '' + ); + + updateSignedTransactionStatus({ + transactionHash: hash, + sessionId, + status: TransactionServerStatusesEnum.fail, + errorMessage: resultWithError?.returnMessage, + inTransit: false, + serverTransaction: resultWithError as unknown as ServerTransactionType + }); + + updateTransactionsSession({ + sessionId, + status: TransactionBatchStatusesEnum.fail, + errorMessage: resultWithError?.returnMessage + }); +} diff --git a/src/core/methods/trackTransactions/helpers/getPendingStoreTransactions.ts b/src/core/methods/trackTransactions/helpers/getPendingStoreTransactions.ts new file mode 100644 index 0000000..7d870b6 --- /dev/null +++ b/src/core/methods/trackTransactions/helpers/getPendingStoreTransactions.ts @@ -0,0 +1,25 @@ +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/helpers/getPollingInterval.ts b/src/core/methods/trackTransactions/helpers/getPollingInterval.ts new file mode 100644 index 0000000..87ba547 --- /dev/null +++ b/src/core/methods/trackTransactions/helpers/getPollingInterval.ts @@ -0,0 +1,14 @@ +import { TRANSACTIONS_STATUS_POLLING_INTERVAL_MS } from 'constants/transactions.constants'; +import { roundDurationSelectorSelector } from 'store/selectors'; +import { getState } from 'store/store'; + +export const getPollingInterval = () => { + const roundDuration = roundDurationSelectorSelector(getState()); + + if (!roundDuration) { + return TRANSACTIONS_STATUS_POLLING_INTERVAL_MS; + } + + // Polling interval should not be less than 1s + return Math.max(1000, roundDuration / 2); +}; diff --git a/src/core/methods/trackTransactions/trackTransactions.ts b/src/core/methods/trackTransactions/trackTransactions.ts new file mode 100644 index 0000000..895d527 --- /dev/null +++ b/src/core/methods/trackTransactions/trackTransactions.ts @@ -0,0 +1,73 @@ +import { getTransactionsByHashes as defaultGetTxByHash } from 'apiCalls/transactions/getTransactionsByHashes'; +import { TransactionsTrackerType } from './trackTransactions.types'; +import { getPollingInterval } from './helpers/getPollingInterval'; +import { checkTransactionStatus } from './helpers/checkTransactionStatus'; +import { + websocketConnection, + WebsocketConnectionStatusEnum +} from '../initApp/websocket/websocket.constants'; +import { getStore } from 'store/store'; +import { websocketEventSelector } from 'store/selectors/accountSelectors'; + +/** + * Tracks transactions using websocket or polling + * @param props - optional object with additional websocket parameters + * @returns cleanup function + */ +export async function trackTransactions(props?: TransactionsTrackerType) { + const store = getStore(); + const pollingInterval = getPollingInterval(); + 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 + }); + }; + + // recheck on page initial page load + recheckStatus(); + + if (isWebsocketCompleted) { + // Do not set polling interval if websocket is complete + if (pollingIntervalTimer) { + clearInterval(pollingIntervalTimer); + pollingIntervalTimer = null; + } + store.subscribe(async ({ account: { websocketEvent } }) => { + if (websocketEvent?.message && timestamp !== websocketEvent.timestamp) { + timestamp = websocketEvent.timestamp; + recheckStatus(); + } + }); + } else { + // Set polling interval if websocket is not complete and no existing interval is set + if (!pollingIntervalTimer) { + pollingIntervalTimer = setInterval(recheckStatus, pollingInterval); + } + } + + // Return cleanup function for clearing the interval + function cleanup() { + if (pollingIntervalTimer) { + clearInterval(pollingIntervalTimer); + pollingIntervalTimer = null; + } + } + + return { + cleanup + }; +} diff --git a/src/core/methods/trackTransactions/trackTransactions.types.ts b/src/core/methods/trackTransactions/trackTransactions.types.ts new file mode 100644 index 0000000..fb6eba6 --- /dev/null +++ b/src/core/methods/trackTransactions/trackTransactions.types.ts @@ -0,0 +1,7 @@ +import { GetTransactionsByHashesType } from 'types/transactions.types'; + +export interface TransactionsTrackerType { + getTransactionsByHash?: GetTransactionsByHashesType; + onSuccess?: (sessionId: string | null) => void; + onFail?: (sessionId: string | null, errorMessage?: string) => void; +} diff --git a/src/store/actions/account/accountActions.ts b/src/store/actions/account/accountActions.ts index 29f0fc2..279ff4e 100644 --- a/src/store/actions/account/accountActions.ts +++ b/src/store/actions/account/accountActions.ts @@ -11,13 +11,14 @@ export const setAddress = (address: string) => state.address = address; }); -export const setAccount = (account: AccountType) => +export const setAccount = (account: AccountType) => { getStore().setState(({ account: state }) => { const isSameAddress = state.address === account.address; state.accounts = { [state.address]: isSameAddress ? account : emptyAccount }; }); +}; // TODO: check if needed export const setLedgerAccount = (ledgerAccount: LedgerAccountType | null) => diff --git a/src/store/actions/sharedActions/sharedActions.ts b/src/store/actions/sharedActions/sharedActions.ts index 91b89b1..fc7ba06 100644 --- a/src/store/actions/sharedActions/sharedActions.ts +++ b/src/store/actions/sharedActions/sharedActions.ts @@ -12,10 +12,8 @@ export interface LoginActionPayloadType { export const loginAction = ({ address, providerType -}: LoginActionPayloadType) => +}: LoginActionPayloadType) => { getStore().setState(({ account, loginInfo }) => { - console.log('settings address with:', address); - account.address = address; account.publicKey = new Address(address).hex(); @@ -23,3 +21,4 @@ export const loginAction = ({ loginInfo.providerType = providerType; } }); +}; diff --git a/src/store/actions/transactions/transactionStateByStatus.ts b/src/store/actions/transactions/transactionStateByStatus.ts new file mode 100644 index 0000000..4397715 --- /dev/null +++ b/src/store/actions/transactions/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/actions/transactions/transactionsActions.ts b/src/store/actions/transactions/transactionsActions.ts new file mode 100644 index 0000000..5b431f6 --- /dev/null +++ b/src/store/actions/transactions/transactionsActions.ts @@ -0,0 +1,104 @@ +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 UpdateSignedTransactionStatusPayloadType { + sessionId: string; + transactionHash: string; + status: TransactionServerStatusesEnum | TransactionBatchStatusesEnum; + serverTransaction?: ServerTransactionType; + errorMessage?: string; + inTransit?: boolean; +} + +export const createTransactionsSession = ({ + transactions +}: { + transactions: SignedTransactionType[]; +}) => { + const sessionId = Date.now().toString(); + getStore().setState(({ transactions: state }) => { + state[sessionId] = { + transactions, + status: TransactionBatchStatusesEnum.sent + }; + }); + return sessionId; +}; + +export const updateTransactionsSession = ({ + sessionId, + status, + errorMessage +}: { + sessionId: string; + status: TransactionBatchStatusesEnum; + errorMessage?: string; +}) => { + getStore().setState(({ transactions: state }) => { + state[sessionId].status = status; + state[sessionId].errorMessage = errorMessage; + }); +}; + +export const updateSignedTransactionStatus = ( + payload: UpdateSignedTransactionStatusPayloadType +) => { + const { + sessionId, + status, + errorMessage, + transactionHash, + serverTransaction, + inTransit + } = payload; + getStore().setState(({ transactions: 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, // TODO: @CiprianDraghici is this correct?s + 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/selectors/accountSelectors.ts b/src/store/selectors/accountSelectors.ts index a592554..bfddcc7 100644 --- a/src/store/selectors/accountSelectors.ts +++ b/src/store/selectors/accountSelectors.ts @@ -6,6 +6,10 @@ export const accountSelector = ({ export const addressSelector = ({ account: { address } }: StoreType) => address; +export const websocketEventSelector = ({ + account: { websocketEvent } +}: StoreType) => websocketEvent; + export const accountNonceSelector = (store: StoreType) => accountSelector(store)?.nonce || 0; diff --git a/src/store/selectors/networkSelectors.ts b/src/store/selectors/networkSelectors.ts index 1868485..39a9527 100644 --- a/src/store/selectors/networkSelectors.ts +++ b/src/store/selectors/networkSelectors.ts @@ -9,3 +9,7 @@ export const chainIdSelector = ({ network: { network } }: StoreType) => export const walletAddressSelector = ({ network: { network } }: StoreType) => network.walletAddress; + +export const roundDurationSelectorSelector = ({ + network: { network } +}: StoreType) => network.roundDuration; diff --git a/src/store/selectors/transactionsSelector.ts b/src/store/selectors/transactionsSelector.ts new file mode 100644 index 0000000..f9344e2 --- /dev/null +++ b/src/store/selectors/transactionsSelector.ts @@ -0,0 +1,40 @@ +import { TransactionsSliceType } from 'store/slices/transactions/transacitionsSlice.types'; +import { StoreType } from 'store/store.types'; +import { TransactionServerStatusesEnum } from 'types/enums.types'; +import { SignedTransactionType } from 'types/transactions.types'; + +export const transactionsSliceSelector = ({ transactions }: StoreType) => + transactions; + +export const pendingSessionsSelector = ({ + transactions: state +}: StoreType): TransactionsSliceType => { + const pendingSessions: TransactionsSliceType = {}; + + 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 pendingTransactionsSelector = ({ + transactions: 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/account/emptyAccount.ts b/src/store/slices/account/emptyAccount.ts index 08b9bcd..a7a0550 100644 --- a/src/store/slices/account/emptyAccount.ts +++ b/src/store/slices/account/emptyAccount.ts @@ -1,4 +1,4 @@ -import { ELLIPSIS, ZERO } from 'constants/placeholders'; +import { ELLIPSIS, ZERO } from 'constants/placeholders.constants'; import { AccountType } from 'types/account.types'; export const emptyAccount: AccountType = { diff --git a/src/store/slices/index.ts b/src/store/slices/index.ts index 80d5a64..aea5138 100644 --- a/src/store/slices/index.ts +++ b/src/store/slices/index.ts @@ -2,3 +2,4 @@ export * from './account'; export * from './network'; export * from './loginInfo'; export * from './config'; +export * from './transactions'; diff --git a/src/store/slices/network/emptyNetwork.ts b/src/store/slices/network/emptyNetwork.ts index 2250fdf..b1d664b 100644 --- a/src/store/slices/network/emptyNetwork.ts +++ b/src/store/slices/network/emptyNetwork.ts @@ -16,5 +16,6 @@ export const emptyNetwork: CurrentNetworkType = { walletAddress: '', apiAddress: '', explorerAddress: '', - apiTimeout: '4000' + apiTimeout: '4000', + roundDuration: 60000 }; diff --git a/src/store/slices/transactions/index.ts b/src/store/slices/transactions/index.ts new file mode 100644 index 0000000..e9e07c2 --- /dev/null +++ b/src/store/slices/transactions/index.ts @@ -0,0 +1 @@ +export { transactionsSlice } from './transacitionsSlice'; diff --git a/src/store/slices/transactions/transacitionsSlice.ts b/src/store/slices/transactions/transacitionsSlice.ts new file mode 100644 index 0000000..81f4c51 --- /dev/null +++ b/src/store/slices/transactions/transacitionsSlice.ts @@ -0,0 +1,16 @@ +import { StateCreator } from 'zustand/vanilla'; +import { StoreType, MutatorsIn } from 'store/store.types'; +import { TransactionsSliceType } from './transacitionsSlice.types'; + +const initialState: TransactionsSliceType = {}; + +function getTransactionsSlice(): StateCreator< + StoreType, + MutatorsIn, + [], + TransactionsSliceType +> { + return () => initialState; +} + +export const transactionsSlice = getTransactionsSlice(); diff --git a/src/store/slices/transactions/transacitionsSlice.types.ts b/src/store/slices/transactions/transacitionsSlice.types.ts new file mode 100644 index 0000000..282c7fa --- /dev/null +++ b/src/store/slices/transactions/transacitionsSlice.types.ts @@ -0,0 +1,10 @@ +import { TransactionBatchStatusesEnum } from 'types/enums.types'; +import { SignedTransactionType } from 'types/transactions.types'; + +export interface TransactionsSliceType { + [sessionId: string]: { + transactions: SignedTransactionType[]; + status?: TransactionBatchStatusesEnum; + errorMessage?: string; + }; +} diff --git a/src/store/store.ts b/src/store/store.ts index 32d2e3a..7e4a27f 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -6,7 +6,12 @@ import { defaultStorageCallback, StorageCallback } from './storage'; -import { networkSlice, accountSlice, loginInfoSlice } from './slices'; +import { + networkSlice, + accountSlice, + loginInfoSlice, + transactionsSlice +} from './slices'; import { createBoundedUseStore } from './createBoundedStore'; import { StoreType } from './store.types'; import { applyMiddlewares } from './middleware'; @@ -30,6 +35,7 @@ export const createDAppStore = (getStorageCallback: StorageCallback) => { persist( immer((...args) => ({ network: networkSlice(...args), + transactions: transactionsSlice(...args), account: accountSlice(...args), loginInfo: loginInfoSlice(...args), config: configSlice(...args) diff --git a/src/store/store.types.ts b/src/store/store.types.ts index 8145523..3e41282 100644 --- a/src/store/store.types.ts +++ b/src/store/store.types.ts @@ -2,12 +2,14 @@ import { AccountSliceType } from './slices/account/account.types'; import { LoginInfoSliceType } from './slices/loginInfo/loginInfo.types'; import { NetworkSliceType } from './slices/network/networkSlice.types'; import { ConfigSliceType } from './slices/config/config.types'; +import { TransactionsSliceType } from './slices/transactions/transacitionsSlice.types'; export type StoreType = { network: NetworkSliceType; account: AccountSliceType; loginInfo: LoginInfoSliceType; config: ConfigSliceType; + transactions: TransactionsSliceType; }; export type MutatorsIn = [ diff --git a/src/types/enums.types.ts b/src/types/enums.types.ts index 4af22bd..4091e71 100644 --- a/src/types/enums.types.ts +++ b/src/types/enums.types.ts @@ -4,6 +4,26 @@ export enum EnvironmentsEnum { mainnet = 'mainnet' } +export enum TransactionServerStatusesEnum { + pending = 'pending', + fail = 'fail', + invalid = 'invalid', + success = 'success', + executed = 'executed', + notExecuted = 'not executed', + rewardReverted = 'reward-reverted' +} + +export enum TransactionBatchStatusesEnum { + signed = 'signed', + cancelled = 'cancelled', + success = 'success', + sent = 'sent', + fail = 'fail', + timedOut = 'timedOut', + invalid = 'invalid' +} + export enum TypesOfSmartContractCallsEnum { MultiESDTNFTTransfer = 'MultiESDTNFTTransfer', ESDTNFTTransfer = 'ESDTNFTTransfer' diff --git a/src/types/network.types.ts b/src/types/network.types.ts index ee8a732..450fbc2 100644 --- a/src/types/network.types.ts +++ b/src/types/network.types.ts @@ -14,6 +14,8 @@ export interface BaseNetworkType { walletConnectV2ProjectId?: string; walletConnectV2Options?: Record; xAliasAddress?: string; + roundDuration: number; + metamaskSnapWalletAddress?: string; } export interface CurrentNetworkType extends BaseNetworkType { diff --git a/src/types/serverTransactions.types.ts b/src/types/serverTransactions.types.ts new file mode 100644 index 0000000..ae6b3c7 --- /dev/null +++ b/src/types/serverTransactions.types.ts @@ -0,0 +1,347 @@ +import { AssetType, ScamInfoType } from './account.types'; +import { EsdtEnumType, NftEnumType } from './tokens.types'; +import { SignedTransactionType } from './transactions.types'; + +//#region server trasactions + +export interface ScResultType { + callType: string; + gasLimit: number; + gasPrice: number; + nonce: number; + prevTxHash: string; + hash: string; + originalTxHash: string; + receiver: string; + sender: string; + timestamp: number; + value: string; + data?: string; + returnMessage?: string; +} + +export interface TransactionTokensType { + esdts: string[]; + nfts: string[]; +} + +export enum TransactionActionsEnum { + // esdtNft category + transfer = 'transfer', + // legacy delegation + unBond = 'unBond', + unStake = 'unStake', + // stake category + delegate = 'delegate', + stake = 'stake', + unDelegate = 'unDelegate', + stakeClaimRewards = 'claimRewards', + reDelegateRewards = 'reDelegateRewards', + withdraw = 'withdraw', + // mex category + claimLockedAssets = 'claimLockedAssets', + swapTokensFixedInput = 'swapTokensFixedInput', + swapTokensFixedOutput = 'swapTokensFixedOutput', + swap = 'swap', + addLiquidity = 'addLiquidity', + addLiquidityProxy = 'addLiquidityProxy', + removeLiquidity = 'removeLiquidity', + removeLiquidityProxy = 'removeLiquidityProxy', + enterFarm = 'enterFarm', + enterFarmProxy = 'enterFarmProxy', + enterFarmAndLockRewards = 'enterFarmAndLockRewards', + enterFarmAndLockRewardsProxy = 'enterFarmAndLockRewardsProxy', + exitFarm = 'exitFarm', + exitFarmProxy = 'exitFarmProxy', + claimRewards = 'claimRewards', + claimRewardsProxy = 'claimRewardsProxy', + compoundRewards = 'compoundRewards', + compoundRewardsProxy = 'compoundRewardsProxy', + wrapEgld = 'wrapEgld', + unwrapEgld = 'unwrapEgld', + unlockAssets = 'unlockAssets', + mergeLockedAssetTokens = 'mergeLockedAssetTokens', + stakeFarm = 'stakeFarm', + stakeFarmProxy = 'stakeFarmProxy', + stakeFarmTokens = 'stakeFarmTokens', + stakeFarmTokensProxy = 'stakeFarmTokensProxy', + unstakeFarm = 'unstakeFarm', + unstakeFarmProxy = 'unstakeFarmProxy', + unstakeFarmTokens = 'unstakeFarmTokens', + unstakeFarmTokensProxy = 'unstakeFarmTokensProxy', + claimDualYield = 'claimDualYield', + claimDualYieldProxy = 'claimDualYieldProxy', + unbondFarm = 'unbondFarm', + ping = 'ping', + lockTokens = 'lockTokens', + migrateOldTokens = 'migrateOldTokens' +} + +export enum TransactionActionCategoryEnum { + esdtNft = 'esdtNft', + mex = 'mex', + stake = 'stake', + scCall = 'scCall' +} + +export interface TokenArgumentType { + type: NftEnumType | EsdtEnumType; + name: string; + ticker: string; + collection?: string; + identifier?: string; + token?: string; + decimals: number; + value: string; + providerName?: string; + providerAvatar?: string; + svgUrl?: string; + valueUSD?: string; +} + +export interface TransactionActionType { + category: string; + name: TransactionActionsEnum; + description?: string; + arguments?: { [key: string]: any }; +} + +export interface UnwrapperType { + token?: TokenArgumentType[]; + tokenNoValue?: TokenArgumentType[]; + tokenNoLink?: TokenArgumentType[]; + address?: string; + egldValue?: string; + value?: string; + providerName?: string; + providerAvatar?: string; +} + +export enum TransactionOperationActionTypeEnum { + none = 'none', + transfer = 'transfer', + burn = 'burn', + addQuantity = 'addQuantity', + create = 'create', + multiTransfer = 'multiTransfer', + localMint = 'localMint', + localBurn = 'localBurn', + wipe = 'wipe', + freeze = 'freeze', + writeLog = 'writeLog', + signalError = 'signalError', + + // to be deprecated ? + ESDTLocalMint = 'ESDTLocalMint', + ESDTLocalBurn = 'ESDTLocalBurn' +} + +export enum VisibleTransactionOperationType { + nft = 'nft', + esdt = 'esdt', + egld = 'egld' +} +export enum HiddenTransactionOperationType { + none = 'none', + error = 'error', + log = 'log' +} + +export interface OperationType { + id?: string; + action: TransactionOperationActionTypeEnum; + type: VisibleTransactionOperationType | HiddenTransactionOperationType; + esdtType?: NftEnumType | EsdtEnumType; + collection?: string; + name?: string; + identifier?: string; + sender: string; + ticker?: string; + receiver: string; + value: string; + decimals?: number; + data?: string; + message?: string; + svgUrl?: string; + senderAssets?: AssetType; + receiverAssets?: AssetType; + valueUSD?: string; +} + +export interface LogType { + hash: string; + callType: string; + gasLimit: number; + gasPrice: number; + nonce: number; + prevTxHash: string; + receiver?: string; + sender: string; + value: string; + data?: string; + originalTxHash: string; + returnMessage?: string; + logs?: any; +} + +export interface EventType { + address: string; + identifier: string; + topics: string[]; + order: number; + data?: string; + additionalData?: string[]; +} + +export interface ResultLogType { + id: string; + address: string; + events: EventType[]; +} + +export interface ResultType { + hash: string; + callType: string; + gasLimit: number; + gasPrice: number; + nonce: number; + prevTxHash: string; + receiver?: string; + sender: string; + value: string; + data?: string; + originalTxHash: string; + returnMessage?: string; + logs?: ResultLogType; + senderAssets?: AssetType; + receiverAssets?: AssetType; + miniBlockHash?: string; + function?: string; + timestamp?: number; +} + +export interface ReceiptType { + value: string; + sender: string; + data: string; +} + +export interface ServerTransactionType { + fee?: string; + data: string; + gasLimit: number; + gasPrice: number; + gasUsed: number; + txHash: string; + miniBlockHash: string; + nonce: number; + receiver: string; + receiverShard: number; + round: number; + sender: string; + senderShard: number; + signature: string; + status: string; + inTransit?: boolean; + timestamp: number; + value: string; + price: number; + results?: ResultType[]; + operations?: OperationType[]; + action?: TransactionActionType; + logs?: { + id: string; + address: string; + events: EventType[]; + }; + scamInfo?: ScamInfoType; + pendingResults?: boolean; + receipt?: ReceiptType; + senderAssets?: AssetType; + receiverAssets?: AssetType; + type?: TransferTypeEnum; + originalTxHash?: string; + isNew?: boolean; // UI flag + tokenValue?: string; + tokenIdentifier?: string; + function?: string; +} + +export enum TransferTypeEnum { + Transaction = 'Transaction', + SmartContractResult = 'SmartContractResult' +} + +//#endregion + +//#region interpreted trasactions + +export enum TransactionDirectionEnum { + SELF = 'Self', + INTERNAL = 'Internal', + IN = 'In', + OUT = 'Out' +} + +export interface InterpretedTransactionType extends ServerTransactionType { + transactionDetails: { + direction?: TransactionDirectionEnum; + method: string; + transactionTokens: TokenArgumentType[]; + isContract?: boolean; + }; + links: { + senderLink?: string; + receiverLink?: string; + senderShardLink?: string; + receiverShardLink?: string; + transactionLink?: string; + }; +} + +export interface DecodeForDisplayPropsType { + input: string; + decodeMethod: DecodeMethodEnum; + identifier?: string; +} + +export interface DecodedDisplayType { + displayValue: string; + validationWarnings: string[]; +} + +export enum DecodeMethodEnum { + raw = 'raw', + text = 'text', + decimal = 'decimal', + smart = 'smart' +} + +//#endregion + +export enum BatchTransactionStatus { + pending = 'pending', + success = 'success', + invalid = 'invalid', + dropped = 'dropped', + fail = 'fail' +} + +export interface BatchTransactionsRequestType { + id: string; + transactions: SignedTransactionType[][]; +} + +export interface BatchTransactionsResponseType { + id: string; + status: BatchTransactionStatus; + transactions: SignedTransactionType[][]; + error?: string; + message?: string; + statusCode?: string; +} + +export type BatchTransactionsWSResponseType = { + batchId: string; + txHashes: string[]; +}; diff --git a/src/types/tokens.types.ts b/src/types/tokens.types.ts new file mode 100644 index 0000000..b39b9c1 --- /dev/null +++ b/src/types/tokens.types.ts @@ -0,0 +1,136 @@ +import { ScamInfoType } from './account.types'; + +export interface TokenRolesType { + address: string; + roles: string[]; +} + +export interface TokenLockedAccountType { + address: string; + name: string; + balance: string; +} +export interface TokenSupplyType { + supply: number; + circulatingSupply: number; + minted: number; + burnt: number; + initialMinted: number; + lockedAccounts?: TokenLockedAccountType[]; +} + +export interface TokenType { + identifier: string; + ticker?: string; + name: string; + balance?: string; + decimals?: number; + owner: string; + minted: string; + burnt: string; + supply: string | number; + circulatingSupply: string | number; + canBurn: boolean; + canChangeOwner: boolean; + canFreeze: boolean; + canMint: boolean; + canPause: boolean; + canUpgrade: boolean; + canWipe: boolean; + isPaused: boolean; + transactions: number; + accounts: number; + price?: number; + marketCap?: number; + valueUsd?: number; + assets?: { + website?: string; + description?: string; + status?: string; + pngUrl?: string; + svgUrl?: string; + social?: any; + extraTokens?: string[]; + lockedAccounts?: { [key: string]: string }; + ledgerSignature?: string; + }; +} + +export interface CollectionType { + collection: string; + type: NftEnumType; + name: string; + ticker: string; + timestamp: number; + canFreeze: boolean; + canWipe: boolean; + canPause: boolean; + canTransferRole: boolean; + owner: string; + decimals?: number; + assets?: { + website?: string; + description?: string; + status?: string; + pngUrl?: string; + svgUrl?: string; + }; + scamInfo?: ScamInfoType; +} + +export enum EsdtEnumType { + FungibleESDT = 'FungibleESDT' +} + +export enum NftEnumType { + NonFungibleESDT = 'NonFungibleESDT', + SemiFungibleESDT = 'SemiFungibleESDT', + MetaESDT = 'MetaESDT' +} + +export interface NftType { + identifier: string; + collection: string; + ticker?: string; + timestamp: number; + attributes: string; + nonce: number; + type: NftEnumType; + name: string; + creator: string; + royalties: number; + balance: string; + uris?: string[]; + url?: string; + thumbnailUrl?: string; + tags?: string[]; + decimals?: number; + owner?: string; + supply?: string; + isWhitelistedStorage?: boolean; + owners?: { + address: string; + balance: string; + }[]; + assets?: { + website?: string; + description?: string; + status?: string; + pngUrl?: string; + svgUrl?: string; + }; + metadata?: { + description?: string; + fileType?: string; + fileUri?: string; + fileName?: string; + }; + media?: { + url: string; + originalUrl: string; + thumbnailUrl: string; + fileType: string; + fileSize: number; + }[]; + scamInfo?: ScamInfoType; +} diff --git a/src/types/transactions.types.ts b/src/types/transactions.types.ts new file mode 100644 index 0000000..32e9139 --- /dev/null +++ b/src/types/transactions.types.ts @@ -0,0 +1,50 @@ +import { + TransactionBatchStatusesEnum, + TransactionServerStatusesEnum +} from 'types/enums.types'; +import { IPlainTransactionObject } from '@multiversx/sdk-core/out'; + +export interface SignedTransactionType extends IPlainTransactionObject { + hash: string; + status: TransactionServerStatusesEnum; + inTransit?: boolean; +} + +export type PendingTransactionsType = { + hash: string; + previousStatus: string; +}[]; + +export type GetTransactionsByHashesReturnType = { + hash: string; + invalidTransaction: boolean; + status: TransactionServerStatusesEnum | TransactionBatchStatusesEnum; + inTransit?: boolean; + results: SmartContractResult[]; + sender: string; + receiver: string; + data: string; + previousStatus: string; + hasStatusChanged: boolean; +}[]; + +export type GetTransactionsByHashesType = ( + pendingTransactions: PendingTransactionsType +) => Promise; + +export interface SmartContractResult { + hash: string; + timestamp: number; + nonce: number; + gasLimit: number; + gasPrice: number; + value: string; + sender: string; + receiver: string; + data: string; + prevTxHash: string; + originalTxHash: string; + callType: string; + miniBlockHash: string; + returnMessage: string; +} diff --git a/src/utils/account/fetchAccount.ts b/src/utils/account/fetchAccount.ts new file mode 100644 index 0000000..76e8587 --- /dev/null +++ b/src/utils/account/fetchAccount.ts @@ -0,0 +1,3 @@ +import { getAccountFromApi } from 'apiCalls/account/getAccountFromApi'; + +export const fetchAccount = (address?: string) => getAccountFromApi(address); diff --git a/src/utils/account/getAccount.ts b/src/utils/account/getAccount.ts deleted file mode 100644 index b4da6d5..0000000 --- a/src/utils/account/getAccount.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { getAccountFromApi } from 'apiCalls/accounts/getAccountFromApi'; - -export const getAccount = (address?: string) => getAccountFromApi(address); diff --git a/src/utils/account/index.ts b/src/utils/account/index.ts index 7603022..dde3d74 100644 --- a/src/utils/account/index.ts +++ b/src/utils/account/index.ts @@ -1 +1,2 @@ -export * from './getAccount'; +export * from './fetchAccount'; +export * from './refreshAccount'; diff --git a/src/utils/account/refreshAccount.ts b/src/utils/account/refreshAccount.ts new file mode 100644 index 0000000..1134fcd --- /dev/null +++ b/src/utils/account/refreshAccount.ts @@ -0,0 +1,58 @@ +import { getAddress } from 'core/methods/account/getAddress'; +import { fetchAccount } from './fetchAccount'; +import { getLatestNonce } from 'core/methods/account/getLatestNonce'; +import { getAccountProvider } from 'core/providers/accountProvider'; +import { setAccount } from 'store/actions'; + +const setNewAccount = async () => { + try { + const address = getAddress(); + + try { + const account = await fetchAccount(address); + + if (account != null) { + const accountData = { + ...account, + nonce: getLatestNonce(account) + }; + + setAccount(accountData); + + return accountData; + } + } catch (e) { + console.error('Failed getting account ', e); + } + } catch (e) { + console.error('Failed getting address ', e); + } + + return null; +}; + +export async function refreshAccount() { + const provider = getAccountProvider(); + + if (provider == null) { + throw 'Provider not initialized'; + } + + try { + if (!provider.init) { + throw 'Current provider does not have init() function'; + } + + const initialized = await provider.init(); + + if (!initialized) { + return; + } + + return setNewAccount(); + } catch (e) { + console.error('Failed initializing provider ', e); + } + + return undefined; +} diff --git a/yarn.lock b/yarn.lock index 1f51752..928b469 100644 --- a/yarn.lock +++ b/yarn.lock @@ -965,20 +965,6 @@ resolved "https://registry.yarnpkg.com/@multiversx/sdk-bls-wasm/-/sdk-bls-wasm-0.3.5.tgz#2e83308fdc7a0928c6d5a7f910d796fd8eb2d90b" integrity sha512-c0tIdQUnbBLSt6NYU+OpeGPYdL0+GV547HeHT8Xc0BKQ7Cj0v82QUoA2QRtWrR1G4MNZmLsIacZSsf6DrIS2Bw== -"@multiversx/sdk-core@12.18.0": - version "12.18.0" - resolved "https://registry.yarnpkg.com/@multiversx/sdk-core/-/sdk-core-12.18.0.tgz#ae99665f9afb2bd4f1e325cb7daabb1dbcc55ca6" - integrity sha512-F+xGslPMkkZ0S/Q8UJZsMYl0mgHIuK/GdVsNFPiMKxQsKkxA2LTjNdPxVxjwgvRmN7WfdsTtQvmlsA5O1NYhBg== - dependencies: - "@multiversx/sdk-transaction-decoder" "1.0.2" - bech32 "1.1.4" - bignumber.js "9.0.1" - blake2b "2.1.3" - buffer "6.0.3" - json-duplicate-key-handle "1.0.0" - keccak "3.0.2" - protobufjs "7.2.4" - "@multiversx/sdk-core@>= 13.0.0": version "13.4.2" resolved "https://registry.yarnpkg.com/@multiversx/sdk-core/-/sdk-core-13.4.2.tgz#bfe524b9b18b631bef96acead7713d2d1ccab15c" @@ -1015,13 +1001,12 @@ buffer "6.0.3" platform "1.3.6" -"@multiversx/sdk-metamask-provider@0.0.5": - version "0.0.5" - resolved "https://registry.yarnpkg.com/@multiversx/sdk-metamask-provider/-/sdk-metamask-provider-0.0.5.tgz#9f07ec8b9d1d2b2d88e8a9bccc149f01f144be37" - integrity sha512-IicSD/0G6/ZyU5j1Q6YXcXqILZu54ZjvoN1iunffYCDYeBSt1RZrUUdvBAfzjx47ubvX8Ic0iVbfgykjBCLfCQ== +"@multiversx/sdk-metamask-provider@0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@multiversx/sdk-metamask-provider/-/sdk-metamask-provider-0.0.7.tgz#d53e15493a94d44490c47ea8e9a3eafa9b63591b" + integrity sha512-eqA1z/QIflauv5lqetKw2J5E7UooSTcHbZsxwkquWFnO6j1hj35/odS4P8AcbCOVssenZ+THkLOR7kxx5l7e5g== dependencies: "@metamask/providers" "16.0.0" - "@multiversx/sdk-core" "12.18.0" "@multiversx/sdk-native-auth-client@^1.0.8": version "1.0.9" @@ -1329,6 +1314,11 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@socket.io/component-emitter@~3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" + integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== + "@stablelib/aead@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/aead/-/aead-1.0.1.tgz#c4b1106df9c23d1b867eb9b276d8f42d5fc4c0c3" @@ -1661,10 +1651,10 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash@^4.17.4": - version "4.17.7" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612" - integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== +"@types/lodash@4.17.4": + version "4.17.4" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.4.tgz#0303b64958ee070059e3a7184048a55159fe20b7" + integrity sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ== "@types/ms@*": version "0.7.34" @@ -2344,11 +2334,6 @@ babel-preset-jest@^29.6.3: babel-plugin-jest-hoist "^29.6.3" babel-preset-current-node-syntax "^1.0.0" -backslash@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/backslash/-/backslash-0.2.0.tgz#6c3c1fce7e7e714ccfc10fd74f0f73410677375f" - integrity sha512-Avs+8FUZ1HF/VFP4YWwHQZSGzRPm37ukU1JQYQWijuHhtXdOuAzcZ8PcAzfIw898a8PyBzdn+RtnKA6MzW0X2A== - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -2369,11 +2354,6 @@ bech32@^2.0.0: resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== -bignumber.js@9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" - integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== - bignumber.js@9.x, bignumber.js@^9.0.0: version "9.1.2" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" @@ -2960,6 +2940,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@~4.3.1, debug@~4.3.2: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + decimal.js@^10.4.2: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" @@ -3149,6 +3136,22 @@ end-of-stream@^1.4.1: dependencies: once "^1.4.0" +engine.io-client@~6.5.2: + version "6.5.4" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.4.tgz#b8bc71ed3f25d0d51d587729262486b4b33bd0d0" + integrity sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + xmlhttprequest-ssl "~2.0.0" + +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== + enhanced-resolve@^5.12.0: version "5.17.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" @@ -4443,7 +4446,7 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -isomorphic-fetch@^3.0.0: +isomorphic-fetch@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== @@ -4977,13 +4980,6 @@ json-buffer@3.0.1: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== -json-duplicate-key-handle@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-duplicate-key-handle/-/json-duplicate-key-handle-1.0.0.tgz#0678bd17822d23d8c0d0958b43011875fa37f363" - integrity sha512-OLIxL+UpfwUsqcLX3i6Z51ChTou/Vje+6bSeGUSubj96dF/SfjObDprLy++ZXYH07KITuEzsXS7PX7e/BGf4jw== - dependencies: - backslash "^0.2.0" - json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -5121,7 +5117,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.21: +lodash@4.17.21, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5298,7 +5294,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1: +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -5877,24 +5873,6 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -protobufjs@7.2.4: - version "7.2.4" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.4.tgz#3fc1ec0cdc89dd91aef9ba6037ba07408485c3ae" - integrity sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/node" ">=13.7.0" - long "^5.0.0" - protobufjs@^7.3.0: version "7.4.0" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.4.0.tgz#7efe324ce9b3b61c82aae5de810d287bc08a248a" @@ -6341,6 +6319,24 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +socket.io-client@4.7.5: + version "4.7.5" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.5.tgz#919be76916989758bdc20eec63f7ee0ae45c05b7" + integrity sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.5.2" + socket.io-parser "~4.2.4" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + sonic-boom@^2.2.1: version "2.8.0" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-2.8.0.tgz#c1def62a77425090e6ad7516aad8eb402e047611" @@ -6921,10 +6917,10 @@ url@^0.11.0: punycode "^1.4.1" qs "^6.12.3" -use-sync-external-store@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" - integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== +use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" @@ -7134,6 +7130,11 @@ ws@^8.11.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + xml-name-validator@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" @@ -7144,6 +7145,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + xtend@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -7182,9 +7188,9 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zustand@^4.4.7: - version "4.5.5" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.5.tgz#f8c713041543715ec81a2adda0610e1dc82d4ad1" - integrity sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q== +zustand@4.4.7: + version "4.4.7" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.7.tgz#355406be6b11ab335f59a66d2cf9815e8f24038c" + integrity sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw== dependencies: - use-sync-external-store "1.2.2" + use-sync-external-store "1.2.0"