From e1a4532baa273766f42c8eeb50a3258bdc5d140f Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Wed, 21 Jun 2023 11:46:34 +0100 Subject: [PATCH] [release] Interlay 2.34.0 (#1312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: redirect when access from forbidden country is detected (#1209) * Feature/updated transfer UI (#876) * refactor: use updated tab component * refactor: duplicated form titles * refactor: remove redundant hook calls * refactor: prefer title case * wip: XCM transfer form UI * wip: updated form UI * wip: account selector placeholder component * wip: account selector modal * wip: modal open and close actions * wip: update modal type * wip: get accounts * wip: add identicon and rename component for consistency * wip: account input component * fix: remove redundant icons prop * feat: implement with SelectTrigger * wip: styling and account selection value * wip: handle setting account data * refactor: better naming * wip: address list styling * refactor: rename defaultAccount * wip: chain selector placeholder component * wip: duplicate account component and rename * chore: delete redundant legacy component * wip: logic for fetching and rendering chain ids * wip: chain item styling * wip: selected chain styling * chore: add comment * refactor: pass through native token to icon component * feature: add chain icon component * chore: add comment * chore: correct file name casing * refactor: improve folder structure * wip: form layout styling * chore: add arrow icon * chore: add logos and correct svg titles * chore: remove redundant svg prop * chore: rename arrow icon * chore: consistent use of styled components * refactor: remove padding from modal body * wip: formik integration work * wip: extend useXCMBridge to return available chains and utility methods * chore: move Chain and Chains types to types directory * feat: layout and form implementation * feat: add schema * feat: final * wip: refactor useXCMBridge hook * refactor: add endpoints type * refactor: wrap methods in useCallback * refactor: fix bug in hook method * chore: bump bridge version * wip: set originating and destination chain values * refactor: set from chain value on field change * wip: set originating chain value * refactor: mergeProps to set field value * refactor: handle setting origin/destination chain values * wip: get tokens method * wip: first iteration of balances function * wip: handle tokens array * wip: set token value * wip: get token balances * wip: return token and balances in single method * wip: mapped tokens * refactor: handle default chain values * refactor: better organised function order * wip: handle change events * wip: handle setting tokens * wip: handle fetching tokens and balances * wip: convert input configs * wip: handle token change * wip: get token USD price * Trigger Build * chore: remove unused import * chore: correct eslintignore syntax * wip: handle breaking changes * wip: disable token input when select items value is 1 * chore: set first token item as variable * wip: handle setting and changing values * chire: add loading spinner * refactor: add loading state * refactor: filter destination chains * chore: remove console log * chore: bump XCM bridge version * chore: update config * refactor: configure validation * chore: revert change to useForm hook * wip: form validation * wip: working form validation * wip: undefined validation parameters * refactor: return dest fee estimate from bridge hook * feature: show fees and fee estimates * chore: conditional operators * refactor: handle ticker change correctly * wip: sendTransaction method * Revert "wip: sendTransaction method" This reverts commit 3ade26dda26c7cc14f9db9e7c005b66863fa9139. * fix: USD amounts * wip: send transactions * refactor: bump bridge and use getNativeToken method * chore: bump bridge * refactor: move submit logic to useMutation hook * fix: type mismatches * refactor: white space/comments * refactor: add transaction fee validation * chore: typo * chore: remove console log * refactor: remove duplicated monetary conversion * refactor: remove duplicate code * Revert "refactor: remove duplicate code" This reverts commit bd29f8c5661e327c5285d1020c534dab2deae806. * Revert "refactor: remove duplicated monetary conversion" This reverts commit 5fd3d645eb7d8edc00cfe8ced186d4e2432af9fc. * refactor: use monetaryAmount when constructing transaction * refactor: remove duplicated code for fetching tokens * refactor: default XCM origin * Revert "refactor: remove duplicated code for fetching tokens" This reverts commit 8f31ee8667adcd49f5aaebb7db2f205afb5e9725. * chore: remove comment * chore: fix errors * fix: set default value to empty string to prevent React error * refactor: removed unwanted force validation parameters * refactor: remove redundant method * refactor: add method return type * refactor: add method return type * refactor: correct type error * refactor: fix destFee type error * refactor: remove fees validation and revert destFee return value * chore: remove console log * refactor: remove redundant method * refactor: disable validation on change * chore: remove commented out code * wip: use select component for chain selector * fix: handle chain select functions * refactor: type chain id as ChainName * Revert "refactor: type chain id as ChainName" This reverts commit d05e0128cb4b5ac1d00ac07808ebdf9858739165. * chore: remove unused component files * refactor: remove duplicated transaction logic * fix: make to/from field types more specific * fix: revert yup.custom changes and cast validation * fix: set correct destination chain * refator: handle token data * refactor: add use callback * fix: correct rendering logic * fix: update dependencies * chore: delete unused styles * chore: fix merge issue with transfer form * fix: change validation handling * Revert "fix: change validation handling" This reverts commit c0cb3062aad3540b2afad7d375024d872924a62c. * refactor: only display transfer amount if amount has been entered * chore: config changes * chore: add missing icons * chore: Hydra chain icon * fix: add error text to CTA * Tom/xcm fixes (#1213) * refactor: specify endpoints and remove unnecessary logic * fix: save file before committing * fix: disable refetch * chore: update endpoints * chore: remove log * chore: rename file * chore: add additional acala/karura endpoints --------- Co-authored-by: Rui Simão * chore: release v2.32.0 * Update API healthchecks (#778) * Chore - add vault healthcheck * Chore - add vault healthcheck * Chore - add vault healthcheck * [earn strategies] placeholder page, nav and feature flag (#1216) * chore: bump icons dependency * feature: earn strategies placeholder page and feature flag * feat: add useTransaction (#1189) * chore: update monetary to latest 0.7.3 (#1214) * chore: update monetary to latest 0.7.3 * chore: update lib * chore: bump lib and bridge (#1219) * chore: release v2.32.1 * fix: add missing icons and remove erroring RPC (#1222) * fix: add missing icons and remove erroring RPC * Update src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Acala.tsx Co-authored-by: Peter Slaný <47864599+peterslany@users.noreply.github.com> * Update src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Astar.tsx Co-authored-by: Peter Slaný <47864599+peterslany@users.noreply.github.com> * Update src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Parallel.tsx Co-authored-by: Peter Slaný <47864599+peterslany@users.noreply.github.com> --------- Co-authored-by: Peter Slaný <47864599+peterslany@users.noreply.github.com> * chore: release v2.32.2 * fix: compare input configs with method not operator (#1225) * refactor: reset selected account on account change (#1226) * chore: release v2.32.3 * feature: add geoblock feature flag (#1230) * chore: release v2.32.4 * chore: bump bridge (#1233) * chore: release v2.32.5 * Peter/earn strategies feat deposit withdraw form (#1229) * chore: update monetary to latest 0.7.3 * wip * feat(earn-strategies): add deposit and withdrawal form components * refactor: add padding under tabs in earn strategy forms * chore(earn-strategies): change file structure * feat: add Popover, Underlay and ProgressBar. Changes to Dialog, Modal and Overlay. (#1236) * fix: Dialog, Modal and Popover (#1245) * chore: rename strategies feature (#1247) * chore: release v2.32.6 * Fix: back button behaviour from bridge page (#1246) * fix: use history replace instead of push to fix looping of bridge page * chore: clean up and bump version --------- Co-authored-by: tomjeatt <40243778+tomjeatt@users.noreply.github.com> * feat: add transaction notifications (#1177) * chore: remove console.log (#1262) * fix(TokenInput): adorment ticker (#1257) * fix: get vesting data (#1264) * Peter/chore update lib 2.3.0 (#1267) * chore: update monetary to latest 0.7.3 * chore: update lib version * fix: sort notifications (#1270) * fix: transaction none (#1271) * fix(Loans): apy label (#1275) * Peter/loans fix subsidy rewards (#1276) * chore: update monetary to latest 0.7.3 * fix(loans): display correct subsidy rewards accrued amount and APY * chore: console log cleanup * chore: replace GOVERNANCE_TOKEN_SYMBOL with GOVERNANCE_TOKEN.ticker * Peter/fix loans incentive apr computation (#1256) * chore: update monetary to latest 0.7.3 * fix: convert incentives apr computation to percentage * fix: change loans incentives annualized return to have label APR * chore: release v2.33.0 * Peter/chore update lib 2.3.3 (#1282) * chore: update monetary to latest 0.7.3 * chore: update lib to 2.3.3. * fix: enable faucet on Interlay testnet (#1289) * fix: enable faucet on Interlay testnet * fix: prefer governance token ticker to symbol * chore: bump bridge (#1285) * fix(Swap): update trade object on each block (#1297) * api: use diadata as main datasource (#1277) * api: use diadata as main datasource * api: add header to select price source --------- Co-authored-by: tomjeatt <40243778+tomjeatt@users.noreply.github.com> * Peter/fix interlay issues (#1300) * chore: update monetary to latest 0.7.3 * fix: add missing translation and fix lend APY display * refactor: bring back formatting with 0 amount case covered * refactor: code review * refactor: code review * api: select price source via query param and ticker renaming (#1307) * api: fix tether label for dia (#1309) * chore: release v2.34.0 * chore: fix merge conflict * Trigger build --------- Co-authored-by: Peter Slaný <47864599+peterslany@users.noreply.github.com> Co-authored-by: Rui Simão Co-authored-by: ns212 <73105077+ns212@users.noreply.github.com> Co-authored-by: Chanakya Kilaru --- api/market_data.py | 66 +++- package.json | 8 +- 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 ++ .../TokenInput/TokenInput.tsx | 2 +- .../LoanApyTooltip/BreakdownGroup.tsx | 2 +- .../LoanApyTooltip/LoanApyTooltip.tsx | 9 +- src/components/LoanPositionsTable/ApyCell.tsx | 8 +- .../LoanPositionsTable/LoanPositionsTable.tsx | 8 +- .../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 +- .../DepositForm/DepositOutputAssets.tsx | 2 +- .../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 | 58 ++- 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 +- .../BorrowAssetsTable/BorrowAssetsTable.tsx | 6 +- .../CollateralModal/CollateralModal.tsx | 49 +-- .../LendAssetsTable/LendAssetsTable.tsx | 6 +- .../LoanActionInfo/RewardsGroup.tsx | 2 +- .../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 | 10 +- src/utils/constants/links.ts | 21 +- src/utils/context/Notifications.tsx | 141 +++++++ src/utils/helpers/loans.ts | 5 +- .../use-get-account-lending-statistics.tsx | 1 + src/utils/hooks/api/use-get-vesting-data.tsx | 2 +- .../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 | 37 +- src/utils/hooks/use-copy-tooltip.tsx | 1 + src/utils/hooks/use-countdown.ts | 67 ++++ src/utils/hooks/use-sign-message.ts | 1 + .../hooks/use-update-query-parameters.ts | 2 +- src/utils/hooks/use-window-focus.ts | 26 ++ yarn.lock | 94 +++-- 99 files changed, 2154 insertions(+), 1273 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/api/market_data.py b/api/market_data.py index f63b4b7cd6..82fd2d076f 100644 --- a/api/market_data.py +++ b/api/market_data.py @@ -8,6 +8,9 @@ api_key = os.environ.get("CG_API_KEY") +tickers = { + "Tether USD": "tether", +} @app.after_request def add_header(response): @@ -15,11 +18,7 @@ def add_header(response): response.cache_control.s_maxage = 300 return response - -@app.route("/marketdata/price", methods=["GET"]) -def get_price(): - args = request.args - +def coingecko(args): headers_dict = { "content-type": "application/json", "accept": "application/json", @@ -28,6 +27,63 @@ def get_price(): url = "https://api.coingecko.com/api/v3/simple/price" resp = requests.get(url, params=args, headers=headers_dict) data = resp.json() + return data + +def dia(asset): + headers_dict = { + "content-type": "application/json", + "accept": "application/json", + "x-cg-pro-api-key": api_key, + } + url = "https://api.diadata.org/v1/assetQuotation" + if asset == "bitcoin": + url += "/Bitcoin/0x0000000000000000000000000000000000000000" + elif asset == "interlay": + url += "/Interlay/0x0000000000000000000000000000000000000000" + elif asset == "liquid-staking-dot": + return { "liquid-staking-dot": None } + elif asset == "polkadot": + url += "/Polkadot/0x0000000000000000000000000000000000000000/" + elif asset == "tether": + url += "/Ethereum/0xdAC17F958D2ee523a2206206994597C13D831ec7" + + resp = requests.get(url, headers=headers_dict) + data = resp.json() + + # optionally rename the ticker + ticker = tickers.get(data["Name"], data["Name"]).lower() + + return { + ticker: { + "usd": data["Price"], + } + } + + +@app.route("/marketdata/price", methods=["GET"]) +def get_price(): + args = request.args + + price_source = args.get('price-source') + + data = {} + + def _dia(): + ticker_ids = args["ids"].split(",") + for ticker_id in ticker_ids: + data.update(dia(ticker_id)) + + if price_source == "dia": + _dia() + elif price_source == "coingecko": + data = coingecko(args) + else: + try: + _dia() + except Exception as e: + print("Error", e) + data = coingecko(args) + return jsonify(data) diff --git a/package.json b/package.json index 581d654255..138d888dde 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "interbtc-ui", - "version": "2.32.6", + "version": "2.34.0", "private": true, "dependencies": { "@craco/craco": "^6.1.1", "@headlessui/react": "^1.1.1", "@heroicons/react": "^2.0.18", - "@interlay/bridge": "^0.3.11", - "@interlay/interbtc-api": "2.2.4", + "@interlay/bridge": "^0.3.13", + "@interlay/interbtc-api": "2.3.3", "@interlay/monetary-js": "0.7.3", "@polkadot/api": "9.14.2", "@polkadot/extension-dapp": "0.44.1", @@ -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/component-library/TokenInput/TokenInput.tsx b/src/component-library/TokenInput/TokenInput.tsx index ebf89b542f..109f6148ad 100644 --- a/src/component-library/TokenInput/TokenInput.tsx +++ b/src/component-library/TokenInput/TokenInput.tsx @@ -63,7 +63,7 @@ const TokenInput = forwardRef( const itemsArr = Array.from(selectProps?.items || []); const isSelectAdornment = itemsArr.length > 1; - const adornmentTicker = !isSelectAdornment && selectProps?.items ? itemsArr[0]?.value : ticker; + const adornmentTicker = !isSelectAdornment && selectProps?.items ? itemsArr[0]?.value : tickerProp; useEffect(() => { if (selectProps?.value === undefined) return; diff --git a/src/components/LoanApyTooltip/BreakdownGroup.tsx b/src/components/LoanApyTooltip/BreakdownGroup.tsx index ebfbd86e95..be97f0249f 100644 --- a/src/components/LoanApyTooltip/BreakdownGroup.tsx +++ b/src/components/LoanApyTooltip/BreakdownGroup.tsx @@ -29,7 +29,7 @@ const BreakdownGroup = ({ apy, rewardsApy, ticker, rewardsTicker, isBorrow }: Br {!!rewardsApy && ( -
Rewards APY {rewardsTicker}:
+
Rewards APR {rewardsTicker}:
{getApyLabel(rewardsApy)}
)} diff --git a/src/components/LoanApyTooltip/LoanApyTooltip.tsx b/src/components/LoanApyTooltip/LoanApyTooltip.tsx index 37a2dbc5d2..f6cb684837 100644 --- a/src/components/LoanApyTooltip/LoanApyTooltip.tsx +++ b/src/components/LoanApyTooltip/LoanApyTooltip.tsx @@ -4,12 +4,12 @@ import { TooltipProps } from '@reach/tooltip'; import Big from 'big.js'; import { Dd, Dl, DlGroup } from '@/component-library'; +import { GOVERNANCE_TOKEN } from '@/config/relay-chains'; import { Prices } from '@/utils/hooks/api/use-get-prices'; import { AssetGroup } from './AssetGroup'; import { BreakdownGroup } from './BreakdownGroup'; import { StyledApyTooltipTitle, StyledTooltip } from './LoanApyTooltip.style'; -import { RewardsGroup } from './RewardsGroup'; type Props = { apy: Big; @@ -17,7 +17,6 @@ type Props = { earnedInterest?: MonetaryAmount; accumulatedDebt?: MonetaryAmount; rewardsApy?: Big; - rewards: MonetaryAmount | null; prices: Prices; isBorrow: boolean; }; @@ -32,12 +31,11 @@ const LoanApyTooltip = ({ earnedInterest, accumulatedDebt, rewardsApy, - rewards, prices, isBorrow, ...props }: LoanApyTooltipProps): JSX.Element => { - const showEarnedRewards = !!rewards || !!earnedInterest; + const showEarnedRewards = !!earnedInterest; const label = (
@@ -45,7 +43,7 @@ const LoanApyTooltip = ({ apy={apy} isBorrow={isBorrow} rewardsApy={rewardsApy} - rewardsTicker={rewards?.currency.ticker} + rewardsTicker={GOVERNANCE_TOKEN.ticker} ticker={currency.ticker} /> {accumulatedDebt && ( @@ -64,7 +62,6 @@ const LoanApyTooltip = ({
{earnedInterest && } - {!!rewards && }
diff --git a/src/components/LoanPositionsTable/ApyCell.tsx b/src/components/LoanPositionsTable/ApyCell.tsx index 1860138278..d36911f7be 100644 --- a/src/components/LoanPositionsTable/ApyCell.tsx +++ b/src/components/LoanPositionsTable/ApyCell.tsx @@ -15,7 +15,6 @@ type ApyCellProps = { earnedInterest?: MonetaryAmount; accumulatedDebt?: MonetaryAmount; rewardsPerYear: MonetaryAmount | null; - accruedRewards: MonetaryAmount | null; prices?: Prices; isBorrow?: boolean; onClick?: () => void; @@ -25,7 +24,6 @@ const ApyCell = ({ apy, currency, rewardsPerYear, - accruedRewards, accumulatedDebt, earnedInterest, prices, @@ -34,8 +32,9 @@ const ApyCell = ({ }: ApyCellProps): JSX.Element => { const rewardsApy = getSubsidyRewardApy(currency, rewardsPerYear, prices); - const totalApy = isBorrow ? apy.sub(rewardsApy || 0) : apy.add(rewardsApy || 0); - const totalApyLabel = isBorrow ? `-${getApyLabel(totalApy)}` : getApyLabel(totalApy); + const totalApy = isBorrow ? (rewardsApy || Big(0)).sub(apy) : apy.add(rewardsApy || 0); + + const totalApyLabel = getApyLabel(totalApy); const earnedAsset = accumulatedDebt || earnedInterest; @@ -54,7 +53,6 @@ const ApyCell = ({ apy={apy} currency={currency} prices={prices} - rewards={accruedRewards} rewardsApy={rewardsApy} isBorrow={isBorrow} accumulatedDebt={accumulatedDebt} diff --git a/src/components/LoanPositionsTable/LoanPositionsTable.tsx b/src/components/LoanPositionsTable/LoanPositionsTable.tsx index ac3ad99d26..ed56422a75 100644 --- a/src/components/LoanPositionsTable/LoanPositionsTable.tsx +++ b/src/components/LoanPositionsTable/LoanPositionsTable.tsx @@ -7,7 +7,6 @@ import { convertMonetaryAmountToValueInUSD } from '@/common/utils/utils'; import { Switch } from '@/component-library'; import { LoanType } from '@/types/loans'; import { getTokenPrice } from '@/utils/helpers/prices'; -import { useGetAccountSubsidyRewards } from '@/utils/hooks/api/loans/use-get-account-subsidy-rewards'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; import { AssetCell, BalanceCell, Table, TableProps } from '../DataGrid'; @@ -53,7 +52,6 @@ const LoanPositionsTable = ({ const titleId = useId(); const { t } = useTranslation(); const prices = useGetPrices(); - const { data: subsidyRewards } = useGetAccountSubsidyRewards(); const isLending = variant === 'lend'; const showCollateral = !!onPressCollateralSwitch && isLending; @@ -91,13 +89,11 @@ const LoanPositionsTable = ({ const apyCellProps = isLending ? { apy: lendApy, - rewardsPerYear: lendReward, - accruedRewards: subsidyRewards ? subsidyRewards.perMarket[currency.ticker].lend : null + rewardsPerYear: lendReward } : { apy: borrowApy, rewardsPerYear: borrowReward, - accruedRewards: subsidyRewards ? subsidyRewards.perMarket[currency.ticker].borrow : null, accumulatedDebt: (position as BorrowPosition).accumulatedDebt, isBorrow: true }; @@ -140,7 +136,7 @@ const LoanPositionsTable = ({ collateral }; }), - [assets, isLending, onPressCollateralSwitch, onRowAction, positions, prices, showCollateral, subsidyRewards] + [assets, isLending, onPressCollateralSwitch, onRowAction, positions, prices, showCollateral] ); return ( diff --git a/src/components/NotificationsPopover/NotificationsList.tsx b/src/components/NotificationsPopover/NotificationsList.tsx new file mode 100644 index 0000000000..a84dc4091e --- /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).sort((a, b) => b.date.getTime() - a.date.getTime()); + + 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/DepositForm/DepositOutputAssets.tsx b/src/pages/AMM/Pools/components/DepositForm/DepositOutputAssets.tsx index 5a17465ec9..f21e8f81af 100644 --- a/src/pages/AMM/Pools/components/DepositForm/DepositOutputAssets.tsx +++ b/src/pages/AMM/Pools/components/DepositForm/DepositOutputAssets.tsx @@ -56,7 +56,7 @@ const DepositOutputAssets = ({ pool, values, prices }: DepositOutputAssetsProps) return (

- {t('amm.pools.receivable_assets')} + {t('receivable_assets')}

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..25753b06ca 100644 --- a/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx +++ b/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx @@ -4,8 +4,7 @@ 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 { useDebounce, useInterval } from 'react-use'; import { StoreType } from '@/common/types/util.types'; import { convertMonetaryAmountToValueInUSD, formatUSD, newSafeMonetaryAmount } from '@/common/utils/utils'; @@ -21,6 +20,7 @@ import { } from '@/lib/form'; import { SlippageManager } from '@/pages/AMM/shared/components'; import { SwapPair } from '@/types/swap'; +import { REFETCH_INTERVAL } from '@/utils/constants/api'; import { SWAP_PRICE_IMPACT_LIMIT } from '@/utils/constants/swap'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; @@ -113,31 +113,29 @@ 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( - () => { - if (!pair.input || !pair.output || !inputAmount) { - return setTrade(undefined); - } + const handleChangeTrade = () => { + if (!pair.input || !pair.output || !inputAmount) { + return setTrade(undefined); + } + + const inputMonetaryAmount = newMonetaryAmount(inputAmount, pair.input, true); + const trade = window.bridge.amm.getOptimalTrade(inputMonetaryAmount, pair.output, liquidityPools); - const inputMonetaryAmount = newMonetaryAmount(inputAmount, pair.input, true); - const trade = window.bridge.amm.getOptimalTrade(inputMonetaryAmount, pair.output, liquidityPools); + setTrade(trade); + }; - setTrade(trade); - }, - 500, - [inputAmount, pair] - ); + // attemp to update trade object on each new block + useInterval(handleChangeTrade, REFETCH_INTERVAL.BLOCK); + + useDebounce(handleChangeTrade, 500, [inputAmount, pair]); const inputBalance = pair.input && getAvailableBalance(pair.input.ticker); const outputBalance = pair.output && getAvailableBalance(pair.output.ticker); @@ -157,12 +155,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 +190,6 @@ const SwapForm = ({ initialValues, validationSchema: swapSchema({ [SWAP_INPUT_AMOUNT_FIELD]: inputSchemaParams }), onSubmit: handleSubmit, - disableValidation: transaction.isLoading, validateOnMount: true }); @@ -216,16 +212,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 +308,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 && ( Object.values(assets).map(({ borrowApy, currency, availableCapacity, totalBorrows, borrowReward }) => { const asset = ; - const accruedRewards = subsidyRewards ? subsidyRewards.perMarket[currency.ticker].borrow : null; const apy = ( { - 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/LendAssetsTable/LendAssetsTable.tsx b/src/pages/Loans/LoansOverview/components/LendAssetsTable/LendAssetsTable.tsx index ab5e125316..ea399226b5 100644 --- a/src/pages/Loans/LoansOverview/components/LendAssetsTable/LendAssetsTable.tsx +++ b/src/pages/Loans/LoansOverview/components/LendAssetsTable/LendAssetsTable.tsx @@ -8,7 +8,6 @@ import { AssetCell, BalanceCell, Cell, Table, TableProps } from '@/components'; import { ApyCell } from '@/components/LoanPositionsTable/ApyCell'; import { LoanTablePlaceholder } from '@/components/LoanPositionsTable/LoanTablePlaceholder'; import { getTokenPrice } from '@/utils/helpers/prices'; -import { useGetAccountSubsidyRewards } from '@/utils/hooks/api/loans/use-get-account-subsidy-rewards'; import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; @@ -48,20 +47,17 @@ const LendAssetsTable = ({ assets, onRowAction, ...props }: LendAssetsTableProps const { t } = useTranslation(); const prices = useGetPrices(); const { data: balances } = useGetBalances(); - const { data: subsidyRewards } = useGetAccountSubsidyRewards(); const rows: LendAssetsTableRow[] = useMemo( () => Object.values(assets).map(({ lendApy, currency, totalLiquidity, lendReward }) => { const asset = ; - const accruedRewards = subsidyRewards ? subsidyRewards.perMarket[currency.ticker].lend : null; const apy = ( onRowAction?.(currency.ticker as Key)} @@ -87,7 +83,7 @@ const LendAssetsTable = ({ assets, onRowAction, ...props }: LendAssetsTableProps totalSupply }; }), - [assets, balances, onRowAction, prices, subsidyRewards] + [assets, balances, onRowAction, prices] ); return ( diff --git a/src/pages/Loans/LoansOverview/components/LoanActionInfo/RewardsGroup.tsx b/src/pages/Loans/LoansOverview/components/LoanActionInfo/RewardsGroup.tsx index 29164ddbc2..6dd8c5372b 100644 --- a/src/pages/Loans/LoansOverview/components/LoanActionInfo/RewardsGroup.tsx +++ b/src/pages/Loans/LoansOverview/components/LoanActionInfo/RewardsGroup.tsx @@ -26,7 +26,7 @@ const RewardsGroup = ({ isBorrow, apy, assetCurrency, rewards, prices }: Rewards return ( <> -
Rewards APY {rewards.currency.ticker}
+
Rewards APR {rewards.currency.ticker}
{getApyLabel(subsidyRewardApy)}
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 ( <>
@@ -171,22 +148,10 @@ const TransferForm = (): JSX.Element => { size='large' type='submit' disabled={parachainStatus === (ParachainStatus.Loading || ParachainStatus.Shutdown)} - loading={submitStatus === STATUSES.PENDING} > {t('transfer')}
- {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')} +

{t('vault.issue_description')}

@@ -416,19 +407,8 @@ const RequestIssueModal = ({ onClose, open, collateralToken, vaultAddress }: Pro {t('confirm')}

-
-
- {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')} +

{t('vault.redeem_description')}

@@ -142,8 +136,8 @@ const RequestRedeemModal = ({ onClose, open, collateralToken, vaultAddress, lock

- - + + ); }; 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')} +

{t('vault.withdraw_your_collateral')}

{t('vault.you_have')}

@@ -197,8 +190,8 @@ const RequestReplacementModal = ({
- - + + ); } 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} +

{t('vault.current_total_collateral', { @@ -326,8 +319,8 @@ const UpdateCollateralModal = ({

{renderSubmitButton()}
-
-
+ + ); }; 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 ( -
- - {title} - - - - Current Total Collateral - - {formatNumber(collateral.amount.toNumber())} {collateralToken.ticker} ({formatUSD(collateral.usd)}) - - - - Minimum Required Collateral - - {isMinCollateralLoading ? ( - '-' - ) : ( - <> - {displayMonetaryAmount(requiredCollateralTokenAmount)} {collateralToken.ticker} ( - {displayMonetaryAmountInUSDFormat(requiredCollateralTokenAmount as any, collateralUSDAmount)}) - - )} - - - {/* New Collateralization} - sublabel={{getCollateralStatusLabel(collateralStatus)}} - ranges={ranges} - /> */} - - New liquidation Price - - {formatUSD(12.32)} {collateralToken.ticker} / {formatUSD(42324.32)} BTC - - - - - Fees - - 0.01 KINT ({formatUSD(0.24)}) - - - - - {title} - - -
- ); -}; - -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..63098f3442 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,8 +39,9 @@ const Topbar = (): JSX.Element => { const isBanxaEnabled = useFeatureFlag(FeatureFlags.BANXA); const { setSelectedAccount, removeSelectedAccount } = useSubstrate(); const { selectProps } = useSignMessage(); + const { list } = useNotifications(); - const kintBalanceIsZero = getAvailableBalance('KINT')?.isZero(); + const governanceTokenBalanceIsZero = getAvailableBalance(GOVERNANCE_TOKEN.ticker)?.isZero(); const handleRequestFromFaucet = async (): Promise => { if (!selectedAccount) return; @@ -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}`); @@ -103,7 +106,7 @@ const Topbar = (): JSX.Element => { {isBanxaEnabled ? : } {selectedAccount !== undefined && ( <> - {process.env.REACT_APP_FAUCET_URL && kintBalanceIsZero && ( + {process.env.REACT_APP_FAUCET_URL && governanceTokenBalanceIsZero && ( <> { )} + {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/helpers/loans.ts b/src/utils/helpers/loans.ts index c7241abb5e..35ceb31d07 100644 --- a/src/utils/helpers/loans.ts +++ b/src/utils/helpers/loans.ts @@ -11,6 +11,9 @@ const MIN_DECIMAL_NUMBER = 0.01; // MEMO: returns formatted apy or better representation of a very small apy const getApyLabel = (apy: Big): string => { + if (apy.eq(0)) { + return formatPercentage(0); + } const isPositive = apy.gt(0); const isTinyApy = isPositive ? apy.lt(MIN_DECIMAL_NUMBER) : apy.gt(-MIN_DECIMAL_NUMBER); @@ -41,7 +44,7 @@ const getSubsidyRewardApy = ( } const exchangeRate = rewardCurrencyPriceUSD / positionCurrencyPriceUSD; - const apy = reward.toBig().mul(exchangeRate); + const apy = reward.toBig().mul(exchangeRate).mul(100); return apy; }; diff --git a/src/utils/hooks/api/loans/use-get-account-lending-statistics.tsx b/src/utils/hooks/api/loans/use-get-account-lending-statistics.tsx index 0aa6d77d05..3facffc723 100644 --- a/src/utils/hooks/api/loans/use-get-account-lending-statistics.tsx +++ b/src/utils/hooks/api/loans/use-get-account-lending-statistics.tsx @@ -60,6 +60,7 @@ const getNetAPY = ( const totalBorrowApy = borrowPositions.reduce((total, position) => { const { currency } = position.amount; const { borrowApy, borrowReward } = assets[currency.ticker]; + const rewardsApy = getSubsidyRewardApy(currency, borrowReward, prices); const positionApy = borrowApy.sub(rewardsApy || 0); const positionUSDValue = convertMonetaryAmountToValueInUSD( diff --git a/src/utils/hooks/api/use-get-vesting-data.tsx b/src/utils/hooks/api/use-get-vesting-data.tsx index 48972a88f1..5c011ef2cc 100644 --- a/src/utils/hooks/api/use-get-vesting-data.tsx +++ b/src/utils/hooks/api/use-get-vesting-data.tsx @@ -23,7 +23,7 @@ const getVestingData = async (accountId: AccountId): Promise => { const schedules = await window.bridge.api.query.vesting.vestingSchedules(accountId); const schedule = schedules[0]; - const isClaimable = !!schedule && currentBlockNumber > schedule.start + schedule.period; + const isClaimable = !!schedule && currentBlockNumber > schedule.start.toNumber() + schedule.period.toNumber(); return { schedules, 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..ceb930d7db 100644 --- a/src/utils/hooks/transaction/utils/submit.ts +++ b/src/utils/hooks/transaction/utils/submit.ts @@ -6,12 +6,10 @@ 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 }; - const handleTransaction = async ( account: AddressOrPair, extrinsicData: ExtrinsicData, @@ -27,7 +25,7 @@ const handleTransaction = async ( let unsubscribe: () => void; (extrinsicData.extrinsic as SubmittableExtrinsic<'promise'>) - .signAndSend(account, transactionOptions, callback) + .signAndSend(account, { nonce: -1 }, callback) .then((unsub) => (unsubscribe = unsub)) .catch((error) => reject(error)); @@ -43,7 +41,7 @@ const handleTransaction = async ( isComplete = expectedStatus === result.status.type; } - if (isComplete) { + if (isComplete || result.status.isUsurped) { resolve({ unsubscribe, result }); } } @@ -53,25 +51,20 @@ const handleTransaction = async ( 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 +82,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..49e74aa05d --- /dev/null +++ b/src/utils/hooks/use-countdown.ts @@ -0,0 +1,67 @@ +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]); + + 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-update-query-parameters.ts b/src/utils/hooks/use-update-query-parameters.ts index 80ba8ff7ed..9970299c52 100644 --- a/src/utils/hooks/use-update-query-parameters.ts +++ b/src/utils/hooks/use-update-query-parameters.ts @@ -13,7 +13,7 @@ const useUpdateQueryParameters = (): ((newQueryParameters: QueryParameters) => v ...newQueryParameters }; - history.push({ + history.replace({ ...location, search: queryString.stringify(queryParameters) }); 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..8b0e526b36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2099,10 +2099,10 @@ resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg== -"@interlay/bridge@^0.3.11": - version "0.3.11" - resolved "https://registry.yarnpkg.com/@interlay/bridge/-/bridge-0.3.11.tgz#45b2f3bb44d5e7eb1777ba82cfdf1a2f5dbf2b1d" - integrity sha512-HMgUlSFw5wOR7Qi+JxrDeY8TqoybRd7MWdXUqswDpiCgc0WZGTSDK+2NmuKRgDjRYoly0xIpzpkb8oek6v/JQw== +"@interlay/bridge@^0.3.13": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@interlay/bridge/-/bridge-0.3.13.tgz#8add2a9d8a811ea3bbe73498bf3ebc19cd279ec6" + integrity sha512-LXXomxfI2n1h2MHeN8woRaQgh+gLKKlHfH1oTBAMyKPpSI7tTvtrE2XwIKt+Qg1TvmukRngtmwWtEXh760Dtkw== dependencies: "@acala-network/api" "4.1.8-13" "@acala-network/sdk" "4.1.8-13" @@ -2120,15 +2120,16 @@ dependencies: axios "^0.21.1" -"@interlay/interbtc-api@2.2.4": - version "2.2.4" - resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-2.2.4.tgz#28b429d066d35f77fdc72f4cf57e2452507c37f7" - integrity sha512-cJxSE7J41JPE8QhV0YiLCJEfvpv9JcSWmieITTSOWQCW8GFFXnSTU0iPA2Tgw6s9ea3uxoM2DLGhlDQL8c0ktw== +"@interlay/interbtc-api@2.3.3": + version "2.3.3" + resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-2.3.3.tgz#e75f0aa64ae6db604d4314cadf307fe09d128741" + integrity sha512-q5uDFejEJoy4ZC5sc2YSmksILDA14qR/A+oQonMJGIh2F8k58YHdC8Zpp+6ayYUjp13rwkeQQwoBS1kwBFFdqg== dependencies: "@interlay/esplora-btc-api" "0.4.0" "@interlay/interbtc-types" "1.12.0" "@interlay/monetary-js" "0.7.3" "@polkadot/api" "9.14.2" + "@types/bitcoinjs-lib" "^5.0.0" big.js "6.1.1" bitcoin-core "^3.0.0" bitcoinjs-lib "^5.2.0" @@ -2156,15 +2157,6 @@ big.js "6.1.1" typescript "^4.3.2" -"@interlay/monetary-js@0.7.3": - version "0.7.3" - resolved "https://registry.yarnpkg.com/@interlay/monetary-js/-/monetary-js-0.7.3.tgz#0bf4c56b15fde2fd0573e6cac185b0703f368133" - integrity sha512-LbCtLRNjl1/LO8R1ay6lJwKgOC/J40YywF+qSuQ7hEjLIkAslY5dLH11heQgQW9hOmqCSS5fTUQWXhmYQr6Ksg== - dependencies: - "@types/big.js" "6.1.2" - big.js "6.1.1" - typescript "^4.3.2" - "@internationalized/date@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.0.1.tgz#66332e9ca8f59b7be010ca65d946bca430ba4b66" @@ -2574,6 +2566,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ== +"@noble/hashes@^1.2.0": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" + integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== + "@noble/secp256k1@1.7.1": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -6004,6 +6001,13 @@ resolved "https://registry.yarnpkg.com/@types/big.js/-/big.js-6.1.2.tgz#68a952b629a6aaa2b5855a2f63363d1e77f6dd91" integrity sha512-h24JIZ52rvSvi2jkpYDk2yLH99VzZoCJiSfDWwjst7TwJVuXN61XVCUlPCzRl7mxKEMsGf8z42Q+J4TZwU3z2w== +"@types/bitcoinjs-lib@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/bitcoinjs-lib/-/bitcoinjs-lib-5.0.0.tgz#f2905d673d1c4b42a91d64d95f1c464f1a48cb56" + integrity sha512-9zXjgmH2E8qEZ9gQ9GH+I6Cze3bweQbyXtR/X4RD3SdR5I4jdRPvmBrKmjegV3HZG03KNricjEoq+lQUtIXCKQ== + dependencies: + bitcoinjs-lib "*" + "@types/bn.js@^5.1.1": version "5.1.1" resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.1.tgz#b51e1b55920a4ca26e9285ff79936bbdec910682" @@ -7727,6 +7731,11 @@ base-x@^3.0.2: dependencies: safe-buffer "^5.0.1" +base-x@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a" + integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== + base64-js@^1.0.2, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -7762,6 +7771,11 @@ bech32@1.1.4, bech32@^1.1.2: resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== +bech32@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" + integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== + before-after-hook@^2.2.0: version "2.2.3" resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" @@ -7831,6 +7845,11 @@ bip174@^2.0.1: resolved "https://registry.yarnpkg.com/bip174/-/bip174-2.0.1.tgz#39cf8ca99e50ce538fb762589832f4481d07c254" integrity sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ== +bip174@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/bip174/-/bip174-2.1.0.tgz#cd3402581feaa5116f0f00a0eaee87a5843a2d30" + integrity sha512-lkc0XyiX9E9KiVAS1ZiOqK1xfiwvf4FXDDdkDq5crcDzOq+xGytY+14qCsqz7kCiy8rpN1CRNfacRhf9G3JNSA== + bip32@^2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/bip32/-/bip32-2.0.6.tgz#6a81d9f98c4cd57d05150c60d8f9e75121635134" @@ -7869,6 +7888,18 @@ bitcoin-ops@^1.3.0, bitcoin-ops@^1.4.0: resolved "https://registry.yarnpkg.com/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz#e45de620398e22fd4ca6023de43974ff42240278" integrity sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow== +bitcoinjs-lib@*: + version "6.1.1" + resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-6.1.1.tgz#3950c29fd96f07131e41a36a265b17ebd02b4a11" + integrity sha512-FYihfgTk29lt1eK2y48OtuarEDUnTprNBW3ctT8yHiOhvmeS3DzAVG6gI0VCvMkydz6UdlXlYNWIPqGD0SUYRQ== + dependencies: + "@noble/hashes" "^1.2.0" + bech32 "^2.0.0" + bip174 "^2.1.0" + bs58check "^3.0.1" + typeforce "^1.11.3" + varuint-bitcoin "^1.1.2" + bitcoinjs-lib@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-5.2.0.tgz#caf8b5efb04274ded1b67e0706960b93afb9d332" @@ -8121,6 +8152,13 @@ bs58@^4.0.0: dependencies: base-x "^3.0.2" +bs58@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279" + integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ== + dependencies: + base-x "^4.0.0" + bs58check@<3.0.0, bs58check@^2.0.0, bs58check@^2.1.1, bs58check@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" @@ -8130,6 +8168,14 @@ bs58check@<3.0.0, bs58check@^2.0.0, bs58check@^2.1.1, bs58check@^2.1.2: create-hash "^1.1.0" safe-buffer "^5.1.2" +bs58check@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-3.0.1.tgz#2094d13720a28593de1cba1d8c4e48602fdd841c" + integrity sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ== + dependencies: + "@noble/hashes" "^1.2.0" + bs58 "^5.0.0" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -17669,16 +17715,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== @@ -20553,7 +20597,7 @@ value-equal@^1.0.1: resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== -varuint-bitcoin@^1.0.4: +varuint-bitcoin@^1.0.4, varuint-bitcoin@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz#e76c138249d06138b480d4c5b40ef53693e24e92" integrity sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==