From ce9f2846d0630c79651bfec0738bfc5eee9c9b30 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Sim=C3=A3o?=
Date: Thu, 1 Jun 2023 16:04:59 +0100
Subject: [PATCH] feat: add transaction notifications (#1177)
---
package.json | 2 +-
src/App.tsx | 3 +-
src/assets/icons/CheckCircle.tsx | 25 ++
src/assets/icons/ListBullet.tsx | 25 ++
src/assets/icons/XCircle.tsx | 25 ++
src/assets/icons/index.ts | 3 +
src/assets/locales/en/translation.json | 76 ++++
src/common/actions/general.actions.ts | 20 +-
src/common/reducers/general.reducer.ts | 29 +-
src/common/types/actions.types.ts | 21 +-
src/common/types/util.types.ts | 22 ++
.../NotificationsList.tsx | 35 ++
.../NotificationsListItem.tsx | 42 ++
.../NotificationsPopover.styles.tsx | 18 +
.../NotificationsPopover.tsx | 55 +++
src/components/NotificationsPopover/index.tsx | 2 +
.../ToastContainer/ToastContainer.styles.tsx | 36 ++
.../ToastContainer/ToastContainer.tsx | 5 +
src/components/ToastContainer/index.tsx | 2 +
.../TransactionModal.style.tsx | 21 +
.../TransactionModal/TransactionModal.tsx | 112 ++++++
src/components/TransactionModal/index.tsx | 1 +
.../TransactionToast.styles.tsx | 13 +
.../TransactionToast/TransactionToast.tsx | 132 +++++++
src/components/TransactionToast/index.tsx | 2 +
src/components/index.tsx | 6 +
src/index.tsx | 9 +-
src/legacy-components/ErrorModal/index.tsx | 34 --
.../ConfirmedIssueRequest/index.tsx | 33 --
.../ManualIssueExecutionUI/index.tsx | 16 +-
src/legacy-components/IssueUI/index.tsx | 5 +-
.../RedeemUI/ReimburseStatusUI/index.tsx | 70 ++--
src/legacy-components/RedeemUI/index.tsx | 5 +-
.../index.tsx | 3 +-
.../components/DepositForm/DepositForm.tsx | 24 +-
.../Pools/components/PoolModal/PoolModal.tsx | 9 +-
.../PoolsInsights/PoolsInsights.tsx | 8 +-
.../components/WithdrawForm/WithdrawForm.tsx | 21 +-
.../AMM/Swap/components/SwapForm/SwapCTA.tsx | 8 +-
.../AMM/Swap/components/SwapForm/SwapForm.tsx | 30 +-
src/pages/Bridge/BurnForm/index.tsx | 51 +--
.../SubmittedIssueRequestModal/index.tsx | 42 +-
src/pages/Bridge/IssueForm/index.tsx | 80 ++--
.../SubmittedRedeemRequestModal/index.tsx | 16 +-
src/pages/Bridge/RedeemForm/index.tsx | 18 +-
.../CollateralModal/CollateralModal.tsx | 49 +--
.../components/LoanForm/LoanForm.tsx | 58 ++-
.../LoansInsights/LoansInsights.tsx | 18 +-
.../Staking/ClaimRewardsButton/index.tsx | 29 +-
src/pages/Staking/WithdrawButton/index.tsx | 39 +-
src/pages/Staking/index.tsx | 12 -
.../IssueRequestModal/index.tsx | 23 +-
.../RedeemRequestModal/index.tsx | 23 +-
.../CrossChainTransferForm.tsx | 62 +--
src/pages/Transfer/TransferForm/index.tsx | 53 +--
.../Vaults/Vault/RequestIssueModal/index.tsx | 68 ++--
.../Vaults/Vault/RequestRedeemModal/index.tsx | 26 +-
.../Vault/RequestReplacementModal/index.tsx | 23 +-
.../Vault/UpdateCollateralModal/index.tsx | 23 +-
.../CollateralForm/CollateralForm.styles.tsx | 44 ---
.../CollateralForm/CollateralForm.tsx | 312 ---------------
.../Vault/components/CollateralForm/index.tsx | 2 -
.../Vault/components/Rewards/Rewards.tsx | 11 -
src/pages/Vaults/Vault/components/index.tsx | 4 +-
.../DespositCollateralStep.tsx | 12 +-
.../AvailableAssetsTable/ActionsCell.tsx | 22 +-
src/parts/Topbar/index.tsx | 6 +-
src/utils/constants/links.ts | 21 +-
src/utils/context/Notifications.tsx | 141 +++++++
.../transaction/extrinsics/extrinsics.ts | 46 +++
.../hooks/transaction/extrinsics/index.ts | 1 +
.../{utils/extrinsic.ts => extrinsics/lib.ts} | 44 +--
src/utils/hooks/transaction/extrinsics/xcm.ts | 27 ++
src/utils/hooks/transaction/types/index.ts | 29 +-
src/utils/hooks/transaction/types/vesting.ts | 13 +
src/utils/hooks/transaction/types/xcm.ts | 21 +
.../use-transaction-notifications.tsx | 107 ++++++
.../hooks/transaction/use-transaction.ts | 95 +++--
.../hooks/transaction/utils/description.ts | 363 ++++++++++++++++++
src/utils/hooks/transaction/utils/submit.ts | 46 ++-
src/utils/hooks/use-copy-tooltip.tsx | 1 +
src/utils/hooks/use-countdown.ts | 69 ++++
src/utils/hooks/use-sign-message.ts | 1 +
src/utils/hooks/use-window-focus.ts | 26 ++
yarn.lock | 12 +-
85 files changed, 2000 insertions(+), 1197 deletions(-)
create mode 100644 src/assets/icons/CheckCircle.tsx
create mode 100644 src/assets/icons/ListBullet.tsx
create mode 100644 src/assets/icons/XCircle.tsx
create mode 100644 src/components/NotificationsPopover/NotificationsList.tsx
create mode 100644 src/components/NotificationsPopover/NotificationsListItem.tsx
create mode 100644 src/components/NotificationsPopover/NotificationsPopover.styles.tsx
create mode 100644 src/components/NotificationsPopover/NotificationsPopover.tsx
create mode 100644 src/components/NotificationsPopover/index.tsx
create mode 100644 src/components/ToastContainer/ToastContainer.styles.tsx
create mode 100644 src/components/ToastContainer/ToastContainer.tsx
create mode 100644 src/components/ToastContainer/index.tsx
create mode 100644 src/components/TransactionModal/TransactionModal.style.tsx
create mode 100644 src/components/TransactionModal/TransactionModal.tsx
create mode 100644 src/components/TransactionModal/index.tsx
create mode 100644 src/components/TransactionToast/TransactionToast.styles.tsx
create mode 100644 src/components/TransactionToast/TransactionToast.tsx
create mode 100644 src/components/TransactionToast/index.tsx
delete mode 100644 src/legacy-components/ErrorModal/index.tsx
delete mode 100644 src/pages/Vaults/Vault/components/CollateralForm/CollateralForm.styles.tsx
delete mode 100644 src/pages/Vaults/Vault/components/CollateralForm/CollateralForm.tsx
delete mode 100644 src/pages/Vaults/Vault/components/CollateralForm/index.tsx
create mode 100644 src/utils/context/Notifications.tsx
create mode 100644 src/utils/hooks/transaction/extrinsics/extrinsics.ts
create mode 100644 src/utils/hooks/transaction/extrinsics/index.ts
rename src/utils/hooks/transaction/{utils/extrinsic.ts => extrinsics/lib.ts} (70%)
create mode 100644 src/utils/hooks/transaction/extrinsics/xcm.ts
create mode 100644 src/utils/hooks/transaction/types/vesting.ts
create mode 100644 src/utils/hooks/transaction/types/xcm.ts
create mode 100644 src/utils/hooks/transaction/use-transaction-notifications.tsx
create mode 100644 src/utils/hooks/transaction/utils/description.ts
create mode 100644 src/utils/hooks/use-countdown.ts
create mode 100644 src/utils/hooks/use-window-focus.ts
diff --git a/package.json b/package.json
index 581d654255..5e804dcbdd 100644
--- a/package.json
+++ b/package.json
@@ -69,7 +69,7 @@
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-table": "^7.6.3",
- "react-toastify": "^6.0.5",
+ "react-toastify": "^9.1.2",
"react-transition-group": "^4.4.5",
"react-use": "^17.2.3",
"redux": "^4.0.5",
diff --git a/src/App.tsx b/src/App.tsx
index a94a8a4eb2..1722f65d75 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,4 +1,3 @@
-import 'react-toastify/dist/ReactToastify.css';
import './i18n';
import { FaucetClient, SecurityStatusCode } from '@interlay/interbtc-api';
@@ -21,6 +20,7 @@ import vaultsByAccountIdQuery from '@/services/queries/vaults-by-accountId-query
import { BitcoinNetwork } from '@/types/bitcoin';
import { PAGES } from '@/utils/constants/links';
+import { TransactionModal } from './components/TransactionModal';
import * as constants from './constants';
import TestnetBanner from './legacy-components/TestnetBanner';
import { FeatureFlags, useFeatureFlag } from './utils/hooks/use-feature-flag';
@@ -234,6 +234,7 @@ const App = (): JSX.Element => {
)}
/>
+
>
);
};
diff --git a/src/assets/icons/CheckCircle.tsx b/src/assets/icons/CheckCircle.tsx
new file mode 100644
index 0000000000..2bcca13aed
--- /dev/null
+++ b/src/assets/icons/CheckCircle.tsx
@@ -0,0 +1,25 @@
+import { forwardRef } from 'react';
+
+import { Icon, IconProps } from '@/component-library/Icon';
+
+const CheckCircle = forwardRef((props, ref) => (
+
+
+
+));
+
+CheckCircle.displayName = 'CheckCircle';
+
+export { CheckCircle };
diff --git a/src/assets/icons/ListBullet.tsx b/src/assets/icons/ListBullet.tsx
new file mode 100644
index 0000000000..21eb5ba490
--- /dev/null
+++ b/src/assets/icons/ListBullet.tsx
@@ -0,0 +1,25 @@
+import { forwardRef } from 'react';
+
+import { Icon, IconProps } from '@/component-library/Icon';
+
+const ListBullet = forwardRef((props, ref) => (
+
+
+
+));
+
+ListBullet.displayName = 'ListBullet';
+
+export { ListBullet };
diff --git a/src/assets/icons/XCircle.tsx b/src/assets/icons/XCircle.tsx
new file mode 100644
index 0000000000..c1b84d58cd
--- /dev/null
+++ b/src/assets/icons/XCircle.tsx
@@ -0,0 +1,25 @@
+import { forwardRef } from 'react';
+
+import { Icon, IconProps } from '@/component-library/Icon';
+
+const XCircle = forwardRef((props, ref) => (
+
+
+
+));
+
+XCircle.displayName = 'XCircle';
+
+export { XCircle };
diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts
index bf537097cc..2508fb9299 100644
--- a/src/assets/icons/index.ts
+++ b/src/assets/icons/index.ts
@@ -2,11 +2,14 @@ export { ArrowRight } from './ArrowRight';
export { ArrowRightCircle } from './ArrowRightCircle';
export { ArrowsUpDown } from './ArrowsUpDown';
export { ArrowTopRightOnSquare } from './ArrowTopRightOnSquare';
+export { CheckCircle } from './CheckCircle';
export { ChevronDown } from './ChevronDown';
export { Cog } from './Cog';
export { DocumentDuplicate } from './DocumentDuplicate';
export { InformationCircle } from './InformationCircle';
+export { ListBullet } from './ListBullet';
export { PencilSquare } from './PencilSquare';
export { PlusCircle } from './PlusCircle';
export { Warning } from './Warning';
+export { XCircle } from './XCircle';
export { XMark } from './XMark';
diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json
index 9e03c16050..e6cd47edf2 100644
--- a/src/assets/locales/en/translation.json
+++ b/src/assets/locales/en/translation.json
@@ -156,6 +156,7 @@
"staked": "Staked",
"sign_t&cs": "Sign T&Cs",
"receivable_assets": "Receivable Assets",
+ "dismiss": "Dismiss",
"redeem_page": {
"maximum_in_single_request": "Max redeemable in single request",
"redeem": "Redeem",
@@ -636,5 +637,80 @@
"strategy": {
"withdraw_rewards_in_wrapped": "Withdraw rewards in {{wrappedCurrencySymbol}}:",
"update_position": "Update position"
+ },
+ "transaction": {
+ "recent_transactions": "Recent transactions",
+ "no_recent_transactions": "No recent transactions",
+ "confirm_transaction_wallet": "Confirm this transaction in your wallet",
+ "confirm_transaction": "Confirm transaction",
+ "transaction_processing": "Transaction processing",
+ "transaction_failed": "Transaction failed",
+ "transaction_successful": "Transaction successful",
+ "swapping_to": "Swapping {{fromAmount}} {{fromCurrency}} to {{toAmount}} {{toCurrency}}",
+ "swapped_to": "Swapped {{fromAmount}} {{fromCurrency}} to {{toAmount}} {{toCurrency}}",
+ "adding_liquidity_to_pool": "Adding liquidity to {{poolName}} Pool",
+ "added_liquidity_to_pool": "Added liquidity to {{poolName}} Pool",
+ "removing_liquidity_from_pool": "Removing liquidity from {{poolName}} Pool",
+ "removed_liquidity_from_pool": "Removed liquidity from {{poolName}} Pool",
+ "claiming_pool_rewards": "Claiming pools rewards",
+ "claimed_pool_rewards": "Claimed pools rewards",
+ "issuing_amount": "Issuing {{amount}} {{currency}}",
+ "issued_amount": "Issuing {{amount}} {{currency}}",
+ "redeeming_amount": "Redeeming {{amount}} {{currency}}",
+ "redeemed_amount": "Redeemed {{amount}} {{currency}}",
+ "burning_amount": "Burning {{amount}} {{currency}}",
+ "burned_amount": "Burned {{amount}} {{currency}}",
+ "retrying_redeem_id": "Retrying redeem {{resquestId}}",
+ "retried_redeem_id": "Retried redeem {{resquestId}}",
+ "reimbursing_redeem_id": "Reimbursing redeem {{resquestId}}",
+ "reimbersed_redeem_id": "Reimbursed redeem {{resquestId}}",
+ "executing_issue": "Executing issue",
+ "executed_issue": "Executed issue",
+ "transfering_amount_to_address": "Transfering {{amount}} {{currency}} to {{address}}",
+ "transfered_amount_to_address": "Transfered {{amount}} {{currency}} to {{address}}",
+ "transfering_amount_from_chain_to_chain": "Transfering {{amount}} {{currency}} from {{fromChain}} to {{toChain}}",
+ "transfered_amount_from_chain_to_chain": "Transfered {{amount}} {{currency}} from {{fromChain}} to {{toChain}}",
+ "claiming_lending_rewards": "Claiming lending rewards",
+ "claimed_lending_rewards": "Claimed lending rewards",
+ "borrowing_amount": "Borrowing {{amount}} {{currency}}",
+ "borrowed_amount": "Borrowed {{amount}} {{currency}}",
+ "lending_amount": "Lending {{amount}} {{currency}}",
+ "lent_amount": "Lent {{amount}} {{currency}}",
+ "repaying_amount": "Repaying {{amount}} {{currency}}",
+ "repaid_amount": "Repaid {{amount}} {{currency}}",
+ "repaying": "Repaying {{currency}}",
+ "repaid": "Repaid {{currency}}",
+ "withdrawing_amount": "Withdrawing {{amount}} {{currency}}",
+ "withdrew_amount": "Withdrew {{amount}} {{currency}}",
+ "withdrawing": "Withdrawing {{currency}}",
+ "withdrew": "Withdrew {{currency}}",
+ "disabling_loan_as_collateral": "Disabling {{currency}} as collateral",
+ "disabled_loan_as_collateral": "Disabled {{currency}} as collateral",
+ "enabling_loan_as_collateral": "Enabling {{currency}} as collateral",
+ "enabled_loan_as_collateral": "Enabled {{currency}} as collateral",
+ "creating_currency_vault": "Creating {{currency}} vault",
+ "created_currency_vault": "Created {{currency}} vault",
+ "depositing_amount_to_vault": "Depositing {{amount}} {{currency}} to vault",
+ "deposited_amount_to_vault": "Deposited {{amount}} {{currency}} to vault",
+ "withdrawing_amount_from_vault": "Withdrawing {{amount}} {{currency}} from vault",
+ "withdrew_amount_from_vault": "Withdrew {{amount}} {{currency}} from vault",
+ "claiming_vault_rewards": "Claiming vault rewards",
+ "claimed_vault_rewards": "Claimed vault rewards",
+ "staking_amount": "Staking {{amount}} {{currency}}",
+ "staked_amount": "Staking {{amount}} {{currency}}",
+ "adding_amount_to_staked_amount": "Adding {{amount}} {{currency}} to staked amount",
+ "added_amount_to_staked_amount": "Added {{amount}} {{currency}} to staked amount",
+ "increasing_stake_lock_time": "Increasing stake lock time",
+ "increased_stake_lock_time": "Increased stake lock time",
+ "withdrawing_stake": "Withdrawing stake",
+ "withdrew_stake": "Withdrew stake",
+ "claiming_staking_rewards": "Claiming staking rewards",
+ "claimed_staking_rewards": "Claimed staking rewards",
+ "increasing_stake_locked_time_amount": "Increasing stake locked time and amount",
+ "increased_stake_locked_time_amount": "Increased stake locked time and amount",
+ "requesting_vault_replacement": "Requesting vault replacement",
+ "requested_vault_replacement": "Requested vault replacement",
+ "claiming_vesting": "Claiming vesting",
+ "claimed_vesting": "Claimed vesting"
}
}
diff --git a/src/common/actions/general.actions.ts b/src/common/actions/general.actions.ts
index 8bfaa85c76..880e1da1ce 100644
--- a/src/common/actions/general.actions.ts
+++ b/src/common/actions/general.actions.ts
@@ -4,6 +4,8 @@ import { BitcoinAmount, MonetaryAmount } from '@interlay/monetary-js';
import { GovernanceTokenMonetaryAmount } from '@/config/relay-chains';
import {
+ ADD_NOTIFICATION,
+ AddNotification,
INIT_GENERAL_DATA_ACTION,
InitGeneralDataAction,
IS_BRIDGE_LOADED,
@@ -20,10 +22,12 @@ import {
ShowSignTermsModal,
UPDATE_HEIGHTS,
UPDATE_TOTALS,
+ UPDATE_TRANSACTION_MODAL_STATUS,
UpdateHeights,
- UpdateTotals
+ UpdateTotals,
+ UpdateTransactionModal
} from '../types/actions.types';
-import { ParachainStatus } from '../types/util.types';
+import { Notification, ParachainStatus, TransactionModalData } from '../types/util.types';
export const isBridgeLoaded = (isLoaded = false): IsBridgeLoaded => ({
type: IS_BRIDGE_LOADED,
@@ -86,3 +90,15 @@ export const updateTotalsAction = (
totalLockedCollateralTokenAmount,
totalWrappedTokenAmount
});
+
+export const addNotification = (accountAddress: string, notification: Notification): AddNotification => ({
+ type: ADD_NOTIFICATION,
+ accountAddress,
+ notification
+});
+
+export const updateTransactionModal = (isOpen: boolean, data: TransactionModalData): UpdateTransactionModal => ({
+ type: UPDATE_TRANSACTION_MODAL_STATUS,
+ isOpen,
+ data
+});
diff --git a/src/common/reducers/general.reducer.ts b/src/common/reducers/general.reducer.ts
index cc89bc33e7..23093c810a 100644
--- a/src/common/reducers/general.reducer.ts
+++ b/src/common/reducers/general.reducer.ts
@@ -2,8 +2,10 @@ import { newMonetaryAmount } from '@interlay/interbtc-api';
import { BitcoinAmount } from '@interlay/monetary-js';
import { RELAY_CHAIN_NATIVE_TOKEN } from '@/config/relay-chains';
+import { TransactionStatus } from '@/utils/hooks/transaction/types';
import {
+ ADD_NOTIFICATION,
GeneralActions,
INIT_GENERAL_DATA_ACTION,
IS_BRIDGE_LOADED,
@@ -12,7 +14,8 @@ import {
SHOW_BUY_MODAL,
SHOW_SIGN_TERMS_MODAL,
UPDATE_HEIGHTS,
- UPDATE_TOTALS
+ UPDATE_TOTALS,
+ UPDATE_TRANSACTION_MODAL_STATUS
} from '../types/actions.types';
import { GeneralState, ParachainStatus } from '../types/util.types';
@@ -33,6 +36,11 @@ const initialState = {
relayChainNativeToken: { usd: 0 },
governanceToken: { usd: 0 },
wrappedToken: { usd: 0 }
+ },
+ notifications: {},
+ transactionModal: {
+ isOpen: false,
+ data: { variant: TransactionStatus.CONFIRM }
}
};
@@ -65,6 +73,25 @@ export const generalReducer = (state: GeneralState = initialState, action: Gener
return { ...state, isBuyModalOpen: action.isBuyModalOpen };
case SHOW_SIGN_TERMS_MODAL:
return { ...state, isSignTermsModalOpen: action.isSignTermsModalOpen };
+ case ADD_NOTIFICATION: {
+ const newAccountNotifications = [...(state.notifications[action.accountAddress] || []), action.notification];
+
+ return {
+ ...state,
+ notifications: {
+ ...state.notifications,
+ [action.accountAddress]: newAccountNotifications
+ }
+ };
+ }
+ case UPDATE_TRANSACTION_MODAL_STATUS:
+ return {
+ ...state,
+ transactionModal: {
+ ...state.transactionModal,
+ ...action
+ }
+ };
default:
return state;
}
diff --git a/src/common/types/actions.types.ts b/src/common/types/actions.types.ts
index 4ebf3cb5df..f4744b03ac 100644
--- a/src/common/types/actions.types.ts
+++ b/src/common/types/actions.types.ts
@@ -3,7 +3,7 @@ import { BitcoinAmount, MonetaryAmount } from '@interlay/monetary-js';
import { GovernanceTokenMonetaryAmount } from '@/config/relay-chains';
-import { ParachainStatus, StoreType } from './util.types';
+import { Notification, ParachainStatus, StoreType, TransactionModalData } from './util.types';
// GENERAL ACTIONS
export const IS_BRIDGE_LOADED = 'IS_BRIDGE_LOADED';
@@ -20,6 +20,9 @@ export const SHOW_SIGN_TERMS_MODAL = 'SHOW_SIGN_TERMS_MODAL';
export const UPDATE_HEIGHTS = 'UPDATE_HEIGHTS';
export const UPDATE_TOTALS = 'UPDATE_TOTALS';
export const SHOW_BUY_MODAL = 'SHOW_BUY_MODAL';
+export const ADD_NOTIFICATION = 'ADD_NOTIFICATION';
+export const SHOW_TRANSACTION_MODAL = 'SHOW_TRANSACTION_MODAL';
+export const UPDATE_TRANSACTION_MODAL_STATUS = 'UPDATE_TRANSACTION_MODAL_STATUS';
export interface UpdateTotals {
type: typeof UPDATE_TOTALS;
@@ -98,6 +101,18 @@ export interface ShowBuyModal {
isBuyModalOpen: boolean;
}
+export interface AddNotification {
+ type: typeof ADD_NOTIFICATION;
+ accountAddress: string;
+ notification: Notification;
+}
+
+export interface UpdateTransactionModal {
+ type: typeof UPDATE_TRANSACTION_MODAL_STATUS;
+ isOpen: boolean;
+ data: TransactionModalData;
+}
+
export type GeneralActions =
| IsBridgeLoaded
| InitGeneralDataAction
@@ -110,7 +125,9 @@ export type GeneralActions =
| UpdateHeights
| UpdateTotals
| ShowBuyModal
- | ShowSignTermsModal;
+ | ShowSignTermsModal
+ | AddNotification
+ | UpdateTransactionModal;
// REDEEM
export const ADD_VAULT_REDEEMS = 'ADD_VAULT_REDEEMS';
diff --git a/src/common/types/util.types.ts b/src/common/types/util.types.ts
index b49a70f30b..922531dad0 100644
--- a/src/common/types/util.types.ts
+++ b/src/common/types/util.types.ts
@@ -3,6 +3,8 @@ import { BitcoinAmount, MonetaryAmount } from '@interlay/monetary-js';
import { u256 } from '@polkadot/types/primitive';
import { CombinedState, Store } from 'redux';
+import { TransactionStatus } from '@/utils/hooks/transaction/types';
+
import { rootReducer } from '../reducers/index';
import { GeneralActions, RedeemActions, VaultActions } from './actions.types';
import { RedeemState } from './redeem.types';
@@ -45,6 +47,21 @@ export enum ParachainStatus {
Shutdown
}
+export type Notification = {
+ status: TransactionStatus;
+ description: string;
+ date: Date;
+ url?: string;
+};
+
+export type TransactionModalData = {
+ variant: TransactionStatus;
+ timestamp?: number;
+ description?: string;
+ url?: string;
+ errorMessage?: string;
+};
+
export type GeneralState = {
bridgeLoaded: boolean;
vaultClientLoaded: boolean;
@@ -56,6 +73,11 @@ export type GeneralState = {
btcRelayHeight: number;
bitcoinHeight: number;
parachainStatus: ParachainStatus;
+ notifications: Record;
+ transactionModal: {
+ isOpen: boolean;
+ data: TransactionModalData;
+ };
};
export type AppState = ReturnType;
diff --git a/src/components/NotificationsPopover/NotificationsList.tsx b/src/components/NotificationsPopover/NotificationsList.tsx
new file mode 100644
index 0000000000..97d51a2c9a
--- /dev/null
+++ b/src/components/NotificationsPopover/NotificationsList.tsx
@@ -0,0 +1,35 @@
+import { useTranslation } from 'react-i18next';
+
+import { Notification } from '@/common/types/util.types';
+import { Flex, P } from '@/component-library';
+
+import { NotificationListItem } from './NotificationsListItem';
+
+type NotificationsListProps = {
+ items: Notification[];
+};
+
+const NotificationsList = ({ items }: NotificationsListProps): JSX.Element => {
+ const { t } = useTranslation();
+
+ if (!items.length) {
+ return (
+
+ {t('transaction.no_recent_transactions')}
+
+ );
+ }
+
+ const latestTransactions = items.slice(-5);
+
+ return (
+
+ {latestTransactions.map((item, index) => (
+
+ ))}
+
+ );
+};
+
+export { NotificationsList };
+export type { NotificationsListProps };
diff --git a/src/components/NotificationsPopover/NotificationsListItem.tsx b/src/components/NotificationsPopover/NotificationsListItem.tsx
new file mode 100644
index 0000000000..7c29cdfce8
--- /dev/null
+++ b/src/components/NotificationsPopover/NotificationsListItem.tsx
@@ -0,0 +1,42 @@
+import { useButton } from '@react-aria/button';
+import { formatDistanceToNowStrict } from 'date-fns';
+import { useRef } from 'react';
+
+import { CheckCircle, XCircle } from '@/assets/icons';
+import { Notification } from '@/common/types/util.types';
+import { Flex, P } from '@/component-library';
+import { TransactionStatus } from '@/utils/hooks/transaction/types';
+
+import { StyledListItem } from './NotificationsPopover.styles';
+
+type NotificationListItemProps = Notification;
+
+const NotificationListItem = ({ date, description, status, url }: NotificationListItemProps): JSX.Element => {
+ const ref = useRef(null);
+
+ const ariaLabel = url ? 'navigate to transaction subscan page' : undefined;
+
+ const handlePress = () => window.open(url, '_blank', 'noopener');
+
+ const { buttonProps } = useButton(
+ { 'aria-label': ariaLabel, isDisabled: !url, elementType: 'div', onPress: handlePress },
+ ref
+ );
+
+ return (
+
+
+
+ {status === TransactionStatus.SUCCESS ? : }
+ {description}
+
+
+ {formatDistanceToNowStrict(date)} ago
+
+
+
+ );
+};
+
+export { NotificationListItem };
+export type { NotificationListItemProps };
diff --git a/src/components/NotificationsPopover/NotificationsPopover.styles.tsx b/src/components/NotificationsPopover/NotificationsPopover.styles.tsx
new file mode 100644
index 0000000000..828c137438
--- /dev/null
+++ b/src/components/NotificationsPopover/NotificationsPopover.styles.tsx
@@ -0,0 +1,18 @@
+import styled from 'styled-components';
+
+import { CTA, theme } from '@/component-library';
+
+const StyledListItem = styled.div`
+ padding: ${theme.spacing.spacing3} ${theme.spacing.spacing2};
+
+ &:not(:last-of-type) {
+ border-bottom: ${theme.border.default};
+ }
+`;
+
+const StyledCTA = styled(CTA)`
+ padding: ${theme.spacing.spacing3};
+ border: ${theme.border.default};
+`;
+
+export { StyledCTA, StyledListItem };
diff --git a/src/components/NotificationsPopover/NotificationsPopover.tsx b/src/components/NotificationsPopover/NotificationsPopover.tsx
new file mode 100644
index 0000000000..334298a192
--- /dev/null
+++ b/src/components/NotificationsPopover/NotificationsPopover.tsx
@@ -0,0 +1,55 @@
+import { useTranslation } from 'react-i18next';
+
+import { ListBullet } from '@/assets/icons';
+import { Notification } from '@/common/types/util.types';
+import {
+ Popover,
+ PopoverBody,
+ PopoverContent,
+ PopoverFooter,
+ PopoverHeader,
+ PopoverTrigger,
+ TextLink
+} from '@/component-library';
+import { EXTERNAL_PAGES, EXTERNAL_URL_PARAMETERS } from '@/utils/constants/links';
+
+import { NotificationsList } from './NotificationsList';
+import { StyledCTA } from './NotificationsPopover.styles';
+
+type NotificationsPopoverProps = {
+ address?: string;
+ items: Notification[];
+};
+
+const NotificationsPopover = ({ address, items }: NotificationsPopoverProps): JSX.Element => {
+ const { t } = useTranslation();
+
+ const accountTransactionsUrl =
+ address && EXTERNAL_PAGES.SUBSCAN.ACCOUNT.replace(`:${EXTERNAL_URL_PARAMETERS.SUBSCAN.ACCOUNT.ADDRESS}`, address);
+
+ return (
+
+
+
+
+
+
+
+ {t('transaction.recent_transactions')}
+
+
+
+ {accountTransactionsUrl && (
+
+
+ View all transactions
+
+
+ )}
+
+
+ );
+};
+
+export { NotificationsPopover };
+export type { NotificationsPopoverProps };
diff --git a/src/components/NotificationsPopover/index.tsx b/src/components/NotificationsPopover/index.tsx
new file mode 100644
index 0000000000..9d68f4a5e0
--- /dev/null
+++ b/src/components/NotificationsPopover/index.tsx
@@ -0,0 +1,2 @@
+export type { NotificationsPopoverProps } from './NotificationsPopover';
+export { NotificationsPopover } from './NotificationsPopover';
diff --git a/src/components/ToastContainer/ToastContainer.styles.tsx b/src/components/ToastContainer/ToastContainer.styles.tsx
new file mode 100644
index 0000000000..0de455dab2
--- /dev/null
+++ b/src/components/ToastContainer/ToastContainer.styles.tsx
@@ -0,0 +1,36 @@
+import 'react-toastify/dist/ReactToastify.css';
+
+import { ToastContainer } from 'react-toastify';
+import styled from 'styled-components';
+
+import { theme } from '@/component-library';
+
+// &&& is used to override css styles
+const StyledToastContainer = styled(ToastContainer)`
+ &&&.Toastify__toast-container {
+ color: ${theme.colors.textPrimary};
+ padding: 0 ${theme.spacing.spacing4};
+ }
+
+ @media ${theme.breakpoints.up('sm')} {
+ &&&.Toastify__toast-container {
+ padding: 0;
+ }
+ }
+
+ .Toastify__toast {
+ margin-bottom: 1rem;
+ padding: 0;
+ border-radius: 12px;
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
+ font-family: inherit;
+ background: ${theme.colors.bgPrimary};
+ border: ${theme.border.default};
+ }
+
+ .Toastify__toast-body {
+ padding: 0;
+ }
+`;
+
+export { StyledToastContainer };
diff --git a/src/components/ToastContainer/ToastContainer.tsx b/src/components/ToastContainer/ToastContainer.tsx
new file mode 100644
index 0000000000..8119b67efd
--- /dev/null
+++ b/src/components/ToastContainer/ToastContainer.tsx
@@ -0,0 +1,5 @@
+import { ToastContainerProps } from 'react-toastify';
+
+import { StyledToastContainer } from './ToastContainer.styles';
+export { StyledToastContainer as ToastContainer };
+export type { ToastContainerProps };
diff --git a/src/components/ToastContainer/index.tsx b/src/components/ToastContainer/index.tsx
new file mode 100644
index 0000000000..31f30105c2
--- /dev/null
+++ b/src/components/ToastContainer/index.tsx
@@ -0,0 +1,2 @@
+export type { ToastContainerProps } from './ToastContainer';
+export { ToastContainer } from './ToastContainer';
diff --git a/src/components/TransactionModal/TransactionModal.style.tsx b/src/components/TransactionModal/TransactionModal.style.tsx
new file mode 100644
index 0000000000..7819fa032b
--- /dev/null
+++ b/src/components/TransactionModal/TransactionModal.style.tsx
@@ -0,0 +1,21 @@
+import styled from 'styled-components';
+
+import { CheckCircle, XCircle } from '@/assets/icons';
+import { Card, theme } from '@/component-library';
+
+const StyledXCircle = styled(XCircle)`
+ width: 4rem;
+ height: 4rem;
+`;
+
+const StyledCheckCircle = styled(CheckCircle)`
+ width: 4rem;
+ height: 4rem;
+`;
+
+const StyledCard = styled(Card)`
+ border-radius: ${theme.rounded.rg};
+ padding: ${theme.spacing.spacing4};
+`;
+
+export { StyledCard, StyledCheckCircle, StyledXCircle };
diff --git a/src/components/TransactionModal/TransactionModal.tsx b/src/components/TransactionModal/TransactionModal.tsx
new file mode 100644
index 0000000000..6ab35ada22
--- /dev/null
+++ b/src/components/TransactionModal/TransactionModal.tsx
@@ -0,0 +1,112 @@
+import { TFunction } from 'i18next';
+import { useTranslation } from 'react-i18next';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { updateTransactionModal } from '@/common/actions/general.actions';
+import { StoreType } from '@/common/types/util.types';
+import {
+ CTA,
+ Flex,
+ H4,
+ H5,
+ LoadingSpinner,
+ Modal,
+ ModalBody,
+ ModalFooter,
+ ModalHeader,
+ P,
+ TextLink
+} from '@/component-library';
+import { NotificationToast, useNotifications } from '@/utils/context/Notifications';
+import { TransactionStatus } from '@/utils/hooks/transaction/types';
+
+import { StyledCard, StyledCheckCircle, StyledXCircle } from './TransactionModal.style';
+
+const loadingSpinner = ;
+
+const getData = (t: TFunction, variant: TransactionStatus) =>
+ ({
+ [TransactionStatus.CONFIRM]: {
+ title: t('transaction.confirm_transaction'),
+ subtitle: t('transaction.confirm_transaction_wallet'),
+ icon: loadingSpinner
+ },
+ [TransactionStatus.SUBMITTING]: {
+ title: t('transaction.transaction_processing'),
+ icon: loadingSpinner
+ },
+ [TransactionStatus.ERROR]: {
+ title: t('transaction.transaction_failed'),
+ icon:
+ },
+ [TransactionStatus.SUCCESS]: {
+ title: t('transaction.transaction_successful'),
+ icon:
+ }
+ }[variant]);
+
+const TransactionModal = (): JSX.Element => {
+ const { t } = useTranslation();
+
+ const notifications = useNotifications();
+
+ const { isOpen, data } = useSelector((state: StoreType) => state.general.transactionModal);
+ const { variant, description, url, timestamp, errorMessage } = data;
+ const dispatch = useDispatch();
+
+ const { title, subtitle, icon } = getData(t, variant);
+
+ const hasDismiss = variant !== TransactionStatus.CONFIRM;
+
+ const handleClose = () => {
+ // Only show toast if the current transaction variant is CONFIRM or SUBMITTING.
+ // No need to show toast if the transaction is SUCCESS or ERROR
+ if (timestamp && (variant === TransactionStatus.CONFIRM || variant === TransactionStatus.SUBMITTING)) {
+ notifications.show(timestamp, {
+ type: NotificationToast.TRANSACTION,
+ props: { variant: variant, url, description }
+ });
+ }
+
+ dispatch(updateTransactionModal(false, data));
+ };
+
+ return (
+
+ {title}
+
+ {icon}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+ {description && (
+
+ {description}
+
+ )}
+ {errorMessage && (
+
+
+ Message:
+
+
+ {errorMessage}
+
+
+ )}
+ {url && (
+
+ View transaction on Subscan
+
+ )}
+
+
+ {hasDismiss && {t('dismiss')}}
+
+ );
+};
+
+export { TransactionModal };
diff --git a/src/components/TransactionModal/index.tsx b/src/components/TransactionModal/index.tsx
new file mode 100644
index 0000000000..db2576f068
--- /dev/null
+++ b/src/components/TransactionModal/index.tsx
@@ -0,0 +1 @@
+export { TransactionModal } from './TransactionModal';
diff --git a/src/components/TransactionToast/TransactionToast.styles.tsx b/src/components/TransactionToast/TransactionToast.styles.tsx
new file mode 100644
index 0000000000..11a85da6e7
--- /dev/null
+++ b/src/components/TransactionToast/TransactionToast.styles.tsx
@@ -0,0 +1,13 @@
+import styled from 'styled-components';
+
+import { Flex, ProgressBar, theme } from '@/component-library';
+
+const StyledWrapper = styled(Flex)`
+ padding: ${theme.spacing.spacing4};
+`;
+
+const StyledProgressBar = styled(ProgressBar)`
+ margin-top: ${theme.spacing.spacing4};
+`;
+
+export { StyledProgressBar, StyledWrapper };
diff --git a/src/components/TransactionToast/TransactionToast.tsx b/src/components/TransactionToast/TransactionToast.tsx
new file mode 100644
index 0000000000..fc413baba1
--- /dev/null
+++ b/src/components/TransactionToast/TransactionToast.tsx
@@ -0,0 +1,132 @@
+import { useHover } from '@react-aria/interactions';
+import { mergeProps } from '@react-aria/utils';
+import { TFunction } from 'i18next';
+import { useTranslation } from 'react-i18next';
+import { useDispatch } from 'react-redux';
+
+import { CheckCircle, XCircle } from '@/assets/icons';
+import { updateTransactionModal } from '@/common/actions/general.actions';
+import { CTA, CTALink, Divider, Flex, FlexProps, LoadingSpinner, P } from '@/component-library';
+import { TransactionStatus } from '@/utils/hooks/transaction/types';
+import { useCountdown } from '@/utils/hooks/use-countdown';
+
+import { StyledProgressBar, StyledWrapper } from './TransactionToast.styles';
+
+const loadingSpinner = ;
+
+const getData = (t: TFunction, variant: TransactionStatus) =>
+ ({
+ [TransactionStatus.CONFIRM]: {
+ title: t('transaction.confirm_transaction'),
+ icon: loadingSpinner
+ },
+ [TransactionStatus.SUBMITTING]: {
+ title: t('transaction.transaction_processing'),
+ icon: loadingSpinner
+ },
+ [TransactionStatus.SUCCESS]: {
+ title: t('transaction.transaction_successful'),
+ icon:
+ },
+ [TransactionStatus.ERROR]: {
+ title: t('transaction.transaction_failed'),
+ icon:
+ }
+ }[variant]);
+
+type Props = {
+ variant?: TransactionStatus;
+ description?: string;
+ url?: string;
+ errorMessage?: string;
+ timeout?: number;
+ onDismiss?: () => void;
+};
+
+type InheritAttrs = Omit;
+
+type TransactionToastProps = Props & InheritAttrs;
+
+const TransactionToast = ({
+ variant = TransactionStatus.SUCCESS,
+ timeout = 8000,
+ url,
+ description,
+ onDismiss,
+ errorMessage,
+ ...props
+}: TransactionToastProps): JSX.Element => {
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+
+ const showCountdown = variant === TransactionStatus.SUCCESS || variant === TransactionStatus.ERROR;
+
+ const { value: countdown, start, stop } = useCountdown({
+ timeout,
+ disabled: !showCountdown,
+ onEndCountdown: onDismiss
+ });
+
+ const { hoverProps } = useHover({
+ onHoverStart: stop,
+ onHoverEnd: start,
+ isDisabled: !showCountdown
+ });
+
+ const handleViewDetails = () => {
+ dispatch(updateTransactionModal(true, { variant: TransactionStatus.ERROR, description, errorMessage }));
+ onDismiss?.();
+ };
+
+ const { title, icon } = getData(t, variant);
+
+ return (
+
+
+
+ {icon}
+
+
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+ {showCountdown && (
+
+ )}
+
+ {(url || errorMessage) && (
+ <>
+ {url && (
+
+ View Subscan
+
+ )}
+ {errorMessage && !url && (
+
+ View Details
+
+ )}
+
+ >
+ )}
+
+ Dismiss
+
+
+
+ );
+};
+
+export { TransactionToast };
+export type { TransactionToastProps };
diff --git a/src/components/TransactionToast/index.tsx b/src/components/TransactionToast/index.tsx
new file mode 100644
index 0000000000..36ce2db462
--- /dev/null
+++ b/src/components/TransactionToast/index.tsx
@@ -0,0 +1,2 @@
+export type { TransactionToastProps } from './TransactionToast';
+export { TransactionToast } from './TransactionToast';
diff --git a/src/components/index.tsx b/src/components/index.tsx
index 83fc0ca6aa..bb20578fa9 100644
--- a/src/components/index.tsx
+++ b/src/components/index.tsx
@@ -10,6 +10,12 @@ export type { IsAuthenticatedProps } from './IsAuthenticated';
export { IsAuthenticated } from './IsAuthenticated';
export type { LoanPositionsTableProps } from './LoanPositionsTable';
export { LoanPositionsTable } from './LoanPositionsTable';
+export type { NotificationsPopoverProps } from './NotificationsPopover';
+export { NotificationsPopover } from './NotificationsPopover';
export type { PoolsTableProps } from './PoolsTable';
export { PoolsTable } from './PoolsTable';
export { ReceivableAssets } from './ReceivableAssets';
+export type { ToastContainerProps } from './ToastContainer';
+export { ToastContainer } from './ToastContainer';
+export type { TransactionToastProps } from './TransactionToast';
+export { TransactionToast } from './TransactionToast';
diff --git a/src/index.tsx b/src/index.tsx
index 4901f7d9a1..327b7658e6 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -21,6 +21,7 @@ import App from './App';
import { GeoblockingWrapper } from './components/Geoblock/Geoblock';
import reportWebVitals from './reportWebVitals';
import { store } from './store';
+import { NotificationsProvider } from './utils/context/Notifications';
configGlobalBig();
@@ -40,9 +41,11 @@ ReactDOM.render(
-
-
-
+
+
+
+
+
diff --git a/src/legacy-components/ErrorModal/index.tsx b/src/legacy-components/ErrorModal/index.tsx
deleted file mode 100644
index 8dc60f3f6e..0000000000
--- a/src/legacy-components/ErrorModal/index.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import clsx from 'clsx';
-import * as React from 'react';
-
-import CloseIconButton from '@/legacy-components/buttons/CloseIconButton';
-import InterlayModal, {
- InterlayModalInnerWrapper,
- InterlayModalTitle,
- Props as ModalProps
-} from '@/legacy-components/UI/InterlayModal';
-
-interface CustomProps {
- title: string;
- description: string;
-}
-
-const ErrorModal = ({ open, onClose, title, description }: Props): JSX.Element => {
- const focusRef = React.useRef(null);
-
- return (
-
-
-
- {title}
-
-
- {description}
-
-
- );
-};
-
-export type Props = Omit & CustomProps;
-
-export default ErrorModal;
diff --git a/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx b/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx
index e76d52fe2d..3472455342 100644
--- a/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx
+++ b/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx
@@ -1,22 +1,14 @@
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
import { FaCheckCircle } from 'react-icons/fa';
-import { useQueryClient } from 'react-query';
-import { toast } from 'react-toastify';
import { BTC_EXPLORER_TRANSACTION_API } from '@/config/blockstream-explorer-links';
import { WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains';
import AddressWithCopyUI from '@/legacy-components/AddressWithCopyUI';
-import ErrorModal from '@/legacy-components/ErrorModal';
import ExternalLink from '@/legacy-components/ExternalLink';
import RequestWrapper from '@/pages/Bridge/RequestWrapper';
-import { ISSUES_FETCHER } from '@/services/fetchers/issues-fetcher';
-import { TABLE_PAGE_LIMIT } from '@/utils/constants/general';
-import { QUERY_PARAMETERS } from '@/utils/constants/links';
import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names';
import { getColorShade } from '@/utils/helpers/colors';
-import { Transaction, useTransaction } from '@/utils/hooks/transaction';
-import useQueryParams from '@/utils/hooks/use-query-params';
import ManualIssueExecutionUI from '../ManualIssueExecutionUI';
@@ -28,21 +20,6 @@ interface Props {
const ConfirmedIssueRequest = ({ request }: Props): JSX.Element => {
const { t } = useTranslation();
- const queryParams = useQueryParams();
- const selectedPage = Number(queryParams.get(QUERY_PARAMETERS.PAGE)) || 1;
- const selectedPageIndex = selectedPage - 1;
-
- const queryClient = useQueryClient();
-
- // TODO: check if this transaction is necessary
- const transaction = useTransaction(Transaction.ISSUE_EXECUTE, {
- onSuccess: (_, variables) => {
- const [requestId] = variables.args;
- queryClient.invalidateQueries([ISSUES_FETCHER, selectedPageIndex * TABLE_PAGE_LIMIT, TABLE_PAGE_LIMIT]);
- toast.success(t('issue_page.successfully_executed', { id: requestId }));
- }
- });
-
return (
<>
@@ -75,16 +52,6 @@ const ConfirmedIssueRequest = ({ request }: Props): JSX.Element => {
- {transaction.isError && transaction.error && (
- {
- transaction.reset();
- }}
- title='Error'
- description={typeof transaction.error === 'string' ? transaction.error : transaction.error.message}
- />
- )}
>
);
};
diff --git a/src/legacy-components/IssueUI/IssueRequestStatusUI/ManualIssueExecutionUI/index.tsx b/src/legacy-components/IssueUI/IssueRequestStatusUI/ManualIssueExecutionUI/index.tsx
index c93aa9aae2..a111faff63 100644
--- a/src/legacy-components/IssueUI/IssueRequestStatusUI/ManualIssueExecutionUI/index.tsx
+++ b/src/legacy-components/IssueUI/IssueRequestStatusUI/ManualIssueExecutionUI/index.tsx
@@ -8,12 +8,10 @@ import {
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
import { useQuery, useQueryClient } from 'react-query';
-import { toast } from 'react-toastify';
import { displayMonetaryAmount } from '@/common/utils/utils';
import { WRAPPED_TOKEN, WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains';
import InterlayDenimOrKintsugiMidnightOutlinedButton from '@/legacy-components/buttons/InterlayDenimOrKintsugiMidnightOutlinedButton';
-import ErrorModal from '@/legacy-components/ErrorModal';
import { useSubstrateSecureState } from '@/lib/substrate';
import { ISSUES_FETCHER } from '@/services/fetchers/issues-fetcher';
import { TABLE_PAGE_LIMIT } from '@/utils/constants/general';
@@ -57,10 +55,8 @@ const ManualIssueExecutionUI = ({ request }: Props): JSX.Element => {
const queryClient = useQueryClient();
const transaction = useTransaction(Transaction.ISSUE_EXECUTE, {
- onSuccess: (_, variables) => {
- const [requestId] = variables.args;
+ onSuccess: () => {
queryClient.invalidateQueries([ISSUES_FETCHER, selectedPageIndex * TABLE_PAGE_LIMIT, TABLE_PAGE_LIMIT]);
- toast.success(t('issue_page.successfully_executed', { id: requestId }));
}
});
@@ -139,16 +135,6 @@ const ManualIssueExecutionUI = ({ request }: Props): JSX.Element => {
wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL
})}
- {transaction.isError && transaction.error && (
- {
- transaction.reset();
- }}
- title='Error'
- description={typeof transaction.error === 'string' ? transaction.error : transaction.error.message}
- />
- )}
);
};
diff --git a/src/legacy-components/IssueUI/index.tsx b/src/legacy-components/IssueUI/index.tsx
index 2158916386..4edd9b6878 100644
--- a/src/legacy-components/IssueUI/index.tsx
+++ b/src/legacy-components/IssueUI/index.tsx
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { ReactComponent as BitcoinLogoIcon } from '@/assets/img/bitcoin-logo.svg';
import { displayMonetaryAmountInUSDFormat, formatNumber } from '@/common/utils/utils';
+import { Flex } from '@/component-library';
import { WRAPPED_TOKEN_SYMBOL, WrappedTokenAmount } from '@/config/relay-chains';
import AddressWithCopyUI from '@/legacy-components/AddressWithCopyUI';
import Hr2 from '@/legacy-components/hrs/Hr2';
@@ -52,7 +53,7 @@ const IssueUI = ({ issue }: Props): JSX.Element => {
const sentBackingTokenAmount = receivedWrappedTokenAmount.add(bridgeFee);
return (
-
+
{/* TODO: could componentize */}
@@ -184,7 +185,7 @@ const IssueUI = ({ issue }: Props): JSX.Element => {
<>{renderModalStatusPanel(issue)}>
-
+
);
};
diff --git a/src/legacy-components/RedeemUI/ReimburseStatusUI/index.tsx b/src/legacy-components/RedeemUI/ReimburseStatusUI/index.tsx
index 2e2cc3b19e..541f4ef875 100644
--- a/src/legacy-components/RedeemUI/ReimburseStatusUI/index.tsx
+++ b/src/legacy-components/RedeemUI/ReimburseStatusUI/index.tsx
@@ -1,14 +1,12 @@
import { newMonetaryAmount } from '@interlay/interbtc-api';
-import { ISubmittableResult } from '@polkadot/types/types';
import Big from 'big.js';
import clsx from 'clsx';
import * as React from 'react';
import { useErrorHandler, withErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import { FaExclamationCircle } from 'react-icons/fa';
-import { useMutation, useQueryClient } from 'react-query';
+import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
-import { toast } from 'react-toastify';
import { StoreType } from '@/common/types/util.types';
import { displayMonetaryAmount, displayMonetaryAmountInUSDFormat } from '@/common/utils/utils';
@@ -22,10 +20,10 @@ import RequestWrapper from '@/pages/Bridge/RequestWrapper';
import { REDEEMS_FETCHER } from '@/services/fetchers/redeems-fetcher';
import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names';
import { getColorShade } from '@/utils/helpers/colors';
-import { submitExtrinsic } from '@/utils/helpers/extrinsic';
import { getExchangeRate } from '@/utils/helpers/oracle';
import { getTokenPrice } from '@/utils/helpers/prices';
import { useGetPrices } from '@/utils/hooks/api/use-get-prices';
+import { Transaction, useTransaction } from '@/utils/hooks/transaction';
interface Props {
redeem: any; // TODO: should type properly (`Relay`)
@@ -45,6 +43,20 @@ const ReimburseStatusUI = ({ redeem, onClose }: Props): JSX.Element => {
);
const { t } = useTranslation();
const handleError = useErrorHandler();
+ const queryClient = useQueryClient();
+
+ const [cancelType, setCancelType] = React.useState<'reimburse' | 'retry'>();
+
+ const transaction = useTransaction(Transaction.REDEEM_CANCEL, {
+ onSuccess: () => {
+ queryClient.invalidateQueries([REDEEMS_FETCHER]);
+ setCancelType(undefined);
+ onClose();
+ },
+ onError: () => {
+ setCancelType(undefined);
+ }
+ });
React.useEffect(() => {
if (!bridgeLoaded) return;
@@ -67,48 +79,13 @@ const ReimburseStatusUI = ({ redeem, onClose }: Props): JSX.Element => {
})();
}, [redeem, bridgeLoaded, handleError]);
- const queryClient = useQueryClient();
- // TODO: should type properly (`Relay`)
- const retryMutation = useMutation
(
- (variables: any) => {
- return submitExtrinsic(window.bridge.redeem.cancel(variables.id, false));
- },
- {
- onSuccess: () => {
- queryClient.invalidateQueries([REDEEMS_FETCHER]);
- toast.success(t('redeem_page.successfully_cancelled_redeem'));
- onClose();
- },
- onError: (error) => {
- console.log('[useMutation] error => ', error);
- toast.error(t('redeem_page.error_cancelling_redeem'));
- }
- }
- );
- // TODO: should type properly (`Relay`)
- const reimburseMutation = useMutation(
- (variables: any) => {
- return submitExtrinsic(window.bridge.redeem.cancel(variables.id, true));
- },
- {
- onSuccess: () => {
- queryClient.invalidateQueries([REDEEMS_FETCHER]);
- toast.success(t('redeem_page.successfully_cancelled_redeem'));
- onClose();
- },
- onError: (error) => {
- console.log('[useMutation] error => ', error);
- toast.error(t('redeem_page.error_cancelling_redeem'));
- }
- }
- );
-
const handleRetry = () => {
if (!bridgeLoaded) {
throw new Error('Bridge is not loaded!');
}
- retryMutation.mutate(redeem);
+ setCancelType('retry');
+ transaction.execute(redeem.id, false);
};
const handleReimburse = () => {
@@ -116,7 +93,8 @@ const ReimburseStatusUI = ({ redeem, onClose }: Props): JSX.Element => {
throw new Error('Bridge is not loaded!');
}
- reimburseMutation.mutate(redeem);
+ setCancelType('reimburse');
+ transaction.execute(redeem.id, true);
};
const isOwner = selectedAccount?.address === redeem.userParachainAddress;
@@ -198,8 +176,8 @@ const ReimburseStatusUI = ({ redeem, onClose }: Props): JSX.Element => {
{t('retry')}
@@ -239,8 +217,8 @@ const ReimburseStatusUI = ({ redeem, onClose }: Props): JSX.Element => {
{t('redeem_page.reimburse')}
diff --git a/src/legacy-components/RedeemUI/index.tsx b/src/legacy-components/RedeemUI/index.tsx
index 34820878c5..229f1f3619 100644
--- a/src/legacy-components/RedeemUI/index.tsx
+++ b/src/legacy-components/RedeemUI/index.tsx
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { ReactComponent as BitcoinLogoIcon } from '@/assets/img/bitcoin-logo.svg';
import { displayMonetaryAmountInUSDFormat, formatNumber } from '@/common/utils/utils';
+import { Flex } from '@/component-library';
import { WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains';
import AddressWithCopyUI from '@/legacy-components/AddressWithCopyUI';
import Hr2 from '@/legacy-components/hrs/Hr2';
@@ -43,7 +44,7 @@ const RedeemUI = ({ redeem, onClose }: Props): JSX.Element => {
};
return (
-
+
@@ -160,7 +161,7 @@ const RedeemUI = ({ redeem, onClose }: Props): JSX.Element => {
<>{renderModalStatusPanel(redeem)}>
-
+
);
};
diff --git a/src/lib/substrate/components/SubstrateLoadingAndErrorHandlingWrapper/index.tsx b/src/lib/substrate/components/SubstrateLoadingAndErrorHandlingWrapper/index.tsx
index 1c378b87cf..0e6a048b99 100644
--- a/src/lib/substrate/components/SubstrateLoadingAndErrorHandlingWrapper/index.tsx
+++ b/src/lib/substrate/components/SubstrateLoadingAndErrorHandlingWrapper/index.tsx
@@ -1,5 +1,5 @@
import { useDispatch } from 'react-redux';
-import { toast, ToastContainer } from 'react-toastify';
+import { toast } from 'react-toastify';
import { isBridgeLoaded } from '@/common/actions/general.actions';
import FullLoadingSpinner from '@/legacy-components/FullLoadingSpinner';
@@ -66,7 +66,6 @@ const SubstrateLoadingAndErrorHandlingWrapper = ({
return (
<>
-
{children}
>
);
diff --git a/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx b/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx
index 4baf947a1b..6b7387aeee 100644
--- a/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx
+++ b/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx
@@ -3,7 +3,6 @@ import { mergeProps } from '@react-aria/utils';
import Big from 'big.js';
import { ChangeEventHandler, RefObject, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'react-toastify';
import { displayMonetaryAmountInUSDFormat, newSafeMonetaryAmount } from '@/common/utils/utils';
import { Alert, Dd, DlGroup, Dt, Flex, TokenInput } from '@/component-library';
@@ -35,10 +34,11 @@ const isCustomAmountsMode = (form: ReturnType) =>
type DepositFormProps = {
pool: LiquidityPool;
slippageModalRef: RefObject;
- onDeposit?: () => void;
+ onSuccess?: () => void;
+ onSigning?: () => void;
};
-const DepositForm = ({ pool, slippageModalRef, onDeposit }: DepositFormProps): JSX.Element => {
+const DepositForm = ({ pool, slippageModalRef, onSuccess, onSigning }: DepositFormProps): JSX.Element => {
const { pooledCurrencies } = pool;
const defaultValues = pooledCurrencies.reduce((acc, amount) => ({ ...acc, [amount.currency.ticker]: '' }), {});
@@ -52,13 +52,8 @@ const DepositForm = ({ pool, slippageModalRef, onDeposit }: DepositFormProps): J
const governanceBalance = getBalance(GOVERNANCE_TOKEN.ticker)?.free || newMonetaryAmount(0, GOVERNANCE_TOKEN);
const transaction = useTransaction(Transaction.AMM_ADD_LIQUIDITY, {
- onSuccess: () => {
- onDeposit?.();
- toast.success('Deposit successful');
- },
- onError: (error) => {
- toast.error(error.message);
- }
+ onSuccess,
+ onSigning
});
const handleSubmit = async (data: DepositLiquidityPoolFormData) => {
@@ -72,8 +67,8 @@ const DepositForm = ({ pool, slippageModalRef, onDeposit }: DepositFormProps): J
const deadline = await window.bridge.system.getFutureBlockNumber(AMM_DEADLINE_INTERVAL);
return transaction.execute(amounts, pool, slippage, deadline, accountId);
- } catch (err: any) {
- toast.error(err.toString());
+ } catch (error: any) {
+ transaction.reject(error);
}
};
@@ -91,8 +86,7 @@ const DepositForm = ({ pool, slippageModalRef, onDeposit }: DepositFormProps): J
const form = useForm({
initialValues: defaultValues,
validationSchema: depositLiquidityPoolSchema({ transactionFee: TRANSACTION_FEE_AMOUNT, governanceBalance, tokens }),
- onSubmit: handleSubmit,
- disableValidation: transaction.isLoading
+ onSubmit: handleSubmit
});
const handleChange: ChangeEventHandler = (e) => {
@@ -189,7 +183,7 @@ const DepositForm = ({ pool, slippageModalRef, onDeposit }: DepositFormProps): J
-
+
{t('amm.pools.add_liquidity')}
diff --git a/src/pages/AMM/Pools/components/PoolModal/PoolModal.tsx b/src/pages/AMM/Pools/components/PoolModal/PoolModal.tsx
index 2e1086a25b..b768873cff 100644
--- a/src/pages/AMM/Pools/components/PoolModal/PoolModal.tsx
+++ b/src/pages/AMM/Pools/components/PoolModal/PoolModal.tsx
@@ -26,11 +26,6 @@ const PoolModal = ({ pool, onClose, ...props }: PoolModalProps): JSX.Element | n
return null;
}
- const handleAction = () => {
- refetch();
- onClose?.();
- };
-
return (
-
+
-
+
diff --git a/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx b/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx
index 1689c20a32..961db4e3c0 100644
--- a/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx
+++ b/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx
@@ -1,7 +1,6 @@
import { LiquidityPool } from '@interlay/interbtc-api';
import Big from 'big.js';
import { useTranslation } from 'react-i18next';
-import { toast } from 'react-toastify';
import { formatUSD } from '@/common/utils/utils';
import { Card, Dl, DlGroup } from '@/component-library';
@@ -49,13 +48,8 @@ const PoolsInsights = ({ pools, accountPoolsData, refetch }: PoolsInsightsProps)
const totalClaimableRewardUSD = calculateClaimableFarmingRewardUSD(accountPoolsData?.claimableRewards, prices);
- const handleSuccess = () => {
- toast.success(t('successfully_claimed_rewards'));
- refetch();
- };
-
const transaction = useTransaction(Transaction.AMM_CLAIM_REWARDS, {
- onSuccess: handleSuccess
+ onSuccess: refetch
});
const handleClickClaimRewards = () => accountPoolsData && transaction.execute(accountPoolsData.claimableRewards);
diff --git a/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx b/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx
index 2d74a356af..10d7f0fa85 100644
--- a/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx
+++ b/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx
@@ -2,7 +2,6 @@ import { LiquidityPool, newMonetaryAmount } from '@interlay/interbtc-api';
import Big from 'big.js';
import { RefObject, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { toast } from 'react-toastify';
import {
convertMonetaryAmountToValueInUSD,
@@ -28,10 +27,11 @@ import { StyledDl } from './WithdrawForm.styles';
type WithdrawFormProps = {
pool: LiquidityPool;
slippageModalRef: RefObject;
- onWithdraw?: () => void;
+ onSuccess?: () => void;
+ onSigning?: () => void;
};
-const WithdrawForm = ({ pool, slippageModalRef, onWithdraw }: WithdrawFormProps): JSX.Element => {
+const WithdrawForm = ({ pool, slippageModalRef, onSuccess, onSigning }: WithdrawFormProps): JSX.Element => {
const [slippage, setSlippage] = useState(0.1);
const accountId = useAccountId();
@@ -40,13 +40,8 @@ const WithdrawForm = ({ pool, slippageModalRef, onWithdraw }: WithdrawFormProps)
const { getBalance } = useGetBalances();
const transaction = useTransaction(Transaction.AMM_REMOVE_LIQUIDITY, {
- onSuccess: () => {
- onWithdraw?.();
- toast.success('Withdraw successful');
- },
- onError: (err) => {
- toast.error(err.message);
- }
+ onSuccess,
+ onSigning
});
const { lpToken } = pool;
@@ -70,8 +65,8 @@ const WithdrawForm = ({ pool, slippageModalRef, onWithdraw }: WithdrawFormProps)
const deadline = await window.bridge.system.getFutureBlockNumber(AMM_DEADLINE_INTERVAL);
return transaction.execute(amount, pool, slippage, deadline, accountId);
- } catch (err: any) {
- toast.error(err.toString());
+ } catch (error: any) {
+ transaction.reject(error);
}
};
@@ -141,7 +136,7 @@ const WithdrawForm = ({ pool, slippageModalRef, onWithdraw }: WithdrawFormProps)
-
+
{t('amm.pools.remove_liquidity')}
diff --git a/src/pages/AMM/Swap/components/SwapForm/SwapCTA.tsx b/src/pages/AMM/Swap/components/SwapForm/SwapCTA.tsx
index 3ef5503393..a817c120ff 100644
--- a/src/pages/AMM/Swap/components/SwapForm/SwapCTA.tsx
+++ b/src/pages/AMM/Swap/components/SwapForm/SwapCTA.tsx
@@ -45,8 +45,7 @@ const getProps = (
}
return {
- children: t('amm.swap'),
- disabled: false
+ children: t('amm.swap')
};
};
@@ -54,15 +53,14 @@ type SwapCTAProps = {
pair: SwapPair;
trade: Trade | null | undefined;
errors: FormErrors;
- loading: boolean;
};
-const SwapCTA = ({ pair, trade, errors, loading }: SwapCTAProps): JSX.Element | null => {
+const SwapCTA = ({ pair, trade, errors }: SwapCTAProps): JSX.Element | null => {
const { t } = useTranslation();
const otherProps = getProps(pair, trade, errors, t);
- return ;
+ return ;
};
export { SwapCTA };
diff --git a/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx b/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx
index a4d60bbcf6..eeccf0d580 100644
--- a/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx
+++ b/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx
@@ -4,7 +4,6 @@ import Big from 'big.js';
import { ChangeEventHandler, Key, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
-import { toast } from 'react-toastify';
import { useDebounce } from 'react-use';
import { StoreType } from '@/common/types/util.types';
@@ -113,15 +112,12 @@ const SwapForm = ({
const { data: currencies } = useGetCurrencies(bridgeLoaded);
const transaction = useTransaction(Transaction.AMM_SWAP, {
- onSuccess: () => {
- toast.success('Swap successful');
- setTrade(undefined);
+ onSigning: () => {
setInputAmount(undefined);
- onSwap();
+ form.setFieldValue(SWAP_INPUT_AMOUNT_FIELD, '', true);
+ setTrade(undefined);
},
- onError: (err) => {
- toast.error(err.message);
- }
+ onSuccess: onSwap
});
useDebounce(
@@ -157,12 +153,11 @@ const SwapForm = ({
try {
const minimumAmountOut = trade.getMinimumOutputAmount(slippage);
-
const deadline = await window.bridge.system.getFutureBlockNumber(30 * 60);
return transaction.execute(trade, minimumAmountOut, accountId, deadline);
- } catch (err: any) {
- toast.error(err.toString());
+ } catch (error: any) {
+ transaction.reject(error);
}
};
@@ -193,7 +188,6 @@ const SwapForm = ({
initialValues,
validationSchema: swapSchema({ [SWAP_INPUT_AMOUNT_FIELD]: inputSchemaParams }),
onSubmit: handleSubmit,
- disableValidation: transaction.isLoading,
validateOnMount: true
});
@@ -216,16 +210,6 @@ const SwapForm = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pair]);
- // MEMO: amount field cleaned up after successful swap
- useEffect(() => {
- const isAmountFieldEmpty = form.values[SWAP_INPUT_AMOUNT_FIELD] === '';
-
- if (isAmountFieldEmpty || !transaction.isSuccess) return;
-
- form.setFieldValue(SWAP_INPUT_AMOUNT_FIELD, '');
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [transaction.isSuccess]);
-
const handleChangeInput: ChangeEventHandler = (e) => {
setInputAmount(e.target.value);
setTrade(undefined);
@@ -322,7 +306,7 @@ const SwapForm = ({
/>
{trade && }
-
+
diff --git a/src/pages/Bridge/BurnForm/index.tsx b/src/pages/Bridge/BurnForm/index.tsx
index 2063218a57..4699f19aa7 100644
--- a/src/pages/Bridge/BurnForm/index.tsx
+++ b/src/pages/Bridge/BurnForm/index.tsx
@@ -6,7 +6,6 @@ import { useErrorHandler, withErrorBoundary } from 'react-error-boundary';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
-import { toast } from 'react-toastify';
import { ParachainStatus, StoreType } from '@/common/types/util.types';
import { displayMonetaryAmountInUSDFormat } from '@/common/utils/utils';
@@ -15,7 +14,6 @@ import { AuthCTA } from '@/components';
import { WRAPPED_TOKEN, WRAPPED_TOKEN_SYMBOL, WrappedTokenLogoIcon } from '@/config/relay-chains';
import { BALANCE_MAX_INTEGER_LENGTH } from '@/constants';
import ErrorFallback from '@/legacy-components/ErrorFallback';
-import ErrorModal from '@/legacy-components/ErrorModal';
import FormTitle from '@/legacy-components/FormTitle';
import Hr2 from '@/legacy-components/hrs/Hr2';
import PriceInfo from '@/legacy-components/PriceInfo';
@@ -70,10 +68,12 @@ const BurnForm = (): JSX.Element | null => {
const [burnableCollateral, setBurnableCollateral] = React.useState();
const [selectedCollateral, setSelectedCollateral] = React.useState();
- const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE);
- const [submitError, setSubmitError] = React.useState(null);
-
- const transaction = useTransaction(Transaction.REDEEM_BURN);
+ const transaction = useTransaction(Transaction.REDEEM_BURN, {
+ onSuccess: () =>
+ reset({
+ [WRAPPED_TOKEN_AMOUNT]: ''
+ })
+ });
const handleUpdateCollateral = (collateral: TokenOption) => {
const selectedCollateral = burnableCollateral?.find(
@@ -128,18 +128,6 @@ const BurnForm = (): JSX.Element | null => {
})();
}, [bridgeLoaded, collateralCurrencies, handleError]);
- // This ensures that triggering the notification and clearing
- // the form happen at the same time.
- React.useEffect(() => {
- if (submitStatus !== STATUSES.RESOLVED) return;
-
- toast.success(t('burn_page.successfully_burned'));
-
- reset({
- [WRAPPED_TOKEN_AMOUNT]: ''
- });
- }, [submitStatus, reset, t]);
-
if (status === STATUSES.IDLE || status === STATUSES.PENDING) {
return ;
}
@@ -149,18 +137,8 @@ const BurnForm = (): JSX.Element | null => {
throw new Error('Something went wrong!');
}
- const onSubmit = async (data: BurnFormData) => {
- try {
- setSubmitStatus(STATUSES.PENDING);
-
- await transaction.executeAsync(new BitcoinAmount(data[WRAPPED_TOKEN_AMOUNT]), selectedCollateral.currency);
-
- setSubmitStatus(STATUSES.RESOLVED);
- } catch (error) {
- setSubmitStatus(STATUSES.REJECTED);
- setSubmitError(error);
- }
- };
+ const onSubmit = async (data: BurnFormData) =>
+ transaction.execute(new BitcoinAmount(data[WRAPPED_TOKEN_AMOUNT]), selectedCollateral.currency);
const validateForm = (value: string): string | undefined => {
// TODO: should use wrapped token amount type (e.g. InterBtcAmount or KBtcAmount)
@@ -305,23 +283,12 @@ const BurnForm = (): JSX.Element | null => {
fullWidth
size='large'
type='submit'
- loading={submitStatus === STATUSES.PENDING}
+ loading={transaction.isLoading}
disabled={parachainStatus === ParachainStatus.Loading || parachainStatus === ParachainStatus.Shutdown}
>
{t('burn')}
- {submitStatus === STATUSES.REJECTED && submitError && (
- {
- setSubmitStatus(STATUSES.IDLE);
- setSubmitError(null);
- }}
- title='Error'
- description={typeof submitError === 'string' ? submitError : submitError.message}
- />
- )}
>
);
}
diff --git a/src/pages/Bridge/IssueForm/SubmittedIssueRequestModal/index.tsx b/src/pages/Bridge/IssueForm/SubmittedIssueRequestModal/index.tsx
index b8569c7ee6..e5afdc0146 100644
--- a/src/pages/Bridge/IssueForm/SubmittedIssueRequestModal/index.tsx
+++ b/src/pages/Bridge/IssueForm/SubmittedIssueRequestModal/index.tsx
@@ -1,12 +1,11 @@
import { Issue } from '@interlay/interbtc-api';
import clsx from 'clsx';
-import * as React from 'react';
import { useTranslation } from 'react-i18next';
-import CloseIconButton from '@/legacy-components/buttons/CloseIconButton';
+import { Modal, ModalBody, ModalFooter } from '@/component-library';
import InterlayDefaultContainedButton from '@/legacy-components/buttons/InterlayDefaultContainedButton';
import BTCPaymentPendingStatusUI from '@/legacy-components/IssueUI/BTCPaymentPendingStatusUI';
-import InterlayModal, { InterlayModalInnerWrapper, Props as ModalProps } from '@/legacy-components/UI/InterlayModal';
+import { Props as ModalProps } from '@/legacy-components/UI/InterlayModal';
import InterlayRouterLink from '@/legacy-components/UI/InterlayRouterLink';
import { PAGES, QUERY_PARAMETERS } from '@/utils/constants/links';
import { getColorShade } from '@/utils/helpers/colors';
@@ -24,32 +23,31 @@ const SubmittedIssueRequestModal = ({
}: CustomProps & Omit): JSX.Element => {
const { t } = useTranslation();
- const focusRef = React.useRef(null);
-
return (
-
-
-
+
+
{t('issue_page.deposit')}
-
-
- {t('issue_page.i_have_made_the_payment')}
-
-
-
-
+
+
+
+
+ {t('issue_page.i_have_made_the_payment')}
+
+ {' '}
+
+
);
};
diff --git a/src/pages/Bridge/IssueForm/index.tsx b/src/pages/Bridge/IssueForm/index.tsx
index 2e3b83db45..fbffaaae14 100644
--- a/src/pages/Bridge/IssueForm/index.tsx
+++ b/src/pages/Bridge/IssueForm/index.tsx
@@ -42,7 +42,6 @@ import {
} from '@/config/relay-chains';
import AvailableBalanceUI from '@/legacy-components/AvailableBalanceUI';
import ErrorFallback from '@/legacy-components/ErrorFallback';
-import ErrorModal from '@/legacy-components/ErrorModal';
import FormTitle from '@/legacy-components/FormTitle';
import Hr2 from '@/legacy-components/hrs/Hr2';
import PriceInfo from '@/legacy-components/PriceInfo';
@@ -126,7 +125,6 @@ const IssueForm = (): JSX.Element | null => {
);
const [dustValue, setDustValue] = React.useState(new BitcoinAmount(DEFAULT_ISSUE_DUST_AMOUNT));
const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE);
- const [submitError, setSubmitError] = React.useState(null);
const [submittedRequest, setSubmittedRequest] = React.useState();
const [selectVaultManually, setSelectVaultManually] = React.useState(false);
const [selectedVault, setSelectedVault] = React.useState();
@@ -142,7 +140,7 @@ const IssueForm = (): JSX.Element | null => {
});
useErrorHandler(requestLimitsError);
- const transaction = useTransaction(Transaction.ISSUE_REQUEST);
+ const transaction = useTransaction(Transaction.ISSUE_REQUEST, { showSuccessModal: false });
React.useEffect(() => {
if (!bridgeLoaded) return;
@@ -303,43 +301,38 @@ const IssueForm = (): JSX.Element | null => {
};
const onSubmit = async (data: IssueFormData) => {
- try {
- setSubmitStatus(STATUSES.PENDING);
- await requestLimitsRefetch();
- await trigger(BTC_AMOUNT);
-
- const monetaryBtcAmount = new BitcoinAmount(data[BTC_AMOUNT] || '0');
- const vaults = await window.bridge.vaults.getVaultsWithIssuableTokens();
-
- let vaultId: InterbtcPrimitivesVaultId;
- if (selectVaultManually) {
- if (!selectedVault) {
- throw new Error('Specific vault is not selected!');
- }
- vaultId = selectedVault[0];
- } else {
- vaultId = getRandomVaultIdWithCapacity(Array.from(vaults), monetaryBtcAmount);
- }
+ setSubmitStatus(STATUSES.PENDING);
+ await requestLimitsRefetch();
+ await trigger(BTC_AMOUNT);
- const collateralToken = await currencyIdToMonetaryCurrency(window.bridge.api, vaultId.currencies.collateral);
-
- const result = await transaction.executeAsync(
- monetaryBtcAmount,
- vaultId.accountId,
- collateralToken,
- false, // default
- vaults
- );
- const issueRequests = await getIssueRequestsFromExtrinsicResult(window.bridge, result);
-
- // TODO: handle issue aggregation
- const issueRequest = issueRequests[0];
- handleSubmittedRequestModalOpen(issueRequest);
- setSubmitStatus(STATUSES.RESOLVED);
- } catch (error) {
- setSubmitStatus(STATUSES.REJECTED);
- setSubmitError(error);
+ const monetaryBtcAmount = new BitcoinAmount(data[BTC_AMOUNT] || '0');
+ const vaults = await window.bridge.vaults.getVaultsWithIssuableTokens();
+
+ let vaultId: InterbtcPrimitivesVaultId;
+ if (selectVaultManually) {
+ if (!selectedVault) {
+ throw new Error('Specific vault is not selected!');
+ }
+ vaultId = selectedVault[0];
+ } else {
+ vaultId = getRandomVaultIdWithCapacity(Array.from(vaults), monetaryBtcAmount);
}
+
+ const collateralToken = await currencyIdToMonetaryCurrency(window.bridge.api, vaultId.currencies.collateral);
+
+ const result = await transaction.executeAsync(
+ monetaryBtcAmount,
+ vaultId.accountId,
+ collateralToken,
+ false, // default
+ vaults
+ );
+ const issueRequests = await getIssueRequestsFromExtrinsicResult(window.bridge, result.data);
+
+ // TODO: handle issue aggregation
+ const issueRequest = issueRequests[0];
+ handleSubmittedRequestModalOpen(issueRequest);
+ setSubmitStatus(STATUSES.RESOLVED);
};
const monetaryBtcAmount = new BitcoinAmount(btcAmount);
@@ -536,17 +529,6 @@ const IssueForm = (): JSX.Element | null => {
{t('confirm')}
- {submitStatus === STATUSES.REJECTED && submitError && (
- {
- setSubmitStatus(STATUSES.IDLE);
- setSubmitError(null);
- }}
- title='Error'
- description={typeof submitError === 'string' ? submitError : submitError.message}
- />
- )}
{submittedRequest && (
-
-
+
+
{t('redeem_page.redeem')}
@@ -114,8 +110,8 @@ const SubmittedRedeemRequestModal = ({
-
-
+
+
);
};
diff --git a/src/pages/Bridge/RedeemForm/index.tsx b/src/pages/Bridge/RedeemForm/index.tsx
index 357bfcf540..f934ae0a99 100644
--- a/src/pages/Bridge/RedeemForm/index.tsx
+++ b/src/pages/Bridge/RedeemForm/index.tsx
@@ -35,7 +35,6 @@ import {
import { BALANCE_MAX_INTEGER_LENGTH, BTC_ADDRESS_REGEX } from '@/constants';
import AvailableBalanceUI from '@/legacy-components/AvailableBalanceUI';
import ErrorFallback from '@/legacy-components/ErrorFallback';
-import ErrorModal from '@/legacy-components/ErrorModal';
import FormTitle from '@/legacy-components/FormTitle';
import Hr2 from '@/legacy-components/hrs/Hr2';
import PriceInfo from '@/legacy-components/PriceInfo';
@@ -112,14 +111,13 @@ const RedeemForm = (): JSX.Element | null => {
const [premiumRedeemFee, setPremiumRedeemFee] = React.useState(new Big(0));
const [currentInclusionFee, setCurrentInclusionFee] = React.useState(BitcoinAmount.zero());
const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE);
- const [submitError, setSubmitError] = React.useState(null);
const [submittedRequest, setSubmittedRequest] = React.useState();
const [selectVaultManually, setSelectVaultManually] = React.useState(false);
const [selectedVault, setSelectedVault] = React.useState();
- const transaction = useTransaction(Transaction.REDEEM_REQUEST);
+ const transaction = useTransaction(Transaction.REDEEM_REQUEST, { showSuccessModal: false });
React.useEffect(() => {
if (!monetaryWrappedTokenAmount) return;
@@ -305,7 +303,7 @@ const RedeemForm = (): JSX.Element | null => {
const result = await transaction.executeAsync(monetaryWrappedTokenAmount, data[BTC_ADDRESS], vaultId);
- const redeemRequests = await getRedeemRequestsFromExtrinsicResult(window.bridge, result);
+ const redeemRequests = await getRedeemRequestsFromExtrinsicResult(window.bridge, result.data);
// TODO: handle redeem aggregator
const redeemRequest = redeemRequests[0];
@@ -313,7 +311,6 @@ const RedeemForm = (): JSX.Element | null => {
setSubmitStatus(STATUSES.RESOLVED);
} catch (error) {
setSubmitStatus(STATUSES.REJECTED);
- setSubmitError(error);
}
};
@@ -533,17 +530,6 @@ const RedeemForm = (): JSX.Element | null => {
{t('confirm')}
- {submitStatus === STATUSES.REJECTED && submitError && (
- {
- setSubmitStatus(STATUSES.IDLE);
- setSubmitError(null);
- }}
- title='Error'
- description={typeof submitError === 'string' ? submitError : submitError.message}
- />
- )}
{submittedRequest && (
{
- toast.success('Successfully toggled collateral');
- onClose?.();
- refetch();
- }
+ onSigning: onClose,
+ onSuccess: refetch
});
if (!asset || !position) {
@@ -94,31 +89,21 @@ const CollateralModal = ({ asset, position, onClose, ...props }: CollateralModal
};
return (
- <>
-
- {content.title}
-
-
- {content.description}
-
- {variant !== 'disable-error' && }
-
-
-
-
- {content.buttonLabel}
-
-
-
- {transaction.isError && (
- transaction.reset()}
- title='Error'
- description={transaction.error?.message || ''}
- />
- )}
- >
+
+ {content.title}
+
+
+ {content.description}
+
+ {variant !== 'disable-error' && }
+
+
+
+
+ {content.buttonLabel}
+
+
+
);
};
diff --git a/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx b/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx
index edc7763901..390be8cba5 100644
--- a/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx
+++ b/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx
@@ -3,7 +3,6 @@ import { MonetaryAmount } from '@interlay/monetary-js';
import { mergeProps } from '@react-aria/utils';
import { ChangeEventHandler, useState } from 'react';
import { TFunction, useTranslation } from 'react-i18next';
-import { toast } from 'react-toastify';
import { useDebounce } from 'react-use';
import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/common/utils/utils';
@@ -116,42 +115,29 @@ const LoanForm = ({ asset, variant, position, onChangeLoan }: LoanFormProps): JS
[inputAmount]
);
- const transaction = useTransaction({
- onSuccess: () => {
- toast.success(`Successful ${content.title.toLowerCase()}`);
- onChangeLoan?.();
- refetch();
- },
- onError: (error: Error) => {
- toast.error(error.message);
- }
- });
+ const transaction = useTransaction({ onSigning: onChangeLoan, onSuccess: refetch });
const handleSubmit = (data: LoanFormData) => {
- try {
- const amount = data[variant] || 0;
- const monetaryAmount = newMonetaryAmount(amount, asset.currency, true);
-
- switch (variant) {
- case 'lend':
- return transaction.execute(Transaction.LOANS_LEND, monetaryAmount.currency, monetaryAmount);
- case 'withdraw':
- if (isMaxAmount) {
- return transaction.execute(Transaction.LOANS_WITHDRAW_ALL, monetaryAmount.currency);
- } else {
- return transaction.execute(Transaction.LOANS_WITHDRAW, monetaryAmount.currency, monetaryAmount);
- }
- case 'borrow':
- return transaction.execute(Transaction.LOANS_BORROW, monetaryAmount.currency, monetaryAmount);
- case 'repay':
- if (isMaxAmount) {
- return transaction.execute(Transaction.LOANS_REPAY_ALL, monetaryAmount.currency);
- } else {
- return transaction.execute(Transaction.LOANS_REPAY, monetaryAmount.currency, monetaryAmount);
- }
- }
- } catch (err: any) {
- toast.error(err.toString());
+ const amount = data[variant] || 0;
+ const monetaryAmount = newMonetaryAmount(amount, asset.currency, true);
+
+ switch (variant) {
+ case 'lend':
+ return transaction.execute(Transaction.LOANS_LEND, monetaryAmount.currency, monetaryAmount);
+ case 'withdraw':
+ if (isMaxAmount) {
+ return transaction.execute(Transaction.LOANS_WITHDRAW_ALL, monetaryAmount.currency);
+ } else {
+ return transaction.execute(Transaction.LOANS_WITHDRAW, monetaryAmount.currency, monetaryAmount);
+ }
+ case 'borrow':
+ return transaction.execute(Transaction.LOANS_BORROW, monetaryAmount.currency, monetaryAmount);
+ case 'repay':
+ if (isMaxAmount) {
+ return transaction.execute(Transaction.LOANS_REPAY_ALL, monetaryAmount.currency);
+ } else {
+ return transaction.execute(Transaction.LOANS_REPAY, monetaryAmount.currency, monetaryAmount);
+ }
}
};
@@ -216,7 +202,7 @@ const LoanForm = ({ asset, variant, position, onChangeLoan }: LoanFormProps): JS
-
+
{content.title}
diff --git a/src/pages/Loans/LoansOverview/components/LoansInsights/LoansInsights.tsx b/src/pages/Loans/LoansOverview/components/LoansInsights/LoansInsights.tsx
index 6ad85f8d82..2ef53674f4 100644
--- a/src/pages/Loans/LoansOverview/components/LoansInsights/LoansInsights.tsx
+++ b/src/pages/Loans/LoansOverview/components/LoansInsights/LoansInsights.tsx
@@ -1,10 +1,6 @@
-import { useTranslation } from 'react-i18next';
-import { toast } from 'react-toastify';
-
import { formatNumber, formatPercentage, formatUSD } from '@/common/utils/utils';
import { Card, Dl, DlGroup } from '@/component-library';
import { AuthCTA } from '@/components';
-import ErrorModal from '@/legacy-components/ErrorModal';
import { AccountLendingStatistics } from '@/utils/hooks/api/loans/use-get-account-lending-statistics';
import { useGetAccountSubsidyRewards } from '@/utils/hooks/api/loans/use-get-account-subsidy-rewards';
import { Transaction, useTransaction } from '@/utils/hooks/transaction';
@@ -16,14 +12,10 @@ type LoansInsightsProps = {
};
const LoansInsights = ({ statistics }: LoansInsightsProps): JSX.Element => {
- const { t } = useTranslation();
const { data: subsidyRewards, refetch } = useGetAccountSubsidyRewards();
const transaction = useTransaction(Transaction.LOANS_CLAIM_REWARDS, {
- onSuccess: () => {
- toast.success(t('successfully_claimed_rewards'));
- refetch();
- }
+ onSuccess: refetch
});
const handleClickClaimRewards = () => transaction.execute();
@@ -76,14 +68,6 @@ const LoansInsights = ({ statistics }: LoansInsightsProps): JSX.Element => {
)}
- {transaction.isError && (
- transaction.reset()}
- title='Error'
- description={transaction.error?.message || ''}
- />
- )}
>
);
};
diff --git a/src/pages/Staking/ClaimRewardsButton/index.tsx b/src/pages/Staking/ClaimRewardsButton/index.tsx
index 442da162c0..e7ab257735 100644
--- a/src/pages/Staking/ClaimRewardsButton/index.tsx
+++ b/src/pages/Staking/ClaimRewardsButton/index.tsx
@@ -5,7 +5,6 @@ import { GOVERNANCE_TOKEN_SYMBOL } from '@/config/relay-chains';
import InterlayDenimOrKintsugiSupernovaContainedButton, {
Props as InterlayDenimOrKintsugiMidnightContainedButtonProps
} from '@/legacy-components/buttons/InterlayDenimOrKintsugiSupernovaContainedButton';
-import ErrorModal from '@/legacy-components/ErrorModal';
import { useSubstrateSecureState } from '@/lib/substrate';
import { GENERIC_FETCHER } from '@/services/fetchers/generic-fetcher';
import { Transaction, useTransaction } from '@/utils/hooks/transaction';
@@ -35,26 +34,14 @@ const ClaimRewardsButton = ({
};
return (
- <>
-
- Claim {claimableRewardAmount} {GOVERNANCE_TOKEN_SYMBOL} Rewards
-
- {transaction.isError && (
- {
- transaction.reset();
- }}
- title='Error'
- description={transaction.error?.message || ''}
- />
- )}
- >
+
+ Claim {claimableRewardAmount} {GOVERNANCE_TOKEN_SYMBOL} Rewards
+
);
};
diff --git a/src/pages/Staking/WithdrawButton/index.tsx b/src/pages/Staking/WithdrawButton/index.tsx
index 7093017a52..190d2a628c 100644
--- a/src/pages/Staking/WithdrawButton/index.tsx
+++ b/src/pages/Staking/WithdrawButton/index.tsx
@@ -1,19 +1,17 @@
-import { ISubmittableResult } from '@polkadot/types/types';
import clsx from 'clsx';
import { add, format } from 'date-fns';
-import { useMutation, useQueryClient } from 'react-query';
+import { useQueryClient } from 'react-query';
import { BLOCK_TIME } from '@/config/parachain';
import { GOVERNANCE_TOKEN_SYMBOL } from '@/config/relay-chains';
import InterlayDenimOrKintsugiSupernovaContainedButton, {
Props as InterlayDenimOrKintsugiMidnightContainedButtonProps
} from '@/legacy-components/buttons/InterlayDenimOrKintsugiSupernovaContainedButton';
-import ErrorModal from '@/legacy-components/ErrorModal';
import InformationTooltip from '@/legacy-components/tooltips/InformationTooltip';
import { useSubstrateSecureState } from '@/lib/substrate';
import { GENERIC_FETCHER } from '@/services/fetchers/generic-fetcher';
import { YEAR_MONTH_DAY_PATTERN } from '@/utils/constants/date-time';
-import { submitExtrinsic } from '@/utils/helpers/extrinsic';
+import { Transaction, useTransaction } from '@/utils/hooks/transaction';
const getFormattedUnlockDate = (remainingBlockNumbersToUnstake: number, formatPattern: string) => {
const unlockDate = add(new Date(), {
@@ -36,22 +34,15 @@ const WithdrawButton = ({
}: CustomProps & InterlayDenimOrKintsugiMidnightContainedButtonProps): JSX.Element => {
const { selectedAccount } = useSubstrateSecureState();
- const queryClient = useQueryClient();
-
- const withdrawMutation = useMutation(
- () => {
- return submitExtrinsic(window.bridge.escrow.withdraw());
- },
- {
- onSuccess: () => {
- queryClient.invalidateQueries([GENERIC_FETCHER, 'escrow', 'getStakedBalance', selectedAccount?.address]);
- }
+ const transaction = useTransaction(Transaction.ESCROW_WITHDRAW, {
+ onSuccess: () => {
+ queryClient.invalidateQueries([GENERIC_FETCHER, 'escrow', 'getStakedBalance', selectedAccount?.address]);
}
- );
+ });
- const handleUnstake = () => {
- withdrawMutation.mutate();
- };
+ const queryClient = useQueryClient();
+
+ const handleUnstake = () => transaction.execute();
const disabled = remainingBlockNumbersToUnstake ? remainingBlockNumbersToUnstake > 0 : false;
@@ -79,22 +70,12 @@ const WithdrawButton = ({
/>
}
onClick={handleUnstake}
- pending={withdrawMutation.isLoading}
+ pending={transaction.isLoading}
disabled={disabled}
{...rest}
>
Withdraw Staked {GOVERNANCE_TOKEN_SYMBOL} {renderUnlockDateLabel()}
- {withdrawMutation.isError && (
- {
- withdrawMutation.reset();
- }}
- title='Error'
- description={withdrawMutation.error?.message || ''}
- />
- )}
>
);
};
diff --git a/src/pages/Staking/index.tsx b/src/pages/Staking/index.tsx
index 043d6b1185..df2f0b697f 100644
--- a/src/pages/Staking/index.tsx
+++ b/src/pages/Staking/index.tsx
@@ -29,7 +29,6 @@ import {
} from '@/config/relay-chains';
import AvailableBalanceUI from '@/legacy-components/AvailableBalanceUI';
import ErrorFallback from '@/legacy-components/ErrorFallback';
-import ErrorModal from '@/legacy-components/ErrorModal';
import Panel from '@/legacy-components/Panel';
import TitleWithUnderline from '@/legacy-components/TitleWithUnderline';
import TokenField from '@/legacy-components/TokenField';
@@ -837,17 +836,6 @@ const Staking = (): JSX.Element => {
- {(initialStakeTransaction.isError || existingStakeTransaction.isError) && (
- {
- initialStakeTransaction.reset();
- existingStakeTransaction.reset();
- }}
- title='Error'
- description={initialStakeTransaction.error?.message || existingStakeTransaction.error?.message || ''}
- />
- )}
>
);
};
diff --git a/src/pages/Transactions/IssueRequestsTable/IssueRequestModal/index.tsx b/src/pages/Transactions/IssueRequestsTable/IssueRequestModal/index.tsx
index c1457d5a8d..765659e2d9 100644
--- a/src/pages/Transactions/IssueRequestsTable/IssueRequestModal/index.tsx
+++ b/src/pages/Transactions/IssueRequestsTable/IssueRequestModal/index.tsx
@@ -1,13 +1,8 @@
-import clsx from 'clsx';
-import * as React from 'react';
import { useTranslation } from 'react-i18next';
-import CloseIconButton from '@/legacy-components/buttons/CloseIconButton';
-import Hr1 from '@/legacy-components/hrs/Hr1';
+import { Modal, ModalBody, ModalHeader } from '@/component-library';
import IssueUI from '@/legacy-components/IssueUI';
-import InterlayModal, { InterlayModalInnerWrapper, Props as ModalProps } from '@/legacy-components/UI/InterlayModal';
-
-import RequestModalTitle from '../../RequestModalTitle';
+import { Props as ModalProps } from '@/legacy-components/UI/InterlayModal';
interface CustomProps {
request: any; // TODO: should type properly (`Relay`)
@@ -16,17 +11,13 @@ interface CustomProps {
const IssueRequestModal = ({ open, onClose, request }: CustomProps & Omit): JSX.Element => {
const { t } = useTranslation();
- const focusRef = React.useRef(null);
-
return (
-
-
- {t('issue_page.request', { id: request.id })}
-
-
+
+ {t('issue_page.request', { id: request.id })}
+
-
-
+
+
);
};
diff --git a/src/pages/Transactions/RedeemRequestsTable/RedeemRequestModal/index.tsx b/src/pages/Transactions/RedeemRequestsTable/RedeemRequestModal/index.tsx
index ccc3fba223..fee187b468 100644
--- a/src/pages/Transactions/RedeemRequestsTable/RedeemRequestModal/index.tsx
+++ b/src/pages/Transactions/RedeemRequestsTable/RedeemRequestModal/index.tsx
@@ -1,13 +1,8 @@
-import clsx from 'clsx';
-import * as React from 'react';
import { useTranslation } from 'react-i18next';
-import CloseIconButton from '@/legacy-components/buttons/CloseIconButton';
-import Hr1 from '@/legacy-components/hrs/Hr1';
+import { Modal, ModalBody, ModalHeader } from '@/component-library';
import RedeemUI from '@/legacy-components/RedeemUI';
-import InterlayModal, { InterlayModalInnerWrapper, Props as ModalProps } from '@/legacy-components/UI/InterlayModal';
-
-import RequestModalTitle from '../../RequestModalTitle';
+import { Props as ModalProps } from '@/legacy-components/UI/InterlayModal';
interface CustomProps {
// TODO: should type properly (`Relay`)
@@ -21,17 +16,13 @@ const RedeemRequestModal = ({
}: CustomProps & Omit): JSX.Element | null => {
const { t } = useTranslation();
- const focusRef = React.useRef(null);
-
return (
-
-
- {t('issue_page.request', { id: request.id })}
-
-
+
+ {t('issue_page.request', { id: request.id })}
+
-
-
+
+
);
};
diff --git a/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx b/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx
index 1e7a864185..fbd54a3cd8 100644
--- a/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx
+++ b/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx
@@ -1,12 +1,9 @@
-import { FixedPointNumber } from '@acala-network/sdk-core';
-import { ChainName, CrossChainTransferParams } from '@interlay/bridge';
+import { ChainName } from '@interlay/bridge';
import { newMonetaryAmount } from '@interlay/interbtc-api';
import { web3FromAddress } from '@polkadot/extension-dapp';
import { mergeProps } from '@react-aria/utils';
import { ChangeEventHandler, Key, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { useMutation } from 'react-query';
-import { toast } from 'react-toastify';
import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/common/utils/utils';
import { Dd, DlGroup, Dt, Flex, LoadingSpinner, TokenInput } from '@/component-library';
@@ -25,11 +22,11 @@ import {
} from '@/lib/form';
import { useSubstrateSecureState } from '@/lib/substrate';
import { Chains } from '@/types/chains';
-import { submitExtrinsic } from '@/utils/helpers/extrinsic';
import { getTokenPrice } from '@/utils/helpers/prices';
import { useGetCurrencies } from '@/utils/hooks/api/use-get-currencies';
import { useGetPrices } from '@/utils/hooks/api/use-get-prices';
import { useXCMBridge, XCMTokenData } from '@/utils/hooks/api/xcm/use-xcm-bridge';
+import { Transaction, useTransaction } from '@/utils/hooks/transaction';
import useAccountId from '@/utils/hooks/use-account-id';
import { ChainSelect } from './components';
@@ -65,37 +62,36 @@ const CrossChainTransferForm = (): JSX.Element => {
}
};
- const mutateXcmTransfer = async (formData: CrossChainTransferFormData) => {
+ const transaction = useTransaction(Transaction.XCM_TRANSFER, {
+ onSuccess: () => {
+ setTokenData(form.values[CROSS_CHAIN_TRANSFER_TO_FIELD] as ChainName);
+ form.setFieldValue(CROSS_CHAIN_TRANSFER_AMOUNT_FIELD, '');
+ }
+ });
+
+ const handleSubmit = async (formData: CrossChainTransferFormData) => {
if (!data || !formData || !currentToken) return;
- const { signer } = await web3FromAddress(formData[CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD] as string);
+ const address = formData[CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD] as string;
+
+ const { signer } = await web3FromAddress(address);
const adapter = data.bridge.findAdapter(formData[CROSS_CHAIN_TRANSFER_FROM_FIELD] as ChainName);
const apiPromise = data.provider.getApiPromise(formData[CROSS_CHAIN_TRANSFER_FROM_FIELD] as string);
apiPromise.setSigner(signer);
adapter.setApi(apiPromise);
+ const transferCurrency = getCurrencyFromTicker(currentToken.value);
const transferAmount = newMonetaryAmount(
form.values[CROSS_CHAIN_TRANSFER_AMOUNT_FIELD] || 0,
- getCurrencyFromTicker(currentToken.value),
+ transferCurrency,
true
);
- const transferAmountString = transferAmount.toString(true);
- const transferAmountDecimals = transferAmount.currency.decimals;
-
- const tx = adapter.createTx({
- amount: FixedPointNumber.fromInner(transferAmountString, transferAmountDecimals),
- to: formData[CROSS_CHAIN_TRANSFER_TO_FIELD],
- token: formData[CROSS_CHAIN_TRANSFER_TOKEN_FIELD],
- address: formData[CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD]
- } as CrossChainTransferParams);
-
- await submitExtrinsic({ extrinsic: tx });
- };
+ const fromChain = formData[CROSS_CHAIN_TRANSFER_FROM_FIELD] as ChainName;
+ const toChain = formData[CROSS_CHAIN_TRANSFER_TO_FIELD] as ChainName;
- const handleSubmit = (formData: CrossChainTransferFormData) => {
- xcmTransferMutation.mutate(formData);
+ transaction.execute(adapter, fromChain, toChain, address, transferAmount);
};
const form = useForm({
@@ -108,18 +104,6 @@ const CrossChainTransferForm = (): JSX.Element => {
validationSchema: crossChainTransferSchema(schema, t)
});
- const xcmTransferMutation = useMutation(mutateXcmTransfer, {
- onSuccess: async () => {
- toast.success('Transfer successful');
-
- setTokenData(form.values[CROSS_CHAIN_TRANSFER_TO_FIELD] as ChainName);
- form.setFieldValue(CROSS_CHAIN_TRANSFER_AMOUNT_FIELD, '');
- },
- onError: (err) => {
- toast.error(err.message);
- }
- });
-
const handleOriginatingChainChange = (chain: ChainName, name: string) => {
form.setFieldValue(name, chain);
@@ -238,9 +222,7 @@ const CrossChainTransferForm = (): JSX.Element => {
onSelectionChange={(chain: Key) =>
handleOriginatingChainChange(chain as ChainName, CROSS_CHAIN_TRANSFER_FROM_FIELD)
}
- {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_FROM_FIELD, false), {
- onChange: handleOriginatingChainChange
- })}
+ {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_FROM_FIELD, false))}
/>
{
onSelectionChange={(chain: Key) =>
handleDestinationChainChange(chain as ChainName, CROSS_CHAIN_TRANSFER_TO_FIELD)
}
- {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_TO_FIELD, false), {
- onChange: handleDestinationChainChange
- })}
+ {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_TO_FIELD, false))}
/>
@@ -290,7 +270,7 @@ const CrossChainTransferForm = (): JSX.Element => {
{`${currentToken?.destFee.toString()} ${currentToken?.value}`}
-
+
{isCTADisabled ? 'Enter transfer amount' : t('transfer')}
diff --git a/src/pages/Transfer/TransferForm/index.tsx b/src/pages/Transfer/TransferForm/index.tsx
index a488cd288f..2bc9ed3f19 100644
--- a/src/pages/Transfer/TransferForm/index.tsx
+++ b/src/pages/Transfer/TransferForm/index.tsx
@@ -5,19 +5,16 @@ import { withErrorBoundary } from 'react-error-boundary';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
-import { toast } from 'react-toastify';
import { ParachainStatus, StoreType } from '@/common/types/util.types';
import { formatNumber } from '@/common/utils/utils';
import { AuthCTA } from '@/components';
import ErrorFallback from '@/legacy-components/ErrorFallback';
-import ErrorModal from '@/legacy-components/ErrorModal';
import FormTitle from '@/legacy-components/FormTitle';
import TextField from '@/legacy-components/TextField';
import Tokens, { TokenOption } from '@/legacy-components/Tokens';
import InterlayButtonBase from '@/legacy-components/UI/InterlayButtonBase';
import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names';
-import STATUSES from '@/utils/constants/statuses';
import isValidPolkadotAddress from '@/utils/helpers/is-valid-polkadot-address';
import { Transaction, useTransaction } from '@/utils/hooks/transaction';
@@ -47,28 +44,21 @@ const TransferForm = (): JSX.Element => {
});
const [activeToken, setActiveToken] = React.useState(undefined);
- const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE);
- const [submitError, setSubmitError] = React.useState(null);
- const transaction = useTransaction(Transaction.TOKENS_TRANSFER);
+ const transaction = useTransaction(Transaction.TOKENS_TRANSFER, {
+ onSigning: () => {
+ reset({
+ [TRANSFER_AMOUNT]: '',
+ [RECIPIENT_ADDRESS]: ''
+ });
+ }
+ });
const onSubmit = async (data: TransferFormData) => {
if (!activeToken) return;
if (data[TRANSFER_AMOUNT] === undefined) return;
- try {
- setSubmitStatus(STATUSES.PENDING);
-
- await transaction.executeAsync(
- data[RECIPIENT_ADDRESS],
- newMonetaryAmount(data[TRANSFER_AMOUNT], activeToken.token, true)
- );
-
- setSubmitStatus(STATUSES.RESOLVED);
- } catch (error) {
- setSubmitStatus(STATUSES.REJECTED);
- setSubmitError(error);
- }
+ transaction.execute(data[RECIPIENT_ADDRESS], newMonetaryAmount(data[TRANSFER_AMOUNT], activeToken.token, true));
};
const validateTransferAmount = React.useCallback(
@@ -96,19 +86,6 @@ const TransferForm = (): JSX.Element => {
const handleClickBalance = () => setValue(TRANSFER_AMOUNT, activeToken?.transferableBalance || '');
- // This ensures that triggering the notification and clearing
- // the form happen at the same time.
- React.useEffect(() => {
- if (submitStatus !== STATUSES.RESOLVED) return;
-
- toast.success(t('transfer_page.successfully_transferred'));
-
- reset({
- [TRANSFER_AMOUNT]: '',
- [RECIPIENT_ADDRESS]: ''
- });
- }, [submitStatus, reset, t]);
-
return (
<>
- {submitStatus === STATUSES.REJECTED && submitError && (
-
{
- setSubmitStatus(STATUSES.IDLE);
- setSubmitError(null);
- }}
- title='Error'
- description={typeof submitError === 'string' ? submitError : submitError.message}
- />
- )}
>
);
};
diff --git a/src/pages/Vaults/Vault/RequestIssueModal/index.tsx b/src/pages/Vaults/Vault/RequestIssueModal/index.tsx
index 4e9215ce82..4eec9acdd1 100644
--- a/src/pages/Vaults/Vault/RequestIssueModal/index.tsx
+++ b/src/pages/Vaults/Vault/RequestIssueModal/index.tsx
@@ -16,6 +16,7 @@ import { useSelector } from 'react-redux';
import { ReactComponent as BitcoinLogoIcon } from '@/assets/img/bitcoin-logo.svg';
import { ParachainStatus, StoreType } from '@/common/types/util.types';
import { displayMonetaryAmount, displayMonetaryAmountInUSDFormat } from '@/common/utils/utils';
+import { Modal, ModalBody, ModalHeader } from '@/component-library';
import {
BLOCKS_BEHIND_LIMIT,
DEFAULT_ISSUE_BRIDGE_FEE_RATE,
@@ -30,15 +31,12 @@ import {
WRAPPED_TOKEN_SYMBOL,
WrappedTokenLogoIcon
} from '@/config/relay-chains';
-import CloseIconButton from '@/legacy-components/buttons/CloseIconButton';
-import ErrorModal from '@/legacy-components/ErrorModal';
import Hr2 from '@/legacy-components/hrs/Hr2';
import PriceInfo from '@/legacy-components/PriceInfo';
import SubmitButton from '@/legacy-components/SubmitButton';
import TokenField from '@/legacy-components/TokenField';
import InformationTooltip from '@/legacy-components/tooltips/InformationTooltip';
import InterlayButtonBase from '@/legacy-components/UI/InterlayButtonBase';
-import InterlayModal, { InterlayModalInnerWrapper, InterlayModalTitle } from '@/legacy-components/UI/InterlayModal';
import { useSubstrateSecureState } from '@/lib/substrate';
import SubmittedIssueRequestModal from '@/pages/Bridge/IssueForm/SubmittedIssueRequestModal';
import { ForeignAssetIdLiteral } from '@/types/currency';
@@ -90,12 +88,10 @@ const RequestIssueModal = ({ onClose, open, collateralToken, vaultAddress }: Pro
);
const [dustValue, setDustValue] = React.useState(new BitcoinAmount(DEFAULT_ISSUE_DUST_AMOUNT));
const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE);
- const [submitError, setSubmitError] = React.useState(null);
const [submittedRequest, setSubmittedRequest] = React.useState();
const { t } = useTranslation();
const prices = useGetPrices();
- const focusRef = React.useRef(null);
const handleError = useErrorHandler();
@@ -108,7 +104,7 @@ const RequestIssueModal = ({ onClose, open, collateralToken, vaultAddress }: Pro
const vaultAccountId = useAccountId(vaultAddress);
- const transaction = useTransaction(Transaction.ISSUE_REQUEST);
+ const transaction = useTransaction(Transaction.ISSUE_REQUEST, { showSuccessModal: false });
React.useEffect(() => {
if (!bridgeLoaded) return;
@@ -174,31 +170,29 @@ const RequestIssueModal = ({ onClose, open, collateralToken, vaultAddress }: Pro
}
const onSubmit = async (data: RequestIssueFormData) => {
- try {
- setSubmitStatus(STATUSES.PENDING);
- await trigger(WRAPPED_TOKEN_AMOUNT);
+ setSubmitStatus(STATUSES.PENDING);
- const wrappedTokenAmount = new BitcoinAmount(data[WRAPPED_TOKEN_AMOUNT] || '0');
+ await trigger(WRAPPED_TOKEN_AMOUNT);
- const vaults = await window.bridge.vaults.getVaultsWithIssuableTokens();
+ const wrappedTokenAmount = new BitcoinAmount(data[WRAPPED_TOKEN_AMOUNT] || '0');
- const extrinsicResult = await transaction.executeAsync(
- wrappedTokenAmount,
- vaultAccountId,
- collateralToken,
- false, // default
- vaults
- );
+ const vaults = await window.bridge.vaults.getVaultsWithIssuableTokens();
- const issueRequests = await getIssueRequestsFromExtrinsicResult(window.bridge, extrinsicResult);
+ const result = await transaction.executeAsync(
+ wrappedTokenAmount,
+ vaultAccountId,
+ collateralToken,
+ false, // default
+ vaults
+ );
- // TODO: handle issue aggregation
- const issueRequest = issueRequests[0];
- handleSubmittedRequestModalOpen(issueRequest);
- } catch (error) {
- setSubmitStatus(STATUSES.REJECTED);
- }
+ const issueRequests = await getIssueRequestsFromExtrinsicResult(window.bridge, result.data);
+
+ // TODO: handle issue aggregation
+ const issueRequest = issueRequests[0];
+ handleSubmittedRequestModalOpen(issueRequest);
setSubmitStatus(STATUSES.RESOLVED);
+ onClose();
};
const validateForm = (value: string): string | undefined => {
@@ -267,12 +261,9 @@ const RequestIssueModal = ({ onClose, open, collateralToken, vaultAddress }: Pro
return (
<>
-
-
-
- {t('vault.request_issue')}
-
-
+
+ {t('vault.request_issue')}
+
-
-
- {submitStatus === STATUSES.REJECTED && submitError && (
- {
- setSubmitStatus(STATUSES.IDLE);
- setSubmitError(null);
- }}
- title='Error'
- description={typeof submitError === 'string' ? submitError : submitError.message}
- />
- )}
+
+
{submittedRequest && (
();
const [isRequestPending, setRequestPending] = React.useState(false);
const { t } = useTranslation();
- const focusRef = React.useRef(null);
const transaction = useTransaction(Transaction.REDEEM_REQUEST);
const onSubmit = handleSubmit(async (data) => {
setRequestPending(true);
+
try {
// Represents being less than 1 Satoshi
if (new BitcoinAmount(data[WRAPPED_TOKEN_AMOUNT])._rawAmount.lt(1)) {
@@ -67,12 +65,11 @@ const RequestRedeemModal = ({ onClose, open, collateralToken, vaultAddress, lock
queryClient.invalidateQueries(['vaultsOverview', vaultAddress, collateralToken.ticker]);
- toast.success('Redeem request submitted');
onClose();
- } catch (error) {
- toast.error(error.toString());
+ setRequestPending(false);
+ } catch (error: any) {
+ transaction.reject(error);
}
- setRequestPending(false);
});
const validateAmount = (value: string): string | undefined => {
@@ -89,12 +86,9 @@ const RequestRedeemModal = ({ onClose, open, collateralToken, vaultAddress, lock
};
return (
-
-
-
- {t('vault.request_redeem')}
-
-
+
+ {t('vault.request_redeem')}
+
-
-
+
+
);
};
diff --git a/src/pages/Vaults/Vault/RequestReplacementModal/index.tsx b/src/pages/Vaults/Vault/RequestReplacementModal/index.tsx
index a92acc73b2..3001474981 100644
--- a/src/pages/Vaults/Vault/RequestReplacementModal/index.tsx
+++ b/src/pages/Vaults/Vault/RequestReplacementModal/index.tsx
@@ -9,20 +9,18 @@ import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
-import { toast } from 'react-toastify';
import { StoreType } from '@/common/types/util.types';
import { displayMonetaryAmount } from '@/common/utils/utils';
+import { Modal, ModalBody, ModalHeader } from '@/component-library';
import { ACCOUNT_ID_TYPE_NAME } from '@/config/general';
import { DEFAULT_REDEEM_DUST_AMOUNT } from '@/config/parachain';
import { GOVERNANCE_TOKEN, GOVERNANCE_TOKEN_SYMBOL, TRANSACTION_FEE_AMOUNT } from '@/config/relay-chains';
-import CloseIconButton from '@/legacy-components/buttons/CloseIconButton';
import InterlayCinnabarOutlinedButton from '@/legacy-components/buttons/InterlayCinnabarOutlinedButton';
import InterlayMulberryOutlinedButton from '@/legacy-components/buttons/InterlayMulberryOutlinedButton';
import ErrorMessage from '@/legacy-components/ErrorMessage';
import NumberInput from '@/legacy-components/NumberInput';
import PrimaryColorEllipsisLoader from '@/legacy-components/PrimaryColorEllipsisLoader';
-import InterlayModal, { InterlayModalInnerWrapper, InterlayModalTitle } from '@/legacy-components/UI/InterlayModal';
import { GENERIC_FETCHER } from '@/services/fetchers/generic-fetcher';
import STATUSES from '@/utils/constants/statuses';
import { getExchangeRate } from '@/utils/helpers/oracle';
@@ -66,8 +64,6 @@ const RequestReplacementModal = ({
const handleError = useErrorHandler();
const { isLoading: isBalancesLoading, data: balances } = useGetBalances();
- const focusRef = React.useRef(null);
-
const { bridgeLoaded } = useSelector((state: StoreType) => state.general);
const [status, setStatus] = React.useState(STATUSES.IDLE);
@@ -112,10 +108,10 @@ const RequestReplacementModal = ({
const vaultId = window.bridge.api.createType(ACCOUNT_ID_TYPE_NAME, vaultAddress);
queryClient.invalidateQueries([GENERIC_FETCHER, 'mapReplaceRequests', vaultId]);
- toast.success('Replacement request is submitted');
setSubmitStatus(STATUSES.RESOLVED);
onClose();
- } catch (error) {
+ } catch (error: any) {
+ transaction.reject(error);
setSubmitStatus(STATUSES.REJECTED);
}
});
@@ -158,12 +154,9 @@ const RequestReplacementModal = ({
const securityDeposit = btcToGovernanceTokenRate.toCounter(wrappedTokenAmount).mul(griefingRate);
return (
-
-
-
- {t('vault.request_replacement')}
-
-
+
+ {t('vault.request_replacement')}
+
-
-
+
+
);
}
return null;
diff --git a/src/pages/Vaults/Vault/UpdateCollateralModal/index.tsx b/src/pages/Vaults/Vault/UpdateCollateralModal/index.tsx
index dad669da97..c01420c02c 100644
--- a/src/pages/Vaults/Vault/UpdateCollateralModal/index.tsx
+++ b/src/pages/Vaults/Vault/UpdateCollateralModal/index.tsx
@@ -9,16 +9,14 @@ import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useQuery, useQueryClient } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
-import { toast } from 'react-toastify';
import { updateCollateralAction, updateCollateralizationAction } from '@/common/actions/vault.actions';
import { StoreType } from '@/common/types/util.types';
import { displayMonetaryAmount, displayMonetaryAmountInUSDFormat, formatPercentage } from '@/common/utils/utils';
+import { Modal, ModalBody, ModalHeader } from '@/component-library';
import { ACCOUNT_ID_TYPE_NAME } from '@/config/general';
-import CloseIconButton from '@/legacy-components/buttons/CloseIconButton';
import InterlayDefaultContainedButton from '@/legacy-components/buttons/InterlayDefaultContainedButton';
import TokenField from '@/legacy-components/TokenField';
-import InterlayModal, { InterlayModalInnerWrapper, InterlayModalTitle } from '@/legacy-components/UI/InterlayModal';
import genericFetcher, { GENERIC_FETCHER } from '@/services/fetchers/generic-fetcher';
import STATUSES from '@/utils/constants/statuses';
import { getTokenPrice } from '@/utils/helpers/prices';
@@ -73,7 +71,6 @@ const UpdateCollateralModal = ({
const dispatch = useDispatch();
const { t } = useTranslation();
- const focusRef = React.useRef(null);
const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE);
const handleError = useErrorHandler();
@@ -164,11 +161,10 @@ const UpdateCollateralModal = ({
dispatch(updateCollateralizationAction(strVaultCollateralizationPercentage));
}
- toast.success(t('vault.successfully_updated_collateral'));
setSubmitStatus(STATUSES.RESOLVED);
handleClose();
- } catch (error) {
- toast.error(error.message);
+ } catch (error: any) {
+ transaction.reject(error);
handleError(error);
setSubmitStatus(STATUSES.REJECTED);
}
@@ -271,12 +267,9 @@ const UpdateCollateralModal = ({
};
return (
-
-
-
- {collateralUpdateStatusText}
-
-
+
+ {collateralUpdateStatusText}
+
-
-
+
+
);
};
diff --git a/src/pages/Vaults/Vault/components/CollateralForm/CollateralForm.styles.tsx b/src/pages/Vaults/Vault/components/CollateralForm/CollateralForm.styles.tsx
deleted file mode 100644
index c0591711d6..0000000000
--- a/src/pages/Vaults/Vault/components/CollateralForm/CollateralForm.styles.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import styled from 'styled-components';
-
-import { H2, theme } from '@/component-library';
-
-const StyledDl = styled.dl`
- display: flex;
- flex-direction: column;
- gap: ${theme.spacing.spacing2};
-`;
-
-const StyledDItem = styled.div`
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: ${theme.spacing.spacing2};
-`;
-
-const StyledDt = styled.dt`
- font-size: ${theme.text.xs};
- line-height: ${theme.lineHeight.base};
- color: ${theme.colors.textTertiary};
-`;
-
-const StyledDd = styled.dd`
- font-size: ${theme.text.xs};
- line-height: ${theme.lineHeight.base};
-`;
-
-const StyledTitle = styled(H2)`
- font-size: ${theme.text.base};
- line-height: ${theme.lineHeight.base};
- color: #d57b33;
- padding: ${theme.spacing.spacing3};
- border-bottom: 2px solid #feca2f;
- text-align: center;
-`;
-
-const StyledHr = styled.hr`
- border: 0;
- border-bottom: ${theme.border.default};
- margin: ${theme.spacing.spacing4} 0;
-`;
-
-export { StyledDd, StyledDItem, StyledDl, StyledDt, StyledHr, StyledTitle };
diff --git a/src/pages/Vaults/Vault/components/CollateralForm/CollateralForm.tsx b/src/pages/Vaults/Vault/components/CollateralForm/CollateralForm.tsx
deleted file mode 100644
index 7f00b8be13..0000000000
--- a/src/pages/Vaults/Vault/components/CollateralForm/CollateralForm.tsx
+++ /dev/null
@@ -1,312 +0,0 @@
-import { CollateralCurrencyExt, CurrencyExt, newMonetaryAmount } from '@interlay/interbtc-api';
-import { MonetaryAmount } from '@interlay/monetary-js';
-import { useId } from '@react-aria/utils';
-import Big from 'big.js';
-import { FormHTMLAttributes, useEffect, useState } from 'react';
-import { useErrorHandler } from 'react-error-boundary';
-import { useForm } from 'react-hook-form';
-import { useTranslation } from 'react-i18next';
-import { useQuery } from 'react-query';
-import { useSelector } from 'react-redux';
-import { useParams } from 'react-router';
-
-import { StoreType } from '@/common/types/util.types';
-import {
- convertMonetaryAmountToValueInUSD,
- displayMonetaryAmount,
- displayMonetaryAmountInUSDFormat,
- formatNumber,
- formatUSD
-} from '@/common/utils/utils';
-import { CTA, Span, Stack, TokenInput } from '@/component-library';
-import genericFetcher, { GENERIC_FETCHER } from '@/services/fetchers/generic-fetcher';
-import { URL_PARAMETERS } from '@/utils/constants/links';
-import { submitExtrinsic, submitExtrinsicPromise } from '@/utils/helpers/extrinsic';
-import { getTokenPrice } from '@/utils/helpers/prices';
-import { useGetPrices } from '@/utils/hooks/api/use-get-prices';
-import { VaultData } from '@/utils/hooks/api/vaults/get-vault-data';
-
-import { CollateralActions, CollateralStatusRanges } from '../../types';
-import { StyledDd, StyledDItem, StyledDl, StyledDt, StyledHr, StyledTitle } from './CollateralForm.styles';
-
-// const getCollateralStatusLabel = (status: CollateralStatus) => {
-// switch (status) {
-// case 'error':
-// return '(High Risk)';
-// case 'warning':
-// return '(Medium Risk)';
-// case 'success':
-// return '(Low Risk)';
-// }
-// };
-
-const getCollateralTokenAmount = (
- vaultCollateral: Big,
- inputCollateral: MonetaryAmount,
- token: CurrencyExt,
- collateralAction: CollateralActions
-) => {
- let amount = newMonetaryAmount(vaultCollateral, token, true) as MonetaryAmount;
-
- switch (collateralAction) {
- case 'deposit': {
- amount = amount.add(inputCollateral);
- break;
- }
- case 'withdraw': {
- amount = amount.sub(inputCollateral);
- break;
- }
- }
-
- return amount;
-};
-
-const DEPOSIT_COLLATERAL_AMOUNT = 'deposit-collateral-amount';
-const WITHDRAW_COLLATERAL_AMOUNT = 'withdraw-collateral-amount';
-
-type CollateralFormData = {
- [DEPOSIT_COLLATERAL_AMOUNT]?: string;
- [WITHDRAW_COLLATERAL_AMOUNT]?: string;
-};
-
-const collateralInputId: Record = {
- deposit: DEPOSIT_COLLATERAL_AMOUNT,
- withdraw: WITHDRAW_COLLATERAL_AMOUNT
-};
-
-type Props = {
- collateral: VaultData['collateral'];
- collateralToken: CurrencyExt;
- variant?: CollateralActions;
- onSubmit?: () => void;
- ranges: CollateralStatusRanges;
-};
-
-type NativeAttrs = Omit, keyof Props | 'children'>;
-
-type CollateralFormProps = Props & NativeAttrs;
-
-const CollateralForm = ({
- variant = 'deposit',
- onSubmit,
- collateral,
- collateralToken,
- ...props
-}: CollateralFormProps): JSX.Element => {
- const { t } = useTranslation();
- const { bridgeLoaded } = useSelector((state: StoreType) => state.general);
- const { [URL_PARAMETERS.VAULT.ACCOUNT]: vaultAddress } = useParams>();
- const [isSubmitting, setIsSubmitting] = useState(false);
- const prices = useGetPrices();
- const { register, handleSubmit: h, watch } = useForm({
- mode: 'onChange'
- });
- // const [score, setScore] = useState(0);
-
- const tokenInputId = collateralInputId[variant];
- const inputCollateral = watch(tokenInputId) || '0';
- const inputCollateralAmount = newMonetaryAmount(
- inputCollateral,
- collateralToken,
- true
- ) as MonetaryAmount;
-
- const {
- isIdle: requiredCollateralTokenAmountIdle,
- isLoading: requiredCollateralTokenAmountLoading,
- data: requiredCollateralTokenAmount,
- error: requiredCollateralTokenAmountError
- } = useQuery, Error>(
- [GENERIC_FETCHER, 'vaults', 'getRequiredCollateralForVault', vaultAddress, collateralToken],
- genericFetcher>(),
- {
- enabled: !!bridgeLoaded
- }
- );
- useErrorHandler(requiredCollateralTokenAmountError);
-
- const collateralTokenAmount = getCollateralTokenAmount(
- collateral.amount,
- inputCollateralAmount,
- collateralToken,
- variant
- );
-
- const { isLoading: isGetCollateralizationLoading, data: unparsedScore, error } = useQuery(
- [GENERIC_FETCHER, 'vaults', 'getVaultCollateralization', vaultAddress, collateralToken, collateralTokenAmount],
- genericFetcher(),
- {
- enabled: bridgeLoaded
- // TODO: add hasLockedBTC
- // && hasLockedBTC
- }
- );
- useErrorHandler(error);
-
- useEffect(() => {
- if (!isGetCollateralizationLoading) {
- // setScore(unparsedScore?.toNumber() ?? 0);
- }
- }, [isGetCollateralizationLoading, unparsedScore]);
-
- const handleSubmit = async (data: CollateralFormData) => {
- if (!bridgeLoaded) return;
- onSubmit?.();
- setIsSubmitting(true);
-
- try {
- const collateralTokenAmount = newMonetaryAmount(
- data[tokenInputId] || '0',
- collateralToken,
- true
- ) as MonetaryAmount;
-
- switch (variant) {
- case 'deposit': {
- await submitExtrinsic(window.bridge.vaults.depositCollateral(collateralTokenAmount));
- break;
- }
- case 'withdraw': {
- await submitExtrinsicPromise(window.bridge.vaults.withdrawCollateral(collateralTokenAmount));
- break;
- }
- }
-
- // TODO: state changes
-
- // const balanceLockedCollateral = (await window.bridge.tokens.balance(collateralToken, vaultAddress)).reserved;
- // dispatch(updateCollateralAction(balanceLockedCollateral as MonetaryAmount));
-
- // if (vaultCollateralization === undefined) {
- // dispatch(updateCollateralizationAction('∞'));
- // } else {
- // // The vault API returns collateralization as a regular number rather than a percentage
- // const strVaultCollateralizationPercentage = vaultCollateralization.mul(100).toString();
- // dispatch(updateCollateralizationAction(strVaultCollateralizationPercentage));
- // }
-
- // toast.success(t('vault.successfully_updated_collateral'));
- // setSubmitStatus(STATUSES.RESOLVED);
- // onClose();
- } catch (error) {
- // toast.error(error.message);
- // handleError(error);
- setIsSubmitting(false);
- }
- };
-
- const validateCollateralTokenAmount = (value?: string): string | undefined => {
- const collateralTokenAmount = newMonetaryAmount(value || '0', collateralToken, true);
-
- // Collateral update only allowed if above required collateral
- if (variant === 'withdraw' && requiredCollateralTokenAmount) {
- const maxWithdrawableCollateralTokenAmount = collateralTokenAmount.sub(requiredCollateralTokenAmount);
-
- return collateralTokenAmount.gt(maxWithdrawableCollateralTokenAmount)
- ? t('vault.collateral_below_threshold')
- : undefined;
- }
-
- if (collateralTokenAmount.lte(newMonetaryAmount(0, collateralToken, true))) {
- return t('vault.collateral_higher_than_0');
- }
-
- // Represents being less than 1 Planck
- if (collateralTokenAmount.toBig(0).lte(1)) {
- return 'Please enter an amount greater than 1 Planck';
- }
-
- // if (collateralBalance && collateralTokenAmount.gt(collateralBalance.transferable)) {
- // return t(`Must be less than ${collateralToken.ticker} balance!`);
- // }
-
- if (!bridgeLoaded) {
- return 'Bridge must be loaded!';
- }
-
- return undefined;
- };
-
- const collateralUSDAmount = getTokenPrice(prices, collateralToken.ticker)?.usd;
- const isMinCollateralLoading = requiredCollateralTokenAmountIdle || requiredCollateralTokenAmountLoading;
-
- const titleId = useId();
- const title = variant === 'deposit' ? 'Deposit Collateral' : 'Withdraw Collateral';
-
- // TODO: handle infinity collateralization in form
- // const collateralStatus = getCollateralStatus(score, ranges, false);
-
- return (
-
- );
-};
-
-export { CollateralForm };
-export type { CollateralFormProps };
diff --git a/src/pages/Vaults/Vault/components/CollateralForm/index.tsx b/src/pages/Vaults/Vault/components/CollateralForm/index.tsx
deleted file mode 100644
index 1e29b6d0c5..0000000000
--- a/src/pages/Vaults/Vault/components/CollateralForm/index.tsx
+++ /dev/null
@@ -1,2 +0,0 @@
-export type { CollateralFormProps } from './CollateralForm';
-export { CollateralForm } from './CollateralForm';
diff --git a/src/pages/Vaults/Vault/components/Rewards/Rewards.tsx b/src/pages/Vaults/Vault/components/Rewards/Rewards.tsx
index 2b8cedbe6b..d372470202 100644
--- a/src/pages/Vaults/Vault/components/Rewards/Rewards.tsx
+++ b/src/pages/Vaults/Vault/components/Rewards/Rewards.tsx
@@ -1,13 +1,11 @@
import { CollateralCurrencyExt, newVaultId, WrappedCurrency, WrappedIdLiteral } from '@interlay/interbtc-api';
import Big from 'big.js';
import { useQueryClient } from 'react-query';
-import { toast } from 'react-toastify';
import { formatNumber, formatUSD } from '@/common/utils/utils';
import { CardProps } from '@/component-library';
import { LoadingSpinner } from '@/component-library/LoadingSpinner';
import { GOVERNANCE_TOKEN_SYMBOL, WRAPPED_TOKEN } from '@/config/relay-chains';
-import ErrorModal from '@/legacy-components/ErrorModal';
import { ZERO_GOVERNANCE_TOKEN_AMOUNT } from '@/utils/constants/currency';
import { VaultData } from '@/utils/hooks/api/vaults/get-vault-data';
import { Transaction, useTransaction } from '@/utils/hooks/transaction';
@@ -51,7 +49,6 @@ const Rewards = ({
const transaction = useTransaction(Transaction.REWARDS_WITHDRAW, {
onSuccess: () => {
queryClient.invalidateQueries(['vaultsOverview', vaultAddress, collateralToken.ticker]);
- toast.success('Your rewards were successfully withdrawn.');
}
});
@@ -91,14 +88,6 @@ const Rewards = ({
Withdraw all rewards
)}
- {transaction.isError && (
- transaction.reset()}
- title='Error'
- description={transaction.error?.message || ''}
- />
- )}
);
diff --git a/src/pages/Vaults/Vault/components/index.tsx b/src/pages/Vaults/Vault/components/index.tsx
index e85ac5e4b4..eefb92e3b4 100644
--- a/src/pages/Vaults/Vault/components/index.tsx
+++ b/src/pages/Vaults/Vault/components/index.tsx
@@ -1,4 +1,3 @@
-import { CollateralForm, CollateralFormProps } from './CollateralForm';
import { InsightListItem, InsightsList, InsightsListProps } from './InsightsList';
import { PageTitle, PageTitleProps } from './PageTitle';
import { Rewards, RewardsProps } from './Rewards';
@@ -6,9 +5,8 @@ import { TransactionHistory, TransactionHistoryProps } from './TransactionHistor
import { VaultCollateral, VaultCollateralProps } from './VaultCollateral';
import { VaultInfo, VaultInfoProps } from './VaultInfo';
-export { CollateralForm, InsightsList, PageTitle, Rewards, TransactionHistory, VaultCollateral, VaultInfo };
+export { InsightsList, PageTitle, Rewards, TransactionHistory, VaultCollateral, VaultInfo };
export type {
- CollateralFormProps,
InsightListItem,
InsightsListProps,
PageTitleProps,
diff --git a/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/DespositCollateralStep.tsx b/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/DespositCollateralStep.tsx
index 4fbe4efd89..62a3bc21fe 100644
--- a/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/DespositCollateralStep.tsx
+++ b/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/DespositCollateralStep.tsx
@@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next';
import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/common/utils/utils';
import { CTA, ModalBody, ModalDivider, ModalFooter, ModalHeader, Span, Stack, TokenInput } from '@/component-library';
import { GOVERNANCE_TOKEN } from '@/config/relay-chains';
-import ErrorModal from '@/legacy-components/ErrorModal';
import {
CREATE_VAULT_DEPOSIT_FIELD,
CreateVaultFormData,
@@ -38,7 +37,8 @@ const DepositCollateralStep = ({
const { collateral, fee, governance } = useDepositCollateral(collateralCurrency, minCollateralAmount);
const transaction = useTransaction(Transaction.VAULTS_REGISTER_NEW_COLLATERAL, {
- onSuccess: onSuccessfulDeposit
+ onSuccess: onSuccessfulDeposit,
+ showSuccessModal: false
});
const validationParams = {
@@ -108,14 +108,6 @@ const DepositCollateralStep = ({
- {transaction.isError && (
- transaction.reset()}
- title='Error'
- description={transaction.error?.message || ''}
- />
- )}
>
);
};
diff --git a/src/pages/Wallet/WalletOverview/components/AvailableAssetsTable/ActionsCell.tsx b/src/pages/Wallet/WalletOverview/components/AvailableAssetsTable/ActionsCell.tsx
index ca103cb82d..4eeb9f33c4 100644
--- a/src/pages/Wallet/WalletOverview/components/AvailableAssetsTable/ActionsCell.tsx
+++ b/src/pages/Wallet/WalletOverview/components/AvailableAssetsTable/ActionsCell.tsx
@@ -1,21 +1,16 @@
import { CurrencyExt } from '@interlay/interbtc-api';
import { useTranslation } from 'react-i18next';
-import { useMutation } from 'react-query';
import { useDispatch } from 'react-redux';
-import { toast } from 'react-toastify';
import { showBuyModal } from '@/common/actions/general.actions';
import { CTA, CTALink, CTAProps, Divider, Flex, theme } from '@/component-library';
import { useMediaQuery } from '@/component-library/utils/use-media-query';
import { WRAPPED_TOKEN } from '@/config/relay-chains';
import { PAGES, QUERY_PARAMETERS } from '@/utils/constants/links';
+import { Transaction, useTransaction } from '@/utils/hooks/transaction';
const queryString = require('query-string');
-const claimVesting = async () => {
- await window.bridge.api.tx.vesting.claim();
-};
-
type ActionsCellProps = {
currency: CurrencyExt;
isWrappedToken: boolean;
@@ -39,20 +34,9 @@ const ActionsCell = ({
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const isSmallMobile = useMediaQuery(theme.breakpoints.down('sm'));
- const handleClaimVestingSuccess = () => {
- toast.success('Successfully claimed vesting');
- };
-
- const handleClaimVestingError = (error: Error) => {
- toast.success(error);
- };
-
- const claimVestingMutation = useMutation(claimVesting, {
- onSuccess: handleClaimVestingSuccess,
- onError: handleClaimVestingError
- });
+ const vestingClaimTransaction = useTransaction(Transaction.VESTING_CLAIM);
- const handlePressClaimVesting = () => claimVestingMutation.mutate();
+ const handlePressClaimVesting = () => vestingClaimTransaction.execute();
const handlePressBuyGovernance = () => dispatch(showBuyModal(true));
diff --git a/src/parts/Topbar/index.tsx b/src/parts/Topbar/index.tsx
index b287a4420d..4b7aa388e4 100644
--- a/src/parts/Topbar/index.tsx
+++ b/src/parts/Topbar/index.tsx
@@ -9,7 +9,7 @@ import { toast } from 'react-toastify';
import { showAccountModalAction, showSignTermsModalAction } from '@/common/actions/general.actions';
import { StoreType } from '@/common/types/util.types';
-import { FundWallet } from '@/components';
+import { FundWallet, NotificationsPopover } from '@/components';
import { AuthModal, SignTermsModal } from '@/components/AuthModal';
import { ACCOUNT_ID_TYPE_NAME } from '@/config/general';
import { GOVERNANCE_TOKEN } from '@/config/relay-chains';
@@ -21,6 +21,7 @@ import Tokens from '@/legacy-components/Tokens';
import InterlayLink from '@/legacy-components/UI/InterlayLink';
import { KeyringPair, useSubstrate, useSubstrateSecureState } from '@/lib/substrate';
import { BitcoinNetwork } from '@/types/bitcoin';
+import { useNotifications } from '@/utils/context/Notifications';
import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances';
import { FeatureFlags, useFeatureFlag } from '@/utils/hooks/use-feature-flag';
import { useSignMessage } from '@/utils/hooks/use-sign-message';
@@ -38,6 +39,7 @@ const Topbar = (): JSX.Element => {
const isBanxaEnabled = useFeatureFlag(FeatureFlags.BANXA);
const { setSelectedAccount, removeSelectedAccount } = useSubstrate();
const { selectProps } = useSignMessage();
+ const { list } = useNotifications();
const kintBalanceIsZero = getAvailableBalance('KINT')?.isZero();
@@ -47,6 +49,7 @@ const Topbar = (): JSX.Element => {
try {
const receiverId = window.bridge.api.createType(ACCOUNT_ID_TYPE_NAME, selectedAccount.address);
await window.faucet.fundAccount(receiverId, GOVERNANCE_TOKEN);
+ // TODO: show new notification
toast.success('Your account has been funded.');
} catch (error) {
toast.error(`Funding failed. ${error.message}`);
@@ -134,6 +137,7 @@ const Topbar = (): JSX.Element => {
>
)}
+
{accountLabel}
diff --git a/src/utils/constants/links.ts b/src/utils/constants/links.ts
index c6cd038371..7a62ca51f9 100644
--- a/src/utils/constants/links.ts
+++ b/src/utils/constants/links.ts
@@ -1,4 +1,5 @@
import { BANXA_LINK } from '@/config/links';
+import { SUBSCAN_LINK } from '@/config/relay-chains';
const URL_PARAMETERS = Object.freeze({
VAULT: {
@@ -35,8 +36,24 @@ const PAGES = Object.freeze({
WALLET: '/wallet'
});
+const EXTERNAL_URL_PARAMETERS = Object.freeze({
+ SUBSCAN: {
+ BLOCK: {
+ HASH: 'hash'
+ },
+ ACCOUNT: {
+ ADDRESS: 'address'
+ }
+ }
+});
+
const EXTERNAL_PAGES = Object.freeze({
- BANXA: `${BANXA_LINK}`
+ BANXA: `${BANXA_LINK}`,
+ SUBSCAN: {
+ BLOCKS: `${SUBSCAN_LINK}/block`,
+ BLOCK: `${SUBSCAN_LINK}/block/:${EXTERNAL_URL_PARAMETERS.SUBSCAN.BLOCK.HASH}`,
+ ACCOUNT: `${SUBSCAN_LINK}/account/:${EXTERNAL_URL_PARAMETERS.SUBSCAN.ACCOUNT.ADDRESS}`
+ }
});
const QUERY_PARAMETERS = Object.freeze({
@@ -60,4 +77,4 @@ const EXTERNAL_QUERY_PARAMETERS = Object.freeze({
}
});
-export { EXTERNAL_PAGES, EXTERNAL_QUERY_PARAMETERS, PAGES, QUERY_PARAMETERS, URL_PARAMETERS };
+export { EXTERNAL_PAGES, EXTERNAL_QUERY_PARAMETERS, EXTERNAL_URL_PARAMETERS, PAGES, QUERY_PARAMETERS, URL_PARAMETERS };
diff --git a/src/utils/context/Notifications.tsx b/src/utils/context/Notifications.tsx
new file mode 100644
index 0000000000..3dd7f48752
--- /dev/null
+++ b/src/utils/context/Notifications.tsx
@@ -0,0 +1,141 @@
+import { Overlay } from '@react-aria/overlays';
+import { mergeProps } from '@react-aria/utils';
+import React, { useEffect, useRef } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { Id as NotificationId, toast, ToastOptions } from 'react-toastify';
+
+import { addNotification } from '@/common/actions/general.actions';
+import { Notification, StoreType } from '@/common/types/util.types';
+import { ToastContainer, TransactionToast, TransactionToastProps } from '@/components';
+
+import { useWallet } from '../hooks/use-wallet';
+
+// Allows the introduction of diferent
+// notifications toast beyond transactions
+// i.e. claiming faucet funds or sign T&Cs
+enum NotificationToast {
+ TRANSACTION
+}
+
+type NotificationToastAction = { type: NotificationToast.TRANSACTION; props: TransactionToastProps };
+
+const toastComponentMap = { [NotificationToast.TRANSACTION]: TransactionToast };
+
+type ToastMap = Record;
+
+type NotifcationInfo = {
+ // NotificationId - toast is on the screen
+ // null - toast has been dismissed
+ // undefined - toast never existed
+ id: NotificationId | null | undefined;
+ hasRendered: boolean;
+ isOnScreen: boolean;
+};
+
+type NotificationOptions = ToastOptions;
+
+const toastConfig: NotificationOptions = {
+ closeButton: false,
+ autoClose: false,
+ closeOnClick: false,
+ draggable: false,
+ icon: false
+};
+
+type NotificationsConfig = {
+ list: Notification[];
+ // gets notification meta data
+ get: (id: number | string) => NotifcationInfo;
+ // adds to the redux notifications list
+ add: (notification: Omit) => void;
+ // renders toast
+ show: (id: number | string, action: NotificationToastAction) => void;
+ // removes toast from the screen
+ dismiss: (id: number | string) => void;
+};
+
+const defaultContext: NotificationsConfig = {} as NotificationsConfig;
+
+const NotificationsContext = React.createContext(defaultContext);
+
+const useNotifications = (): NotificationsConfig => React.useContext(NotificationsContext);
+
+const NotificationsProvider: React.FC = ({ children }) => {
+ const toastContainerRef = useRef(null);
+
+ const dispatch = useDispatch();
+
+ const { account } = useWallet();
+ const { notifications } = useSelector((state: StoreType) => state.general);
+
+ const idsMap = useRef({});
+
+ const get = (id: number | string) => {
+ const toastId = idsMap.current[id];
+
+ return {
+ id: toastId,
+ hasRendered: toastId === null,
+ isOnScreen: !!toastId
+ };
+ };
+
+ const add = (notification: Omit) =>
+ dispatch(addNotification(account?.toString() as string, { ...notification, date: new Date() }));
+
+ const show = (id: number | string, action: NotificationToastAction) => {
+ const toastInfo = get(id);
+
+ const ToastComponent = toastComponentMap[action.type];
+
+ const onDismiss = () => dismiss(id);
+
+ const render = ;
+
+ if (toastInfo.id) {
+ return toast.update(toastInfo.id, { render, ...toastConfig });
+ }
+
+ const newToastId = toast(render, toastConfig);
+ idsMap.current[id] = newToastId;
+ };
+
+ const dismiss = (id: number | string) => {
+ const toasInfo = get(id);
+
+ if (!toasInfo.id) return;
+
+ toast.dismiss(toasInfo.id);
+ // Set to null, meaning that this toast should never appear again, even if updated
+ idsMap.current[id] = null;
+ };
+
+ // Applying data-react-aria-top-layer="true" makes react-aria overlay consider the element as a visible element.
+ // Non-visible elements get forced with aria-hidden=true.
+ // Check: https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/overlays/src/ariaHideOutside.ts#L32
+ useEffect(() => {
+ if (!toastContainerRef.current) return;
+
+ toastContainerRef.current.setAttribute('data-react-aria-top-layer', 'true');
+ }, [toastContainerRef]);
+
+ return (
+
+ {children}
+
+
+
+
+ );
+};
+
+export { NotificationsContext, NotificationsProvider, NotificationToast, useNotifications };
+export type { NotificationToastAction };
diff --git a/src/utils/hooks/transaction/extrinsics/extrinsics.ts b/src/utils/hooks/transaction/extrinsics/extrinsics.ts
new file mode 100644
index 0000000000..cf63d868c9
--- /dev/null
+++ b/src/utils/hooks/transaction/extrinsics/extrinsics.ts
@@ -0,0 +1,46 @@
+import { ExtrinsicData } from '@interlay/interbtc-api';
+import { ExtrinsicStatus } from '@polkadot/types/interfaces';
+
+import { Transaction, TransactionActions } from '../types';
+import { getLibExtrinsic } from './lib';
+import { getXCMExtrinsic } from './xcm';
+
+/**
+ * SUMMARY: Maps each transaction to the correct lib call,
+ * while maintaining a safe-type check.
+ * HOW TO ADD NEW TRANSACTION: find the correct module to add the transaction
+ * in the types folder. In case you are adding a new type to the loans modules, go
+ * to types/loans and add your new transaction as an action. This actions needs to also be added to the
+ * types/index TransactionActions type. After that, you should be able to add it to the function.
+ * @param {TransactionActions} params contains the type of transaction and
+ * the related args to call the mapped lib call
+ * @return {Promise} every transaction return an extrinsic
+ */
+const getExtrinsic = async (params: TransactionActions): Promise => {
+ switch (params.type) {
+ case Transaction.XCM_TRANSFER:
+ return getXCMExtrinsic(params);
+ default:
+ return getLibExtrinsic(params);
+ }
+};
+
+/**
+ * The status where we want to be notified on the transaction completion
+ * @param {Transaction} type type of transaction
+ * @return {ExtrinsicStatus.type} transaction status
+ */
+const getStatus = (type: Transaction): ExtrinsicStatus['type'] => {
+ switch (type) {
+ // When requesting a replace, wait for the finalized event because we cannot revert BTC transactions.
+ // For more details see: https://github.com/interlay/interbtc-api/pull/373#issuecomment-1058949000
+ case Transaction.ISSUE_REQUEST:
+ case Transaction.REDEEM_REQUEST:
+ case Transaction.REPLACE_REQUEST:
+ return 'Finalized';
+ default:
+ return 'InBlock';
+ }
+};
+
+export { getExtrinsic, getStatus };
diff --git a/src/utils/hooks/transaction/extrinsics/index.ts b/src/utils/hooks/transaction/extrinsics/index.ts
new file mode 100644
index 0000000000..ff986fb28c
--- /dev/null
+++ b/src/utils/hooks/transaction/extrinsics/index.ts
@@ -0,0 +1 @@
+export { getExtrinsic, getStatus } from './extrinsics';
diff --git a/src/utils/hooks/transaction/utils/extrinsic.ts b/src/utils/hooks/transaction/extrinsics/lib.ts
similarity index 70%
rename from src/utils/hooks/transaction/utils/extrinsic.ts
rename to src/utils/hooks/transaction/extrinsics/lib.ts
index 23346819db..0d2b90a727 100644
--- a/src/utils/hooks/transaction/utils/extrinsic.ts
+++ b/src/utils/hooks/transaction/extrinsics/lib.ts
@@ -1,20 +1,8 @@
import { ExtrinsicData } from '@interlay/interbtc-api';
-import { ExtrinsicStatus } from '@polkadot/types/interfaces';
-import { Transaction, TransactionActions } from '../types';
+import { LibActions, Transaction } from '../types';
-/**
- * SUMMARY: Maps each transaction to the correct lib call,
- * while maintaining a safe-type check.
- * HOW TO ADD NEW TRANSACTION: find the correct module to add the transaction
- * in the types folder. In case you are adding a new type to the loans modules, go
- * to types/loans and add your new transaction as an action. This actions needs to also be added to the
- * types/index TransactionActions type. After that, you should be able to add it to the function.
- * @param {TransactionActions} params contains the type of transaction and
- * the related args to call the mapped lib call
- * @return {Promise} every transaction return an extrinsic
- */
-const getExtrinsic = async (params: TransactionActions): Promise => {
+const getLibExtrinsic = async (params: LibActions): Promise => {
switch (params.type) {
/* START - AMM */
case Transaction.AMM_SWAP:
@@ -74,18 +62,19 @@ const getExtrinsic = async (params: TransactionActions): Promise
return window.bridge.loans.enableAsCollateral(...params.args);
/* END - LOANS */
- /* START - LOANS */
+ /* START - VAULTS */
case Transaction.VAULTS_DEPOSIT_COLLATERAL:
return window.bridge.vaults.depositCollateral(...params.args);
case Transaction.VAULTS_WITHDRAW_COLLATERAL:
return window.bridge.vaults.withdrawCollateral(...params.args);
case Transaction.VAULTS_REGISTER_NEW_COLLATERAL:
return window.bridge.vaults.registerNewCollateralVault(...params.args);
+ /* END - VAULTS */
+
/* START - REWARDS */
case Transaction.REWARDS_WITHDRAW:
return window.bridge.rewards.withdrawRewards(...params.args);
/* START - REWARDS */
- /* END - LOANS */
/* START - ESCROW */
case Transaction.ESCROW_CREATE_LOCK:
@@ -109,25 +98,12 @@ const getExtrinsic = async (params: TransactionActions): Promise
return { extrinsic: batch };
}
/* END - ESCROW */
- }
-};
-/**
- * The status where we want to be notified on the transaction completion
- * @param {Transaction} type type of transaction
- * @return {ExtrinsicStatus.type} transaction status
- */
-const getStatus = (type: Transaction): ExtrinsicStatus['type'] => {
- switch (type) {
- // When requesting a replace, wait for the finalized event because we cannot revert BTC transactions.
- // For more details see: https://github.com/interlay/interbtc-api/pull/373#issuecomment-1058949000
- case Transaction.ISSUE_REQUEST:
- case Transaction.REDEEM_REQUEST:
- case Transaction.REPLACE_REQUEST:
- return 'Finalized';
- default:
- return 'InBlock';
+ /* START - VESTING */
+ case Transaction.VESTING_CLAIM:
+ return { extrinsic: window.bridge.api.tx.vesting.claim() };
+ /* END - VESTING */
}
};
-export { getExtrinsic, getStatus };
+export { getLibExtrinsic };
diff --git a/src/utils/hooks/transaction/extrinsics/xcm.ts b/src/utils/hooks/transaction/extrinsics/xcm.ts
new file mode 100644
index 0000000000..785369df31
--- /dev/null
+++ b/src/utils/hooks/transaction/extrinsics/xcm.ts
@@ -0,0 +1,27 @@
+import { FixedPointNumber } from '@acala-network/sdk-core';
+import { CrossChainTransferParams } from '@interlay/bridge';
+import { ExtrinsicData } from '@interlay/interbtc-api';
+
+import { Transaction } from '../types';
+import { XCMActions } from '../types/xcm';
+
+const getXCMExtrinsic = async (params: XCMActions): Promise => {
+ switch (params.type) {
+ case Transaction.XCM_TRANSFER: {
+ const [adapter, , toChain, address, transferAmount] = params.args;
+
+ const transferAmountString = transferAmount.toString(true);
+ const transferAmountDecimals = transferAmount.currency.decimals;
+ const tx = adapter.createTx({
+ amount: FixedPointNumber.fromInner(transferAmountString, transferAmountDecimals),
+ to: toChain,
+ token: transferAmount.currency.ticker,
+ address
+ } as CrossChainTransferParams);
+
+ return { extrinsic: tx };
+ }
+ }
+};
+
+export { getXCMExtrinsic };
diff --git a/src/utils/hooks/transaction/types/index.ts b/src/utils/hooks/transaction/types/index.ts
index 538f820678..81d43097a0 100644
--- a/src/utils/hooks/transaction/types/index.ts
+++ b/src/utils/hooks/transaction/types/index.ts
@@ -9,6 +9,8 @@ import { ReplaceActions } from './replace';
import { RewardsActions } from './rewards';
import { TokensActions } from './tokens';
import { VaultsActions } from './vaults';
+import { VestingActions } from './vesting';
+import { XCMActions } from './xcm';
enum Transaction {
// Issue
@@ -29,6 +31,8 @@ enum Transaction {
ESCROW_WITHDRAW = 'ESCROW_WITHDRAW',
// Tokens
TOKENS_TRANSFER = 'TOKENS_TRANSFER',
+ // XCM
+ XCM_TRANSFER = 'XCM_TRANSFER',
// Vaults
VAULTS_DEPOSIT_COLLATERAL = 'VAULTS_DEPOSIT_COLLATERAL',
VAULTS_WITHDRAW_COLLATERAL = 'VAULTS_WITHDRAW_COLLATERAL',
@@ -49,7 +53,11 @@ enum Transaction {
AMM_SWAP = 'AMM_SWAP',
AMM_ADD_LIQUIDITY = 'AMM_ADD_LIQUIDITY',
AMM_REMOVE_LIQUIDITY = 'AMM_REMOVE_LIQUIDITY',
- AMM_CLAIM_REWARDS = 'AMM_CLAIM_REWARDS'
+ AMM_CLAIM_REWARDS = 'AMM_CLAIM_REWARDS',
+ // Vesting
+ VESTING_CLAIM = 'VESTING_CLAIM',
+ // Faucet
+ FAUCET_FUND_WALLET = 'FAUCET_FUND_WALLET'
}
type TransactionEvents = {
@@ -59,10 +67,11 @@ type TransactionEvents = {
interface TransactionAction {
accountAddress: string;
events: TransactionEvents;
+ timestamp: number;
customStatus?: ExtrinsicStatus['type'];
}
-type TransactionActions =
+type LibActions =
| EscrowActions
| IssueActions
| RedeemActions
@@ -71,9 +80,19 @@ type TransactionActions =
| LoansActions
| AMMActions
| VaultsActions
- | RewardsActions;
+ | RewardsActions
+ | VestingActions;
+
+type TransactionActions = XCMActions | LibActions;
type TransactionArgs = Extract['args'];
-export { Transaction };
-export type { TransactionAction, TransactionActions, TransactionArgs, TransactionEvents };
+enum TransactionStatus {
+ CONFIRM,
+ SUBMITTING,
+ SUCCESS,
+ ERROR
+}
+
+export { Transaction, TransactionStatus };
+export type { LibActions, TransactionAction, TransactionActions, TransactionArgs, TransactionEvents, XCMActions };
diff --git a/src/utils/hooks/transaction/types/vesting.ts b/src/utils/hooks/transaction/types/vesting.ts
new file mode 100644
index 0000000000..ab4ce9a00e
--- /dev/null
+++ b/src/utils/hooks/transaction/types/vesting.ts
@@ -0,0 +1,13 @@
+import { InterBtcApi } from '@interlay/interbtc-api';
+
+import { Transaction } from '.';
+import { TransactionAction } from '.';
+
+interface VestingClaimAction extends TransactionAction {
+ type: Transaction.VESTING_CLAIM;
+ args: Parameters;
+}
+
+type VestingActions = VestingClaimAction;
+
+export type { VestingActions };
diff --git a/src/utils/hooks/transaction/types/xcm.ts b/src/utils/hooks/transaction/types/xcm.ts
new file mode 100644
index 0000000000..71b0276c11
--- /dev/null
+++ b/src/utils/hooks/transaction/types/xcm.ts
@@ -0,0 +1,21 @@
+import { ChainName } from '@interlay/bridge';
+import { BaseCrossChainAdapter } from '@interlay/bridge/build/base-chain-adapter';
+import { CurrencyExt } from '@interlay/interbtc-api';
+import { MonetaryAmount } from '@interlay/monetary-js';
+
+import { Transaction, TransactionAction } from '.';
+
+interface XCMTransferAction extends TransactionAction {
+ type: Transaction.XCM_TRANSFER;
+ args: [
+ adapter: BaseCrossChainAdapter,
+ fromChain: ChainName,
+ toChain: ChainName,
+ destinatary: string,
+ transferAmount: MonetaryAmount
+ ];
+}
+
+type XCMActions = XCMTransferAction;
+
+export type { XCMActions };
diff --git a/src/utils/hooks/transaction/use-transaction-notifications.tsx b/src/utils/hooks/transaction/use-transaction-notifications.tsx
new file mode 100644
index 0000000000..abcb7fda2e
--- /dev/null
+++ b/src/utils/hooks/transaction/use-transaction-notifications.tsx
@@ -0,0 +1,107 @@
+import { ISubmittableResult } from '@polkadot/types/types';
+import { useTranslation } from 'react-i18next';
+import { useDispatch } from 'react-redux';
+
+import { updateTransactionModal } from '@/common/actions/general.actions';
+import { TransactionModalData } from '@/common/types/util.types';
+import { EXTERNAL_PAGES, EXTERNAL_URL_PARAMETERS } from '@/utils/constants/links';
+import { NotificationToast, NotificationToastAction, useNotifications } from '@/utils/context/Notifications';
+
+import { TransactionActions, TransactionStatus } from './types';
+import { TransactionResult } from './use-transaction';
+import { getTransactionDescription } from './utils/description';
+
+type TransactionNotificationsOptions = {
+ showSuccessModal?: boolean;
+};
+
+type UseTransactionNotificationsResult = {
+ onReject: (error?: Error) => void;
+ mutationProps: {
+ onMutate: (variables: TransactionActions) => void;
+ onSigning: (variables: TransactionActions) => void;
+ onSuccess: (data: TransactionResult, variables: TransactionActions) => void;
+ onError: (error: Error, variables: TransactionActions, context: unknown) => void;
+ };
+};
+
+// Handles both transactions notifications and modal
+const useTransactionNotifications = ({
+ showSuccessModal = true
+}: TransactionNotificationsOptions): UseTransactionNotificationsResult => {
+ const { t } = useTranslation();
+
+ const notifications = useNotifications();
+
+ const dispatch = useDispatch();
+
+ const handleModalOrToast = (
+ status: TransactionStatus,
+ variables: TransactionActions,
+ data?: ISubmittableResult,
+ error?: Error
+ ) => {
+ const toastInfo = notifications.get(variables.timestamp);
+
+ const url =
+ data?.txHash &&
+ EXTERNAL_PAGES.SUBSCAN.BLOCK.replace(`:${EXTERNAL_URL_PARAMETERS.SUBSCAN.BLOCK.HASH}`, data.txHash.toString());
+
+ const description = getTransactionDescription(variables, status, t);
+
+ // Add notification to history if status is SUCCESS or ERROR
+ if (description && (status === TransactionStatus.SUCCESS || status === TransactionStatus.ERROR)) {
+ notifications.add({ description, status, url });
+ }
+
+ // If toast already rendered, it means that the user did already dismiss the transaction modal and the toast
+ if (toastInfo.hasRendered) return;
+
+ // creating or updating notification
+ if (toastInfo.isOnScreen) {
+ const toastAction: NotificationToastAction = {
+ type: NotificationToast.TRANSACTION,
+ props: {
+ variant: status,
+ url,
+ errorMessage: error?.message,
+ description
+ }
+ };
+
+ return notifications.show(variables.timestamp, toastAction);
+ }
+
+ // only reach here if the modal has not been dismissed
+ const modalData: TransactionModalData = {
+ url,
+ description,
+ variant: status,
+ errorMessage: error?.message,
+ timestamp: variables?.timestamp
+ };
+
+ const isModalOpen = status === TransactionStatus.SUCCESS ? showSuccessModal : true;
+
+ return dispatch(updateTransactionModal(isModalOpen, modalData));
+ };
+
+ const handleSuccess = (result: TransactionResult, variables: TransactionActions) => {
+ const status = result.status === 'error' ? TransactionStatus.ERROR : TransactionStatus.SUCCESS;
+
+ handleModalOrToast(status, variables, result.data, result.error);
+ };
+
+ return {
+ onReject: (error) =>
+ dispatch(updateTransactionModal(true, { variant: TransactionStatus.ERROR, errorMessage: error?.message })),
+ mutationProps: {
+ onMutate: (variables) => handleModalOrToast(TransactionStatus.CONFIRM, variables),
+ onSigning: (variables) => handleModalOrToast(TransactionStatus.SUBMITTING, variables),
+ onSuccess: (result, variables) => handleSuccess(result, variables),
+ onError: (error, variables) => handleModalOrToast(TransactionStatus.ERROR, variables, undefined, error)
+ }
+ };
+};
+
+export { useTransactionNotifications };
diff --git a/src/utils/hooks/transaction/use-transaction.ts b/src/utils/hooks/transaction/use-transaction.ts
index d18291f94c..3fa2cda32e 100644
--- a/src/utils/hooks/transaction/use-transaction.ts
+++ b/src/utils/hooks/transaction/use-transaction.ts
@@ -1,51 +1,62 @@
import { ExtrinsicStatus } from '@polkadot/types/interfaces';
import { ISubmittableResult } from '@polkadot/types/types';
-import { useCallback } from 'react';
+import { mergeProps } from '@react-aria/utils';
+import { useCallback, useState } from 'react';
import { MutationFunction, useMutation, UseMutationOptions, UseMutationResult } from 'react-query';
import { useSubstrate } from '@/lib/substrate';
+import { getExtrinsic, getStatus } from './extrinsics';
import { Transaction, TransactionActions, TransactionArgs } from './types';
-import { getExtrinsic, getStatus } from './utils/extrinsic';
+import { useTransactionNotifications } from './use-transaction-notifications';
import { submitTransaction } from './utils/submit';
-type UseTransactionOptions = Omit<
- UseMutationOptions,
- 'mutationFn'
-> & {
- customStatus?: ExtrinsicStatus['type'];
-};
+type TransactionResult = { status: 'success' | 'error'; data: ISubmittableResult; error?: Error };
// TODO: add feeEstimate and feeEstimateAsync
type ExecuteArgs = {
// Executes the transaction
execute(...args: TransactionArgs): void;
// Similar to execute but returns a promise which can be awaited.
- executeAsync(...args: TransactionArgs): Promise;
+ executeAsync(...args: TransactionArgs): Promise;
};
// TODO: add feeEstimate and feeEstimateAsync
type ExecuteTypeArgs = {
execute(type: D, ...args: TransactionArgs): void;
- executeAsync(type: D, ...args: TransactionArgs): Promise;
+ executeAsync(type: D, ...args: TransactionArgs): Promise;
};
-type InheritAttrs = Omit<
- UseMutationResult,
+type ExecuteFunctions = ExecuteArgs | ExecuteTypeArgs;
+
+type ReactQueryUseMutationResult = Omit<
+ UseMutationResult,
'mutate' | 'mutateAsync'
>;
-type UseTransactionResult = InheritAttrs & (ExecuteArgs | ExecuteTypeArgs);
+type UseTransactionResult = {
+ reject: (error?: Error) => void;
+ isSigned: boolean;
+} & ReactQueryUseMutationResult &
+ ExecuteFunctions;
-const mutateTransaction: MutationFunction = async (params) => {
+const mutateTransaction: MutationFunction = async (params) => {
const extrinsics = await getExtrinsic(params);
const expectedStatus = params.customStatus || getStatus(params.type);
return submitTransaction(window.bridge.api, params.accountAddress, extrinsics, expectedStatus, params.events);
};
+type UseTransactionOptions = Omit<
+ UseMutationOptions,
+ 'mutationFn'
+> & {
+ customStatus?: ExtrinsicStatus['type'];
+ onSigning?: (variables: TransactionActions) => void;
+ showSuccessModal?: boolean;
+};
+
// The three declared functions are use to infer types on diferent implementations
-// TODO: missing xcm transaction
function useTransaction(
type: T,
options?: UseTransactionOptions
@@ -59,13 +70,31 @@ function useTransaction(
): UseTransactionResult {
const { state } = useSubstrate();
- const hasOnlyOptions = typeof typeOrOptions !== 'string';
+ const [isSigned, setSigned] = useState(false);
+
+ const { showSuccessModal, customStatus, ...mutateOptions } =
+ (typeof typeOrOptions === 'string' ? options : typeOrOptions) || {};
- const { mutate, mutateAsync, ...transactionMutation } = useMutation(
- mutateTransaction,
- (hasOnlyOptions ? typeOrOptions : options) as UseTransactionOptions
+ const notifications = useTransactionNotifications({ showSuccessModal });
+
+ const handleMutate = () => setSigned(false);
+
+ const handleSigning = () => setSigned(true);
+
+ const handleError = (error: Error) => console.error(error.message);
+
+ const { onSigning, ...optionsProp } = mergeProps(
+ mutateOptions,
+ {
+ onMutate: handleMutate,
+ onSigning: handleSigning,
+ onError: handleError
+ },
+ notifications.mutationProps
);
+ const { mutate, mutateAsync, ...transactionMutation } = useMutation(mutateTransaction, optionsProp);
+
// Handles params for both type of implementations
const getParams = useCallback(
(args: Parameters['execute']>) => {
@@ -83,14 +112,21 @@ function useTransaction(
// Execution should only ran when authenticated
const accountAddress = state.selectedAccount?.address;
- // TODO: add event `onReady`
- return {
+ const variables = {
...params,
accountAddress,
- customStatus: options?.customStatus
+ timestamp: new Date().getTime(),
+ customStatus
} as TransactionActions;
+
+ return {
+ ...variables,
+ events: {
+ onReady: () => onSigning(variables)
+ }
+ };
},
- [options?.customStatus, state.selectedAccount?.address, typeOrOptions]
+ [onSigning, customStatus, state.selectedAccount?.address, typeOrOptions]
);
const handleExecute = useCallback(
@@ -111,12 +147,23 @@ function useTransaction(
[getParams, mutateAsync]
);
+ const handleReject = (error?: Error) => {
+ notifications.onReject(error);
+ setSigned(false);
+
+ if (error) {
+ console.error(error.message);
+ }
+ };
+
return {
...transactionMutation,
+ isSigned,
+ reject: handleReject,
execute: handleExecute,
executeAsync: handleExecuteAsync
};
}
export { useTransaction };
-export type { UseTransactionResult };
+export type { TransactionResult, UseTransactionResult };
diff --git a/src/utils/hooks/transaction/utils/description.ts b/src/utils/hooks/transaction/utils/description.ts
new file mode 100644
index 0000000000..f79c121332
--- /dev/null
+++ b/src/utils/hooks/transaction/utils/description.ts
@@ -0,0 +1,363 @@
+import { StringMap, TOptions } from 'i18next';
+import { TFunction } from 'react-i18next';
+
+import { shortAddress } from '@/common/utils/utils';
+
+import { Transaction, TransactionActions, TransactionStatus } from '../types';
+
+const getTranslationArgs = (
+ params: TransactionActions,
+ status: TransactionStatus
+): { key: string; args?: TOptions } | undefined => {
+ const isPast = status === TransactionStatus.SUCCESS;
+
+ switch (params.type) {
+ /* START - AMM */
+ case Transaction.AMM_SWAP: {
+ const [trade] = params.args;
+
+ return {
+ key: isPast ? 'transaction.swapped_to' : 'transaction.swapping_to',
+ args: {
+ fromAmount: trade.inputAmount.toHuman(),
+ fromCurrency: trade.inputAmount.currency.ticker,
+ toAmount: trade.outputAmount.toHuman(),
+ toCurrency: trade.outputAmount.currency.ticker
+ }
+ };
+ }
+ case Transaction.AMM_ADD_LIQUIDITY: {
+ const [, pool] = params.args;
+
+ return {
+ key: isPast ? 'transaction.added_liquidity_to_pool' : 'transaction.adding_liquidity_to_pool',
+ args: {
+ poolName: pool.lpToken.ticker
+ }
+ };
+ }
+ case Transaction.AMM_REMOVE_LIQUIDITY: {
+ const [, pool] = params.args;
+
+ return {
+ key: isPast ? 'transaction.removed_liquidity_from_pool' : 'transaction.removing_liquidity_from_pool',
+ args: {
+ poolName: pool.lpToken.ticker
+ }
+ };
+ }
+ case Transaction.AMM_CLAIM_REWARDS: {
+ return {
+ key: isPast ? 'transaction.claimed_pool_rewards' : 'transaction.claiming_pool_rewards'
+ };
+ }
+ /* END - AMM */
+
+ /* START - ISSUE */
+ case Transaction.ISSUE_REQUEST: {
+ const [amount] = params.args;
+
+ return {
+ key: isPast ? 'transaction.issued_amount' : 'transaction.issuing_amount',
+ args: {
+ amount: amount.toHuman(),
+ currency: amount.currency.ticker
+ }
+ };
+ }
+ case Transaction.ISSUE_EXECUTE: {
+ return {
+ key: isPast ? 'transaction.executed_issue' : 'transaction.executing_issue'
+ };
+ }
+ /* END - ISSUE */
+
+ /* START - REDEEM */
+ case Transaction.REDEEM_CANCEL: {
+ const [redeemId, isReimburse] = params.args;
+
+ const args = {
+ requestId: shortAddress(redeemId)
+ };
+
+ if (isReimburse) {
+ return {
+ key: isPast ? 'transaction.reimbersed_redeem_id' : 'transaction.reimbursing_redeem_id',
+ args
+ };
+ }
+
+ return {
+ key: isPast ? 'transaction.retried_redeem_id' : 'transaction.retrying_redeem_id',
+ args
+ };
+ }
+ case Transaction.REDEEM_BURN: {
+ const [amount] = params.args;
+
+ return {
+ key: isPast ? 'transaction.burned_amount' : 'transaction.burning_amount',
+ args: {
+ amount: amount.toHuman(),
+ currency: amount.currency.ticker
+ }
+ };
+ }
+ case Transaction.REDEEM_REQUEST: {
+ const [amount] = params.args;
+
+ return {
+ key: isPast ? 'transaction.redeemed_amount' : 'transaction.redeeming_amount',
+ args: {
+ amount: amount.toHuman(),
+ currency: amount.currency.ticker
+ }
+ };
+ }
+ /* END - REDEEM */
+
+ /* START - REPLACE */
+ case Transaction.REPLACE_REQUEST: {
+ return {
+ key: isPast ? 'transaction.requested_vault_replacement' : 'transaction.requesting_vault_replacement'
+ };
+ }
+ /* END - REPLACE */
+
+ /* START - TOKENS */
+ case Transaction.TOKENS_TRANSFER: {
+ const [destination, amount] = params.args;
+
+ return {
+ key: isPast ? 'transaction.transfered_amount_to_address' : 'transaction.transfering_amount_to_address',
+ args: {
+ amount: amount.toHuman(),
+ currency: amount.currency.ticker,
+ address: shortAddress(destination)
+ }
+ };
+ }
+ /* END - TOKENS */
+
+ /* START - XCM */
+ case Transaction.XCM_TRANSFER: {
+ const [, fromChain, toChain, , transferAmount] = params.args;
+
+ return {
+ key: isPast
+ ? 'transaction.transfered_amount_from_chain_to_chain'
+ : 'transaction.transfering_amount_from_chain_to_chain',
+ args: {
+ amount: transferAmount.toHuman(),
+ currency: transferAmount.currency.ticker,
+ fromChain: fromChain.toUpperCase(),
+ toChain: toChain.toUpperCase()
+ }
+ };
+ }
+ /* END - XCM */
+
+ /* START - LOANS */
+ case Transaction.LOANS_CLAIM_REWARDS: {
+ return {
+ key: isPast ? 'transaction.claimed_lending_rewards' : 'transaction.claiming_lending_rewards'
+ };
+ }
+ case Transaction.LOANS_BORROW: {
+ const [currency, amount] = params.args;
+
+ return {
+ key: isPast ? 'transaction.borrowed_amount' : 'transaction.borrowing_amount',
+ args: {
+ amount: amount.toHuman(),
+ currency: currency.ticker
+ }
+ };
+ }
+ case Transaction.LOANS_LEND: {
+ const [currency, amount] = params.args;
+
+ return {
+ key: isPast ? 'transaction.lent_amount' : 'transaction.lending_amount',
+ args: {
+ amount: amount.toHuman(),
+ currency: currency.ticker
+ }
+ };
+ }
+ case Transaction.LOANS_REPAY: {
+ const [currency, amount] = params.args;
+
+ return {
+ key: isPast ? 'transaction.repaid_amount' : 'transaction.repaying_amount',
+ args: {
+ amount: amount.toHuman(),
+ currency: currency.ticker
+ }
+ };
+ }
+ case Transaction.LOANS_REPAY_ALL: {
+ const [currency] = params.args;
+
+ return {
+ key: isPast ? 'transaction.repaid' : 'transaction.repaying',
+ args: {
+ currency: currency.ticker
+ }
+ };
+ }
+ case Transaction.LOANS_WITHDRAW: {
+ const [currency, amount] = params.args;
+
+ return {
+ key: isPast ? 'transaction.withdrew_amount' : 'transaction.withdrawing_amount',
+ args: {
+ amount: amount.toHuman(),
+ currency: currency.ticker
+ }
+ };
+ }
+ case Transaction.LOANS_WITHDRAW_ALL: {
+ const [currency] = params.args;
+
+ return {
+ key: isPast ? 'transaction.withdrew' : 'transaction.withdrawing',
+ args: {
+ currency: currency.ticker
+ }
+ };
+ }
+ case Transaction.LOANS_DISABLE_COLLATERAL: {
+ const [currency] = params.args;
+
+ return {
+ key: isPast ? 'transaction.disabled_loan_as_collateral' : 'transaction.disabling_loan_as_collateral',
+ args: {
+ currency: currency.ticker
+ }
+ };
+ }
+ case Transaction.LOANS_ENABLE_COLLATERAL: {
+ const [currency] = params.args;
+
+ return {
+ key: isPast ? 'transaction.enabled_loan_as_collateral' : 'transaction.enabling_loan_as_collateral',
+ args: {
+ currency: currency.ticker
+ }
+ };
+ }
+ /* END - LOANS */
+
+ /* START - VAULTS */
+ case Transaction.VAULTS_DEPOSIT_COLLATERAL: {
+ const [amount] = params.args;
+
+ return {
+ key: isPast ? 'transaction.deposited_amount_to_vault' : 'transaction.depositing_amount_to_vault',
+ args: {
+ amount: amount.toHuman(),
+ currency: amount.currency.ticker
+ }
+ };
+ }
+ case Transaction.VAULTS_WITHDRAW_COLLATERAL: {
+ const [amount] = params.args;
+
+ return {
+ key: isPast ? 'transaction.withdrew_amount_from_vault' : 'transaction.withdrawing_amount_from_vault',
+ args: {
+ amount: amount.toHuman(),
+ currency: amount.currency.ticker
+ }
+ };
+ }
+ case Transaction.VAULTS_REGISTER_NEW_COLLATERAL: {
+ const [collateralAmount] = params.args;
+
+ return {
+ key: isPast ? 'transaction.created_currency_vault' : 'transaction.creating_currency_vault',
+ args: {
+ currency: collateralAmount.currency.ticker
+ }
+ };
+ }
+ /* END - VAULTS */
+
+ /* START - REWARDS */
+ case Transaction.REWARDS_WITHDRAW: {
+ return {
+ key: isPast ? 'transaction.claimed_vault_rewards' : 'transaction.claiming_vault_rewards'
+ };
+ }
+ /* START - REWARDS */
+
+ /* START - ESCROW */
+ case Transaction.ESCROW_CREATE_LOCK: {
+ const [amount] = params.args;
+
+ return {
+ key: isPast ? 'transaction.staked_amount' : 'transaction.staking_amount',
+ args: {
+ amount: amount.toHuman(),
+ currency: amount.currency.ticker
+ }
+ };
+ }
+ case Transaction.ESCROW_INCREASE_LOCKED_AMOUNT: {
+ const [amount] = params.args;
+
+ return {
+ key: isPast ? 'transaction.added_amount_to_staked_amount' : 'transaction.adding_amount_to_staked_amount',
+ args: {
+ amount: amount.toHuman(),
+ currency: amount.currency.ticker
+ }
+ };
+ }
+ case Transaction.ESCROW_INCREASE_LOCKED_TIME: {
+ return {
+ key: isPast ? 'transaction.increased_stake_lock_time' : 'transaction.increasing_stake_lock_time'
+ };
+ }
+ case Transaction.ESCROW_WITHDRAW: {
+ return {
+ key: isPast ? 'transaction.withdrew_stake' : 'transaction.withdrawing_stake'
+ };
+ }
+ case Transaction.ESCROW_WITHDRAW_REWARDS: {
+ return {
+ key: isPast ? 'transaction.claimed_staking_rewards' : 'transaction.claiming_staking_rewards'
+ };
+ }
+ case Transaction.ESCROW_INCREASE_LOOKED_TIME_AND_AMOUNT: {
+ return {
+ key: isPast
+ ? 'transaction.increased_stake_locked_time_amount'
+ : 'transaction.increasing_stake_locked_time_amount'
+ };
+ }
+ /* END - ESCROW */
+ /* START - VESTING */
+ case Transaction.VESTING_CLAIM: {
+ return {
+ key: isPast ? 'transaction.claimed_vesting' : 'transaction.claiming_vesting'
+ };
+ }
+ /* END - VESTING */
+ }
+};
+
+const getTransactionDescription = (
+ params: TransactionActions,
+ status: TransactionStatus,
+ t: TFunction
+): string | undefined => {
+ const translation = getTranslationArgs(params, status);
+
+ if (!translation) return;
+
+ return t(translation.key, translation.args);
+};
+
+export { getTransactionDescription };
diff --git a/src/utils/hooks/transaction/utils/submit.ts b/src/utils/hooks/transaction/utils/submit.ts
index d1c832b023..21c88b0cd5 100644
--- a/src/utils/hooks/transaction/utils/submit.ts
+++ b/src/utils/hooks/transaction/utils/submit.ts
@@ -6,11 +6,11 @@ import { ExtrinsicStatus } from '@polkadot/types/interfaces/author';
import { ISubmittableResult } from '@polkadot/types/types';
import { TransactionEvents } from '../types';
+import { TransactionResult } from '../use-transaction';
type HandleTransactionResult = { result: ISubmittableResult; unsubscribe: () => void };
-// When passing { nonce: -1 } to signAndSend the API will use system.accountNextIndex to determine the nonce
-const transactionOptions = { nonce: -1 };
+let nonce: number | undefined;
const handleTransaction = async (
account: AddressOrPair,
@@ -23,11 +23,16 @@ const handleTransaction = async (
// Extrinsic status
let isReady = false;
+ if (!nonce) {
+ const lastestNonce = await window.bridge.api.rpc.system.accountNextIndex(account.toString());
+ nonce = lastestNonce.toNumber();
+ }
+
return new Promise((resolve, reject) => {
let unsubscribe: () => void;
(extrinsicData.extrinsic as SubmittableExtrinsic<'promise'>)
- .signAndSend(account, transactionOptions, callback)
+ .signAndSend(account, { nonce }, callback)
.then((unsub) => (unsubscribe = unsub))
.catch((error) => reject(error));
@@ -43,35 +48,34 @@ const handleTransaction = async (
isComplete = expectedStatus === result.status.type;
}
- if (isComplete) {
+ if (isComplete || result.status.isUsurped) {
resolve({ unsubscribe, result });
}
}
+
+ if (nonce) {
+ nonce++;
+ }
});
};
const getErrorMessage = (api: ApiPromise, dispatchError: DispatchError) => {
const { isModule, asModule, isBadOrigin } = dispatchError;
- // Construct error message
- const message = 'The transaction failed.';
-
// Runtime error in one of the parachain modules
if (isModule) {
// for module errors, we have the section indexed, lookup
const decoded = api.registry.findMetaError(asModule);
const { docs, name, section } = decoded;
- return message.concat(` The error code is ${section}.${name}. ${docs.join(' ')}`);
+ return `The error code is ${section}.${name}. ${docs.join(' ')}.`;
}
// Bad origin
if (isBadOrigin) {
- return message.concat(
- ` The error is caused by using an incorrect account. The error code is BadOrigin ${dispatchError}.`
- );
+ return `The error is caused by using an incorrect account. The error code is BadOrigin ${dispatchError}.`;
}
- return message.concat(` The error is ${dispatchError}.`);
+ return `The error is ${dispatchError}.`;
};
/**
@@ -89,19 +93,29 @@ const submitTransaction = async (
extrinsicData: ExtrinsicData,
expectedStatus?: ExtrinsicStatus['type'],
callbacks?: TransactionEvents
-): Promise => {
+): Promise => {
const { result, unsubscribe } = await handleTransaction(account, extrinsicData, expectedStatus, callbacks);
unsubscribe();
+ let error: Error | undefined;
+
const { dispatchError } = result;
if (dispatchError) {
- const message = getErrorMessage(api, dispatchError);
- throw new Error(message);
+ error = new Error(getErrorMessage(api, dispatchError));
+ }
+
+ // TODO: determine a description to when transaction ends up usurped
+ if (result.status.isUsurped) {
+ error = new Error();
}
- return result;
+ return {
+ status: error ? 'error' : 'success',
+ data: result,
+ error
+ };
};
export { submitTransaction };
diff --git a/src/utils/hooks/use-copy-tooltip.tsx b/src/utils/hooks/use-copy-tooltip.tsx
index 36ec13ccd4..42ce3c1fcc 100644
--- a/src/utils/hooks/use-copy-tooltip.tsx
+++ b/src/utils/hooks/use-copy-tooltip.tsx
@@ -15,6 +15,7 @@ type CopyTooltipResult = {
};
};
+// FIX: is openning tooltip too fast
const useCopyTooltip = (props?: CopyTooltipProp): CopyTooltipResult => {
const { t } = useTranslation();
diff --git a/src/utils/hooks/use-countdown.ts b/src/utils/hooks/use-countdown.ts
new file mode 100644
index 0000000000..5430a4cf99
--- /dev/null
+++ b/src/utils/hooks/use-countdown.ts
@@ -0,0 +1,69 @@
+import { useCallback, useEffect, useState } from 'react';
+import { useInterval } from 'react-use';
+
+import { theme } from '@/component-library';
+import { useWindowFocus } from '@/utils/hooks/use-window-focus';
+
+type UseCountdownProps = {
+ value?: number;
+ timeout?: number;
+ disabled?: boolean;
+ onEndCountdown?: () => void;
+};
+
+type UseCountdownResult = {
+ value: number;
+ start: () => void;
+ stop: () => void;
+};
+
+const useCountdown = ({
+ value = 100,
+ timeout = 8000,
+ disabled,
+ onEndCountdown
+}: UseCountdownProps): UseCountdownResult => {
+ const windowFocused = useWindowFocus();
+
+ const [countdown, setProgress] = useState(value);
+ const [isRunning, setRunning] = useState(disabled);
+
+ // handles the countdown
+ useInterval(
+ () => setProgress((prev) => prev - 1),
+ isRunning ? timeout / theme.transition.duration.duration100 : null
+ );
+
+ const handleStartCountdown = useCallback(() => {
+ const shouldRun = !disabled && countdown > 0;
+ setRunning(shouldRun);
+ }, [countdown, disabled]);
+
+ const handleStopCountdown = () => setRunning(false);
+
+ useEffect(() => {
+ if (isRunning && countdown === 0) {
+ onEndCountdown?.();
+ handleStopCountdown();
+ }
+ }, [isRunning, countdown, onEndCountdown]);
+
+ useEffect(() => {
+ if (windowFocused && !disabled) {
+ handleStartCountdown();
+ } else {
+ handleStopCountdown();
+ }
+ }, [windowFocused, handleStartCountdown, disabled]);
+
+ console.log(countdown, isRunning);
+
+ return {
+ value: countdown,
+ start: handleStartCountdown,
+ stop: handleStopCountdown
+ };
+};
+
+export { useCountdown };
+export type { UseCountdownProps, UseCountdownResult };
diff --git a/src/utils/hooks/use-sign-message.ts b/src/utils/hooks/use-sign-message.ts
index 78ef745257..ef57dbdfd0 100644
--- a/src/utils/hooks/use-sign-message.ts
+++ b/src/utils/hooks/use-sign-message.ts
@@ -96,6 +96,7 @@ const useSignMessage = (): UseSignMessageResult => {
queryFn: () => selectedAccount && getSignature(selectedAccount)
});
+ // TODO: add new notification
const signMessageMutation = useMutation((account: KeyringPair) => postSignature(account), {
onError: (_, variables) => {
setSignature(variables.address, false);
diff --git a/src/utils/hooks/use-window-focus.ts b/src/utils/hooks/use-window-focus.ts
new file mode 100644
index 0000000000..27d63b7c54
--- /dev/null
+++ b/src/utils/hooks/use-window-focus.ts
@@ -0,0 +1,26 @@
+import { useEffect, useState } from 'react';
+
+const hasFocus = () => typeof document !== 'undefined' && document.hasFocus();
+
+const useWindowFocus = (): boolean => {
+ const [focused, setFocused] = useState(hasFocus); // Focus for first render
+
+ useEffect(() => {
+ setFocused(hasFocus()); // Focus for additional renders
+
+ const onFocus = () => setFocused(true);
+ const onBlur = () => setFocused(false);
+
+ window.addEventListener('focus', onFocus);
+ window.addEventListener('blur', onBlur);
+
+ return () => {
+ window.removeEventListener('focus', onFocus);
+ window.removeEventListener('blur', onBlur);
+ };
+ }, []);
+
+ return focused;
+};
+
+export { useWindowFocus };
diff --git a/yarn.lock b/yarn.lock
index f662a8bcb9..bf4d250f24 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -17669,16 +17669,14 @@ react-table@^7.6.3:
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.7.0.tgz#e2ce14d7fe3a559f7444e9ecfe8231ea8373f912"
integrity sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA==
-react-toastify@^6.0.5:
- version "6.2.0"
- resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-6.2.0.tgz#f2d76747c70b9de91f71f253d9feae6b53dc836c"
- integrity sha512-XpjFrcBhQ0/nBOL4syqgP/TywFnOyxmstYLWgSQWcj39qpp+WU4vPt3C/ayIDx7RFyxRWfzWTdR2qOcDGo7G0w==
+react-toastify@^9.1.2:
+ version "9.1.2"
+ resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.1.2.tgz#293aa1f952240129fe485ae5cb2f8d09c652cf3f"
+ integrity sha512-PBfzXO5jMGEtdYR5jxrORlNZZe/EuOkwvwKijMatsZZm8IZwLj01YvobeJYNjFcA6uy6CVrx2fzL9GWbhWPTDA==
dependencies:
clsx "^1.1.1"
- prop-types "^15.7.2"
- react-transition-group "^4.4.1"
-react-transition-group@^4.4.1, react-transition-group@^4.4.5:
+react-transition-group@^4.4.5:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==