From b04bedae7b8eb9a8b4a51bcf25c476945095e592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sim=C3=A3o?= Date: Tue, 23 May 2023 10:44:46 +0100 Subject: [PATCH] feat: add useTransaction (#1189) --- .../ConfirmedIssueRequest/index.tsx | 37 ++--- .../ManualIssueExecutionUI/index.tsx | 44 +++--- .../components/DepositForm/DepositForm.tsx | 26 +--- .../PoolsInsights/PoolsInsights.tsx | 15 +- .../components/WithdrawForm/WithdrawForm.tsx | 25 +--- .../AMM/Swap/components/SwapForm/SwapForm.tsx | 55 +++----- src/pages/Bridge/BurnForm/index.tsx | 9 +- src/pages/Bridge/IssueForm/index.tsx | 12 +- src/pages/Bridge/RedeemForm/index.tsx | 27 ++-- .../CollateralModal/CollateralModal.tsx | 48 +++---- .../components/LoanForm/LoanForm.tsx | 59 +++++--- .../LoansInsights/LoansInsights.tsx | 30 ++-- .../Staking/ClaimRewardsButton/index.tsx | 32 ++--- src/pages/Staking/index.tsx | 123 +++++++--------- src/pages/Transfer/TransferForm/index.tsx | 12 +- .../Vaults/Vault/RequestIssueModal/index.tsx | 11 +- .../Vaults/Vault/RequestRedeemModal/index.tsx | 10 +- .../Vault/RequestReplacementModal/index.tsx | 10 +- .../Vault/UpdateCollateralModal/index.tsx | 8 +- .../Vault/components/Rewards/Rewards.tsx | 56 ++++---- .../DespositCollateralStep.tsx | 27 ++-- .../hooks/api/loans/use-loan-mutation.tsx | 49 ------- src/utils/hooks/transaction/index.ts | 2 + src/utils/hooks/transaction/types/amm.ts | 28 ++++ src/utils/hooks/transaction/types/escrow.ts | 46 ++++++ src/utils/hooks/transaction/types/index.ts | 79 +++++++++++ src/utils/hooks/transaction/types/issue.ts | 18 +++ src/utils/hooks/transaction/types/loans.ts | 62 ++++++++ src/utils/hooks/transaction/types/redeem.ts | 23 +++ src/utils/hooks/transaction/types/replace.ts | 13 ++ src/utils/hooks/transaction/types/rewards.ts | 13 ++ src/utils/hooks/transaction/types/tokens.ts | 13 ++ src/utils/hooks/transaction/types/vaults.ts | 23 +++ .../hooks/transaction/use-transaction.ts | 122 ++++++++++++++++ .../hooks/transaction/utils/extrinsic.ts | 133 ++++++++++++++++++ src/utils/hooks/transaction/utils/submit.ts | 107 ++++++++++++++ 36 files changed, 960 insertions(+), 447 deletions(-) delete mode 100644 src/utils/hooks/api/loans/use-loan-mutation.tsx create mode 100644 src/utils/hooks/transaction/index.ts create mode 100644 src/utils/hooks/transaction/types/amm.ts create mode 100644 src/utils/hooks/transaction/types/escrow.ts create mode 100644 src/utils/hooks/transaction/types/index.ts create mode 100644 src/utils/hooks/transaction/types/issue.ts create mode 100644 src/utils/hooks/transaction/types/loans.ts create mode 100644 src/utils/hooks/transaction/types/redeem.ts create mode 100644 src/utils/hooks/transaction/types/replace.ts create mode 100644 src/utils/hooks/transaction/types/rewards.ts create mode 100644 src/utils/hooks/transaction/types/tokens.ts create mode 100644 src/utils/hooks/transaction/types/vaults.ts create mode 100644 src/utils/hooks/transaction/use-transaction.ts create mode 100644 src/utils/hooks/transaction/utils/extrinsic.ts create mode 100644 src/utils/hooks/transaction/utils/submit.ts diff --git a/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx b/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx index 3a758d2989..e76d52fe2d 100644 --- a/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx +++ b/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx @@ -1,8 +1,7 @@ -import { ISubmittableResult } from '@polkadot/types/types'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import { FaCheckCircle } from 'react-icons/fa'; -import { useMutation, useQueryClient } from 'react-query'; +import { useQueryClient } from 'react-query'; import { toast } from 'react-toastify'; import { BTC_EXPLORER_TRANSACTION_API } from '@/config/blockstream-explorer-links'; @@ -16,7 +15,7 @@ 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 { submitExtrinsicPromise } from '@/utils/helpers/extrinsic'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import useQueryParams from '@/utils/hooks/use-query-params'; import ManualIssueExecutionUI from '../ManualIssueExecutionUI'; @@ -34,21 +33,15 @@ const ConfirmedIssueRequest = ({ request }: Props): JSX.Element => { const selectedPageIndex = selectedPage - 1; const queryClient = useQueryClient(); - // TODO: should type properly (`Relay`) - const executeMutation = useMutation( - (variables: any) => { - if (!variables.backingPayment.btcTxId) { - throw new Error('Bitcoin transaction ID not identified yet.'); - } - return submitExtrinsicPromise(window.bridge.issue.execute(variables.id, variables.backingPayment.btcTxId)); - }, - { - onSuccess: (_, variables) => { - queryClient.invalidateQueries([ISSUES_FETCHER, selectedPageIndex * TABLE_PAGE_LIMIT, TABLE_PAGE_LIMIT]); - toast.success(t('issue_page.successfully_executed', { id: variables.id })); - } + + // 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 ( <> @@ -82,16 +75,14 @@ const ConfirmedIssueRequest = ({ request }: Props): JSX.Element => {

- {executeMutation.isError && executeMutation.error && ( + {transaction.isError && transaction.error && ( { - executeMutation.reset(); + transaction.reset(); }} title='Error' - description={ - typeof executeMutation.error === 'string' ? executeMutation.error : executeMutation.error.message - } + 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 5aee169f45..c93aa9aae2 100644 --- a/src/legacy-components/IssueUI/IssueRequestStatusUI/ManualIssueExecutionUI/index.tsx +++ b/src/legacy-components/IssueUI/IssueRequestStatusUI/ManualIssueExecutionUI/index.tsx @@ -5,10 +5,9 @@ import { newAccountId, newMonetaryAmount } from '@interlay/interbtc-api'; -import { ISubmittableResult } from '@polkadot/types/types'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; -import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { useQuery, useQueryClient } from 'react-query'; import { toast } from 'react-toastify'; import { displayMonetaryAmount } from '@/common/utils/utils'; @@ -21,7 +20,7 @@ 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 { submitExtrinsicPromise } from '@/utils/helpers/extrinsic'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import useQueryParams from '@/utils/hooks/use-query-params'; // TODO: issue requests should not be typed here but further above in the app @@ -57,21 +56,13 @@ const ManualIssueExecutionUI = ({ request }: Props): JSX.Element => { const queryClient = useQueryClient(); - // TODO: should type properly (`Relay`) - const executeMutation = useMutation( - (variables: any) => { - if (!variables.backingPayment.btcTxId) { - throw new Error('Bitcoin transaction ID not identified yet.'); - } - return submitExtrinsicPromise(window.bridge.issue.execute(variables.id, variables.backingPayment.btcTxId)); - }, - { - onSuccess: (_, variables) => { - queryClient.invalidateQueries([ISSUES_FETCHER, selectedPageIndex * TABLE_PAGE_LIMIT, TABLE_PAGE_LIMIT]); - toast.success(t('issue_page.successfully_executed', { id: variables.id })); - } + 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 })); } - ); + }); const { data: vaultCapacity, error: vaultCapacityError } = useQuery({ queryKey: 'vault-capacity', @@ -91,7 +82,12 @@ const ManualIssueExecutionUI = ({ request }: Props): JSX.Element => { // TODO: should type properly (`Relay`) const handleExecute = (request: any) => () => { - executeMutation.mutate(request); + if (!request.backingPayment.btcTxId) { + console.error('Bitcoin transaction ID not identified yet.'); + return; + } + + transaction.execute(request.id, request.backingPayment.btcTxId); }; const backingPaymentAmount = newMonetaryAmount(request.backingPayment.amount, WRAPPED_TOKEN); @@ -135,7 +131,7 @@ const ManualIssueExecutionUI = ({ request }: Props): JSX.Element => { )} @@ -143,16 +139,14 @@ const ManualIssueExecutionUI = ({ request }: Props): JSX.Element => { wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL })} - {executeMutation.isError && executeMutation.error && ( + {transaction.isError && transaction.error && ( { - executeMutation.reset(); + transaction.reset(); }} title='Error' - description={ - typeof executeMutation.error === 'string' ? executeMutation.error : executeMutation.error.message - } + description={typeof transaction.error === 'string' ? transaction.error : transaction.error.message} /> )} diff --git a/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx b/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx index 4a7030bc4c..4baf947a1b 100644 --- a/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx +++ b/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx @@ -1,11 +1,8 @@ -import { CurrencyExt, LiquidityPool, newMonetaryAmount, PooledCurrencies } from '@interlay/interbtc-api'; -import { AccountId } from '@polkadot/types/interfaces'; -import { ISubmittableResult } from '@polkadot/types/types'; +import { CurrencyExt, LiquidityPool, newMonetaryAmount } from '@interlay/interbtc-api'; import { mergeProps } from '@react-aria/utils'; import Big from 'big.js'; import { ChangeEventHandler, RefObject, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useMutation } from 'react-query'; import { toast } from 'react-toastify'; import { displayMonetaryAmountInUSDFormat, newSafeMonetaryAmount } from '@/common/utils/utils'; @@ -21,10 +18,10 @@ import { } from '@/lib/form'; import { SlippageManager } from '@/pages/AMM/shared/components'; import { AMM_DEADLINE_INTERVAL } from '@/utils/constants/api'; -import { submitExtrinsic } from '@/utils/helpers/extrinsic'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import useAccountId from '@/utils/hooks/use-account-id'; import { PoolName } from '../PoolName'; @@ -35,17 +32,6 @@ import { DepositOutputAssets } from './DepositOutputAssets'; const isCustomAmountsMode = (form: ReturnType) => form.dirty && Object.values(form.touched).filter(Boolean).length > 0; -type DepositData = { - amounts: PooledCurrencies; - pool: LiquidityPool; - slippage: number; - deadline: number; - accountId: AccountId; -}; - -const mutateDeposit = ({ amounts, pool, slippage, deadline, accountId }: DepositData) => - submitExtrinsic(window.bridge.amm.addLiquidity(amounts, pool, slippage, deadline, accountId)); - type DepositFormProps = { pool: LiquidityPool; slippageModalRef: RefObject; @@ -65,7 +51,7 @@ const DepositForm = ({ pool, slippageModalRef, onDeposit }: DepositFormProps): J const governanceBalance = getBalance(GOVERNANCE_TOKEN.ticker)?.free || newMonetaryAmount(0, GOVERNANCE_TOKEN); - const depositMutation = useMutation(mutateDeposit, { + const transaction = useTransaction(Transaction.AMM_ADD_LIQUIDITY, { onSuccess: () => { onDeposit?.(); toast.success('Deposit successful'); @@ -85,7 +71,7 @@ const DepositForm = ({ pool, slippageModalRef, onDeposit }: DepositFormProps): J const deadline = await window.bridge.system.getFutureBlockNumber(AMM_DEADLINE_INTERVAL); - return depositMutation.mutate({ amounts, pool, slippage, deadline, accountId }); + return transaction.execute(amounts, pool, slippage, deadline, accountId); } catch (err: any) { toast.error(err.toString()); } @@ -106,7 +92,7 @@ const DepositForm = ({ pool, slippageModalRef, onDeposit }: DepositFormProps): J initialValues: defaultValues, validationSchema: depositLiquidityPoolSchema({ transactionFee: TRANSACTION_FEE_AMOUNT, governanceBalance, tokens }), onSubmit: handleSubmit, - disableValidation: depositMutation.isLoading + disableValidation: transaction.isLoading }); const handleChange: ChangeEventHandler = (e) => { @@ -203,7 +189,7 @@ const DepositForm = ({ pool, slippageModalRef, onDeposit }: DepositFormProps): J - + {t('amm.pools.add_liquidity')} diff --git a/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx b/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx index a845df1639..1689c20a32 100644 --- a/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx +++ b/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx @@ -1,16 +1,15 @@ import { LiquidityPool } from '@interlay/interbtc-api'; import Big from 'big.js'; import { useTranslation } from 'react-i18next'; -import { useMutation } from 'react-query'; import { toast } from 'react-toastify'; import { formatUSD } from '@/common/utils/utils'; import { Card, Dl, DlGroup } from '@/component-library'; import { AuthCTA } from '@/components'; import { calculateAccountLiquidityUSD, calculateTotalLiquidityUSD } from '@/pages/AMM/shared/utils'; -import { submitExtrinsic } from '@/utils/helpers/extrinsic'; import { AccountPoolsData } from '@/utils/hooks/api/amm/use-get-account-pools'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import { StyledDd, StyledDt } from './PoolsInsights.style'; import { calculateClaimableFarmingRewardUSD } from './utils'; @@ -55,17 +54,11 @@ const PoolsInsights = ({ pools, accountPoolsData, refetch }: PoolsInsightsProps) refetch(); }; - const mutateClaimRewards = async () => { - if (accountPoolsData !== undefined) { - await submitExtrinsic(window.bridge.amm.claimFarmingRewards(accountPoolsData.claimableRewards)); - } - }; - - const claimRewardsMutation = useMutation(mutateClaimRewards, { + const transaction = useTransaction(Transaction.AMM_CLAIM_REWARDS, { onSuccess: handleSuccess }); - const handleClickClaimRewards = () => claimRewardsMutation.mutate(); + const handleClickClaimRewards = () => accountPoolsData && transaction.execute(accountPoolsData.claimableRewards); const hasClaimableRewards = totalClaimableRewardUSD > 0; return ( @@ -88,7 +81,7 @@ const PoolsInsights = ({ pools, accountPoolsData, refetch }: PoolsInsightsProps) {formatUSD(totalClaimableRewardUSD, { compact: true })} {hasClaimableRewards && ( - + Claim )} diff --git a/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx b/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx index 3eeb392479..4f2ea60b55 100644 --- a/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx +++ b/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx @@ -1,11 +1,7 @@ -import { LiquidityPool, LpCurrency, newMonetaryAmount } from '@interlay/interbtc-api'; -import { MonetaryAmount } from '@interlay/monetary-js'; -import { AccountId } from '@polkadot/types/interfaces'; -import { ISubmittableResult } from '@polkadot/types/types'; +import { LiquidityPool, newMonetaryAmount } from '@interlay/interbtc-api'; import Big from 'big.js'; import { RefObject, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useMutation } from 'react-query'; import { toast } from 'react-toastify'; import { @@ -20,27 +16,16 @@ import { isFormDisabled, useForm, WITHDRAW_LIQUIDITY_POOL_FIELD } from '@/lib/fo import { WithdrawLiquidityPoolFormData, withdrawLiquidityPoolSchema } from '@/lib/form/schemas'; import { SlippageManager } from '@/pages/AMM/shared/components'; import { AMM_DEADLINE_INTERVAL } from '@/utils/constants/api'; -import { submitExtrinsic } from '@/utils/helpers/extrinsic'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import useAccountId from '@/utils/hooks/use-account-id'; import { PoolName } from '../PoolName'; import { WithdrawAssets } from './WithdrawAssets'; import { StyledDl } from './WithdrawForm.styles'; -type DepositData = { - amount: MonetaryAmount; - pool: LiquidityPool; - slippage: number; - deadline: number; - accountId: AccountId; -}; - -const mutateWithdraw = ({ amount, pool, slippage, deadline, accountId }: DepositData) => - submitExtrinsic(window.bridge.amm.removeLiquidity(amount, pool, slippage, deadline, accountId)); - type WithdrawFormProps = { pool: LiquidityPool; slippageModalRef: RefObject; @@ -55,7 +40,7 @@ const WithdrawForm = ({ pool, slippageModalRef, onWithdraw }: WithdrawFormProps) const prices = useGetPrices(); const { getBalance } = useGetBalances(); - const withdrawMutation = useMutation(mutateWithdraw, { + const transaction = useTransaction(Transaction.AMM_REMOVE_LIQUIDITY, { onSuccess: () => { onWithdraw?.(); toast.success('Withdraw successful'); @@ -85,7 +70,7 @@ const WithdrawForm = ({ pool, slippageModalRef, onWithdraw }: WithdrawFormProps) const amount = newMonetaryAmount(data[WITHDRAW_LIQUIDITY_POOL_FIELD] || 0, lpToken, true); const deadline = await window.bridge.system.getFutureBlockNumber(AMM_DEADLINE_INTERVAL); - return withdrawMutation.mutate({ amount, pool, deadline, slippage, accountId }); + return transaction.execute(amount, pool, slippage, deadline, accountId); } catch (err: any) { toast.error(err.toString()); } @@ -157,7 +142,7 @@ const WithdrawForm = ({ pool, slippageModalRef, onWithdraw }: WithdrawFormProps) - + {t('amm.pools.remove_liquidity')} diff --git a/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx b/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx index 716ca91398..a4d60bbcf6 100644 --- a/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx +++ b/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx @@ -1,12 +1,8 @@ import { CurrencyExt, LiquidityPool, newMonetaryAmount, Trade } from '@interlay/interbtc-api'; -import { MonetaryAmount } from '@interlay/monetary-js'; -import { AddressOrPair } from '@polkadot/api/types'; -import { ISubmittableResult } from '@polkadot/types/types'; import { mergeProps } from '@react-aria/utils'; import Big from 'big.js'; import { ChangeEventHandler, Key, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useMutation } from 'react-query'; import { useSelector } from 'react-redux'; import { toast } from 'react-toastify'; import { useDebounce } from 'react-use'; @@ -26,11 +22,11 @@ import { import { SlippageManager } from '@/pages/AMM/shared/components'; import { SwapPair } from '@/types/swap'; import { SWAP_PRICE_IMPACT_LIMIT } from '@/utils/constants/swap'; -import { submitExtrinsic } from '@/utils/helpers/extrinsic'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; import { useGetCurrencies } from '@/utils/hooks/api/use-get-currencies'; import { Prices, useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import useAccountId from '@/utils/hooks/use-account-id'; import { PriceImpactModal } from '../PriceImpactModal'; @@ -83,16 +79,6 @@ const getPoolPriceImpact = (trade: Trade | null | undefined, inputAmountUSD: num : new Big(0) }); -type SwapData = { - trade: Trade; - minimumAmountOut: MonetaryAmount; - recipient: AddressOrPair; - deadline: string | number; -}; - -const mutateSwap = ({ deadline, minimumAmountOut, recipient, trade }: SwapData) => - submitExtrinsic(window.bridge.amm.swap(trade, minimumAmountOut, recipient, deadline)); - type Props = { pair: SwapPair; liquidityPools: LiquidityPool[]; @@ -126,6 +112,18 @@ const SwapForm = ({ const { data: balances, getBalance, getAvailableBalance } = useGetBalances(); const { data: currencies } = useGetCurrencies(bridgeLoaded); + const transaction = useTransaction(Transaction.AMM_SWAP, { + onSuccess: () => { + toast.success('Swap successful'); + setTrade(undefined); + setInputAmount(undefined); + onSwap(); + }, + onError: (err) => { + toast.error(err.message); + } + }); + useDebounce( () => { if (!pair.input || !pair.output || !inputAmount) { @@ -141,18 +139,6 @@ const SwapForm = ({ [inputAmount, pair] ); - const swapMutation = useMutation(mutateSwap, { - onSuccess: () => { - toast.success('Swap successful'); - setTrade(undefined); - setInputAmount(undefined); - onSwap(); - }, - onError: (err) => { - toast.error(err.message); - } - }); - const inputBalance = pair.input && getAvailableBalance(pair.input.ticker); const outputBalance = pair.output && getAvailableBalance(pair.output.ticker); @@ -174,12 +160,7 @@ const SwapForm = ({ const deadline = await window.bridge.system.getFutureBlockNumber(30 * 60); - return swapMutation.mutate({ - trade, - recipient: accountId, - minimumAmountOut, - deadline - }); + return transaction.execute(trade, minimumAmountOut, accountId, deadline); } catch (err: any) { toast.error(err.toString()); } @@ -212,7 +193,7 @@ const SwapForm = ({ initialValues, validationSchema: swapSchema({ [SWAP_INPUT_AMOUNT_FIELD]: inputSchemaParams }), onSubmit: handleSubmit, - disableValidation: swapMutation.isLoading, + disableValidation: transaction.isLoading, validateOnMount: true }); @@ -239,11 +220,11 @@ const SwapForm = ({ useEffect(() => { const isAmountFieldEmpty = form.values[SWAP_INPUT_AMOUNT_FIELD] === ''; - if (isAmountFieldEmpty || !swapMutation.isSuccess) return; + if (isAmountFieldEmpty || !transaction.isSuccess) return; form.setFieldValue(SWAP_INPUT_AMOUNT_FIELD, ''); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [swapMutation.isSuccess]); + }, [transaction.isSuccess]); const handleChangeInput: ChangeEventHandler = (e) => { setInputAmount(e.target.value); @@ -341,7 +322,7 @@ const SwapForm = ({ /> {trade && } - + diff --git a/src/pages/Bridge/BurnForm/index.tsx b/src/pages/Bridge/BurnForm/index.tsx index 622b842372..2063218a57 100644 --- a/src/pages/Bridge/BurnForm/index.tsx +++ b/src/pages/Bridge/BurnForm/index.tsx @@ -25,11 +25,11 @@ import Tokens, { TokenOption } from '@/legacy-components/Tokens'; import { ForeignAssetIdLiteral } from '@/types/currency'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import STATUSES from '@/utils/constants/statuses'; -import { submitExtrinsic } from '@/utils/helpers/extrinsic'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; import { useGetCollateralCurrencies } from '@/utils/hooks/api/use-get-collateral-currencies'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; const WRAPPED_TOKEN_AMOUNT = 'wrapped-token-amount'; @@ -73,6 +73,8 @@ const BurnForm = (): JSX.Element | null => { const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE); const [submitError, setSubmitError] = React.useState(null); + const transaction = useTransaction(Transaction.REDEEM_BURN); + const handleUpdateCollateral = (collateral: TokenOption) => { const selectedCollateral = burnableCollateral?.find( (token: BurnableCollateral) => token.currency.ticker === collateral.token.ticker @@ -150,9 +152,8 @@ const BurnForm = (): JSX.Element | null => { const onSubmit = async (data: BurnFormData) => { try { setSubmitStatus(STATUSES.PENDING); - await submitExtrinsic( - window.bridge.redeem.burn(new BitcoinAmount(data[WRAPPED_TOKEN_AMOUNT]), selectedCollateral.currency) - ); + + await transaction.executeAsync(new BitcoinAmount(data[WRAPPED_TOKEN_AMOUNT]), selectedCollateral.currency); setSubmitStatus(STATUSES.RESOLVED); } catch (error) { diff --git a/src/pages/Bridge/IssueForm/index.tsx b/src/pages/Bridge/IssueForm/index.tsx index 6a871b7c1c..2e3b83db45 100644 --- a/src/pages/Bridge/IssueForm/index.tsx +++ b/src/pages/Bridge/IssueForm/index.tsx @@ -56,11 +56,11 @@ import genericFetcher, { GENERIC_FETCHER } from '@/services/fetchers/generic-fet import { ForeignAssetIdLiteral } from '@/types/currency'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import STATUSES from '@/utils/constants/statuses'; -import { getExtrinsicStatus, submitExtrinsic } from '@/utils/helpers/extrinsic'; import { getExchangeRate } from '@/utils/helpers/oracle'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import ManualVaultSelectUI from '../ManualVaultSelectUI'; import SubmittedIssueRequestModal from './SubmittedIssueRequestModal'; @@ -142,6 +142,8 @@ const IssueForm = (): JSX.Element | null => { }); useErrorHandler(requestLimitsError); + const transaction = useTransaction(Transaction.ISSUE_REQUEST); + React.useEffect(() => { if (!bridgeLoaded) return; if (!dispatch) return; @@ -321,18 +323,14 @@ const IssueForm = (): JSX.Element | null => { const collateralToken = await currencyIdToMonetaryCurrency(window.bridge.api, vaultId.currencies.collateral); - const extrinsicData = await window.bridge.issue.request( + const result = await transaction.executeAsync( monetaryBtcAmount, vaultId.accountId, collateralToken, false, // default vaults ); - // When requesting an issue, 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 - const finalizedStatus = getExtrinsicStatus('Finalized'); - const extrinsicResult = await submitExtrinsic(extrinsicData, finalizedStatus); - const issueRequests = await getIssueRequestsFromExtrinsicResult(window.bridge, extrinsicResult); + const issueRequests = await getIssueRequestsFromExtrinsicResult(window.bridge, result); // TODO: handle issue aggregation const issueRequest = issueRequests[0]; diff --git a/src/pages/Bridge/RedeemForm/index.tsx b/src/pages/Bridge/RedeemForm/index.tsx index 1248b5167d..357bfcf540 100644 --- a/src/pages/Bridge/RedeemForm/index.tsx +++ b/src/pages/Bridge/RedeemForm/index.tsx @@ -1,5 +1,10 @@ -import { CollateralCurrencyExt, InterbtcPrimitivesVaultId, newMonetaryAmount, Redeem } from '@interlay/interbtc-api'; -import { getRedeemRequestsFromExtrinsicResult } from '@interlay/interbtc-api'; +import { + CollateralCurrencyExt, + getRedeemRequestsFromExtrinsicResult, + InterbtcPrimitivesVaultId, + newMonetaryAmount, + Redeem +} from '@interlay/interbtc-api'; import { Bitcoin, BitcoinAmount, ExchangeRate } from '@interlay/monetary-js'; import Big from 'big.js'; import clsx from 'clsx'; @@ -44,11 +49,11 @@ import { ForeignAssetIdLiteral } from '@/types/currency'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import STATUSES from '@/utils/constants/statuses'; import { getColorShade } from '@/utils/helpers/colors'; -import { getExtrinsicStatus, submitExtrinsic } from '@/utils/helpers/extrinsic'; import { getExchangeRate } from '@/utils/helpers/oracle'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import ManualVaultSelectUI from '../ManualVaultSelectUI'; import SubmittedRedeemRequestModal from './SubmittedRedeemRequestModal'; @@ -114,6 +119,8 @@ const RedeemForm = (): JSX.Element | null => { const [selectedVault, setSelectedVault] = React.useState(); + const transaction = useTransaction(Transaction.REDEEM_REQUEST); + React.useEffect(() => { if (!monetaryWrappedTokenAmount) return; if (!maxRedeemableCapacity) return; @@ -295,16 +302,10 @@ const RedeemForm = (): JSX.Element | null => { const relevantVaults = new Map(); // FIXME: a bit of a dirty workaround with the capacity relevantVaults.set(vaultId, monetaryWrappedTokenAmount.mul(2)); - const extrinsicData = await window.bridge.redeem.request( - monetaryWrappedTokenAmount, - data[BTC_ADDRESS], - vaultId - ); - // When requesting a redeem, 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 - const finalizedStatus = getExtrinsicStatus('Finalized'); - const extrinsicResult = await submitExtrinsic(extrinsicData, finalizedStatus); - const redeemRequests = await getRedeemRequestsFromExtrinsicResult(window.bridge, extrinsicResult); + + const result = await transaction.executeAsync(monetaryWrappedTokenAmount, data[BTC_ADDRESS], vaultId); + + const redeemRequests = await getRedeemRequestsFromExtrinsicResult(window.bridge, result); // TODO: handle redeem aggregator const redeemRequest = redeemRequests[0]; diff --git a/src/pages/Loans/LoansOverview/components/CollateralModal/CollateralModal.tsx b/src/pages/Loans/LoansOverview/components/CollateralModal/CollateralModal.tsx index 9b545e17ff..2ec3d03b04 100644 --- a/src/pages/Loans/LoansOverview/components/CollateralModal/CollateralModal.tsx +++ b/src/pages/Loans/LoansOverview/components/CollateralModal/CollateralModal.tsx @@ -1,31 +1,19 @@ -import { CollateralPosition, CurrencyExt, LoanAsset } from '@interlay/interbtc-api'; -import { ISubmittableResult } from '@polkadot/types/types'; +import { CollateralPosition, LoanAsset } from '@interlay/interbtc-api'; import { TFunction, useTranslation } from 'react-i18next'; -import { useMutation } from 'react-query'; import { toast } from 'react-toastify'; import { Flex, Modal, ModalBody, ModalFooter, ModalHeader, ModalProps, Status } from '@/component-library'; import { AuthCTA } from '@/components'; import ErrorModal from '@/legacy-components/ErrorModal'; -import { submitExtrinsicPromise } from '@/utils/helpers/extrinsic'; import { useGetAccountLendingStatistics } from '@/utils/hooks/api/loans/use-get-account-lending-statistics'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import { useGetLTV } from '../../hooks/use-get-ltv'; import { BorrowLimit } from '../BorrowLimit'; import { LoanActionInfo } from '../LoanActionInfo'; import { StyledDescription } from './CollateralModal.style'; -type ToggleCollateralVariables = { isEnabling: boolean; underlyingCurrency: CurrencyExt }; - -const toggleCollateral = ({ isEnabling, underlyingCurrency }: ToggleCollateralVariables) => { - if (isEnabling) { - return submitExtrinsicPromise(window.bridge.loans.enableAsCollateral(underlyingCurrency)); - } else { - return submitExtrinsicPromise(window.bridge.loans.disableAsCollateral(underlyingCurrency)); - } -}; - type CollateralModalVariant = 'enable' | 'disable' | 'disable-error'; const getContentMap = (t: TFunction, variant: CollateralModalVariant, asset: LoanAsset) => @@ -73,14 +61,12 @@ const CollateralModal = ({ asset, position, onClose, ...props }: CollateralModal const { getLTV } = useGetLTV(); const prices = useGetPrices(); - const handleSuccess = () => { - toast.success('Successfully toggled collateral'); - onClose?.(); - refetch(); - }; - - const toggleCollateralMutation = useMutation(toggleCollateral, { - onSuccess: handleSuccess + const transaction = useTransaction({ + onSuccess: () => { + toast.success('Successfully toggled collateral'); + onClose?.(); + refetch(); + } }); if (!asset || !position) { @@ -100,9 +86,11 @@ const CollateralModal = ({ asset, position, onClose, ...props }: CollateralModal return onClose?.(); } - const isEnabling = variant === 'enable'; - - return toggleCollateralMutation.mutate({ isEnabling, underlyingCurrency: position.amount.currency }); + if (variant === 'enable') { + return transaction.execute(Transaction.LOANS_ENABLE_COLLATERAL, asset.currency); + } else { + return transaction.execute(Transaction.LOANS_DISABLE_COLLATERAL, asset.currency); + } }; return ( @@ -117,17 +105,17 @@ const CollateralModal = ({ asset, position, onClose, ...props }: CollateralModal - + {content.buttonLabel} - {toggleCollateralMutation.isError && ( + {transaction.isError && ( toggleCollateralMutation.reset()} + open={transaction.isError} + onClose={() => transaction.reset()} title='Error' - description={toggleCollateralMutation.error?.message || ''} + description={transaction.error?.message || ''} /> )} diff --git a/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx b/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx index bd89f6b57e..edc7763901 100644 --- a/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx +++ b/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx @@ -12,8 +12,8 @@ import { AuthCTA } from '@/components'; import { isFormDisabled, LoanFormData, loanSchema, LoanValidationParams, useForm } from '@/lib/form'; import { LoanAction } from '@/types/loans'; import { useGetAccountPositions } from '@/utils/hooks/api/loans/use-get-account-positions'; -import { useLoanMutation } from '@/utils/hooks/api/loans/use-loan-mutation'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import { useLoanFormData } from '../../hooks/use-loan-form-data'; import { isLendAsset } from '../../utils/is-loan-asset'; @@ -116,18 +116,45 @@ const LoanForm = ({ asset, variant, position, onChangeLoan }: LoanFormProps): JS [inputAmount] ); - const handleSuccess = () => { - toast.success(`Successful ${content.title.toLowerCase()}`); - onChangeLoan?.(); - refetch(); - }; + const transaction = useTransaction({ + onSuccess: () => { + toast.success(`Successful ${content.title.toLowerCase()}`); + onChangeLoan?.(); + refetch(); + }, + onError: (error: Error) => { + toast.error(error.message); + } + }); - const handleError = (error: Error) => { - toast.error(error.message); + 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 loanMutation = useLoanMutation({ onSuccess: handleSuccess, onError: handleError }); - const schemaParams: LoanValidationParams = { governanceBalance, transactionFee, @@ -135,16 +162,6 @@ const LoanForm = ({ asset, variant, position, onChangeLoan }: LoanFormProps): JS maxAmount: assetAmount.available }; - const handleSubmit = (data: LoanFormData) => { - try { - const submittedAmount = data[variant] || 0; - const submittedMonetaryAmount = newMonetaryAmount(submittedAmount, asset.currency, true); - loanMutation.mutate({ amount: submittedMonetaryAmount, loanType: variant, isMaxAmount }); - } catch (err: any) { - toast.error(err.toString()); - } - }; - const form = useForm({ initialValues: { [variant]: '' }, validationSchema: loanSchema(variant, schemaParams), @@ -199,7 +216,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 41bfd6a148..6ad85f8d82 100644 --- a/src/pages/Loans/LoansOverview/components/LoansInsights/LoansInsights.tsx +++ b/src/pages/Loans/LoansOverview/components/LoansInsights/LoansInsights.tsx @@ -1,20 +1,16 @@ -import { ISubmittableResult } from '@polkadot/types/types'; import { useTranslation } from 'react-i18next'; -import { useMutation } from 'react-query'; 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 { submitExtrinsic } from '@/utils/helpers/extrinsic'; 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'; import { StyledDd, StyledDt } from './LoansInsights.style'; -const mutateClaimRewards = () => submitExtrinsic(window.bridge.loans.claimAllSubsidyRewards()); - type LoansInsightsProps = { statistics?: AccountLendingStatistics; }; @@ -23,16 +19,14 @@ const LoansInsights = ({ statistics }: LoansInsightsProps): JSX.Element => { const { t } = useTranslation(); const { data: subsidyRewards, refetch } = useGetAccountSubsidyRewards(); - const handleSuccess = () => { - toast.success(t('successfully_claimed_rewards')); - refetch(); - }; - - const claimRewardsMutation = useMutation(mutateClaimRewards, { - onSuccess: handleSuccess + const transaction = useTransaction(Transaction.LOANS_CLAIM_REWARDS, { + onSuccess: () => { + toast.success(t('successfully_claimed_rewards')); + refetch(); + } }); - const handleClickClaimRewards = () => claimRewardsMutation.mutate(); + const handleClickClaimRewards = () => transaction.execute(); const { supplyAmountUSD, netAPY } = statistics || {}; @@ -76,18 +70,18 @@ const LoansInsights = ({ statistics }: LoansInsightsProps): JSX.Element => { {subsidyRewardsAmountLabel} {hasSubsidyRewards && ( - + Claim )} - {claimRewardsMutation.isError && ( + {transaction.isError && ( claimRewardsMutation.reset()} + open={transaction.isError} + onClose={() => transaction.reset()} title='Error' - description={claimRewardsMutation.error?.message || ''} + description={transaction.error?.message || ''} /> )} diff --git a/src/pages/Staking/ClaimRewardsButton/index.tsx b/src/pages/Staking/ClaimRewardsButton/index.tsx index 2ad34879cd..442da162c0 100644 --- a/src/pages/Staking/ClaimRewardsButton/index.tsx +++ b/src/pages/Staking/ClaimRewardsButton/index.tsx @@ -1,6 +1,5 @@ -import { ISubmittableResult } from '@polkadot/types/types'; import clsx from 'clsx'; -import { useMutation, useQueryClient } from 'react-query'; +import { useQueryClient } from 'react-query'; import { GOVERNANCE_TOKEN_SYMBOL } from '@/config/relay-chains'; import InterlayDenimOrKintsugiSupernovaContainedButton, { @@ -9,7 +8,7 @@ import InterlayDenimOrKintsugiSupernovaContainedButton, { import ErrorModal from '@/legacy-components/ErrorModal'; import { useSubstrateSecureState } from '@/lib/substrate'; import { GENERIC_FETCHER } from '@/services/fetchers/generic-fetcher'; -import { submitExtrinsic } from '@/utils/helpers/extrinsic'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; interface CustomProps { claimableRewardAmount: string; @@ -24,20 +23,15 @@ const ClaimRewardsButton = ({ const queryClient = useQueryClient(); - const claimRewardsMutation = useMutation( - () => { - return submitExtrinsic(window.bridge.escrow.withdrawRewards()); - }, - { - onSuccess: () => { - queryClient.invalidateQueries([GENERIC_FETCHER, 'escrow', 'getRewardEstimate', selectedAccount?.address]); - queryClient.invalidateQueries([GENERIC_FETCHER, 'escrow', 'getRewards', selectedAccount?.address]); - } + const transaction = useTransaction(Transaction.ESCROW_WITHDRAW_REWARDS, { + onSuccess: () => { + queryClient.invalidateQueries([GENERIC_FETCHER, 'escrow', 'getRewardEstimate', selectedAccount?.address]); + queryClient.invalidateQueries([GENERIC_FETCHER, 'escrow', 'getRewards', selectedAccount?.address]); } - ); + }); const handleClaimRewards = () => { - claimRewardsMutation.mutate(); + transaction.execute(); }; return ( @@ -45,19 +39,19 @@ const ClaimRewardsButton = ({ Claim {claimableRewardAmount} {GOVERNANCE_TOKEN_SYMBOL} Rewards - {claimRewardsMutation.isError && ( + {transaction.isError && ( { - claimRewardsMutation.reset(); + transaction.reset(); }} title='Error' - description={claimRewardsMutation.error?.message || ''} + description={transaction.error?.message || ''} /> )} diff --git a/src/pages/Staking/index.tsx b/src/pages/Staking/index.tsx index e7c8dfd505..043d6b1185 100644 --- a/src/pages/Staking/index.tsx +++ b/src/pages/Staking/index.tsx @@ -1,5 +1,4 @@ import { newMonetaryAmount } from '@interlay/interbtc-api'; -import { ISubmittableResult } from '@polkadot/types/types'; import Big from 'big.js'; import clsx from 'clsx'; import { add, format } from 'date-fns'; @@ -7,7 +6,7 @@ import * as React from 'react'; import { useErrorHandler, withErrorBoundary } from 'react-error-boundary'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { useQuery, useQueryClient } from 'react-query'; import { useSelector } from 'react-redux'; import { StoreType } from '@/common/types/util.types'; @@ -44,10 +43,10 @@ import { } from '@/services/fetchers/staking-transaction-fee-reserve-fetcher'; import { ZERO_GOVERNANCE_TOKEN_AMOUNT, ZERO_VOTE_GOVERNANCE_TOKEN_AMOUNT } from '@/utils/constants/currency'; import { YEAR_MONTH_DAY_PATTERN } from '@/utils/constants/date-time'; -import { submitExtrinsic } from '@/utils/helpers/extrinsic'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import { useSignMessage } from '@/utils/hooks/use-sign-message'; import BalancesUI from './BalancesUI'; @@ -100,11 +99,6 @@ interface StakedAmountAndEndBlock { endBlock: number; } -interface LockingAmountAndTime { - amount: GovernanceTokenMonetaryAmount; - time: number; // Weeks -} - const Staking = (): JSX.Element => { const [blockLockTimeExtension, setBlockLockTimeExtension] = React.useState(0); @@ -248,63 +242,25 @@ const Staking = (): JSX.Element => { ); useErrorHandler(transactionFeeReserveError); - const initialStakeMutation = useMutation( - (variables: LockingAmountAndTime) => { - if (currentBlockNumber === undefined) { - throw new Error('Something went wrong!'); - } - const unlockHeight = currentBlockNumber + convertWeeksToBlockNumbers(variables.time); - - return submitExtrinsic(window.bridge.escrow.createLock(variables.amount, unlockHeight)); - }, - { - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [GENERIC_FETCHER, 'escrow'] }); - reset({ - [LOCKING_AMOUNT]: '0.0', - [LOCK_TIME]: '0' - }); - } + const initialStakeTransaction = useTransaction(Transaction.ESCROW_CREATE_LOCK, { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [GENERIC_FETCHER, 'escrow'] }); + reset({ + [LOCKING_AMOUNT]: '0.0', + [LOCK_TIME]: '0' + }); } - ); - - const moreStakeMutation = useMutation( - (variables: LockingAmountAndTime) => { - return (async () => { - if (stakedAmountAndEndBlock === undefined) { - throw new Error('Something went wrong!'); - } + }); - if (checkIncreaseLockAmountAndExtendLockTime(variables.time, variables.amount)) { - const unlockHeight = stakedAmountAndEndBlock.endBlock + convertWeeksToBlockNumbers(variables.time); - - const txs = [ - window.bridge.api.tx.escrow.increaseAmount(variables.amount.toString(true)), - window.bridge.api.tx.escrow.increaseUnlockHeight(unlockHeight) - ]; - const batch = window.bridge.api.tx.utility.batchAll(txs); - await submitExtrinsic({ extrinsic: batch }); - } else if (checkOnlyIncreaseLockAmount(variables.time, variables.amount)) { - await submitExtrinsic(window.bridge.escrow.increaseAmount(variables.amount)); - } else if (checkOnlyExtendLockTime(variables.time, variables.amount)) { - const unlockHeight = stakedAmountAndEndBlock.endBlock + convertWeeksToBlockNumbers(variables.time); - - await submitExtrinsic(window.bridge.escrow.increaseUnlockHeight(unlockHeight)); - } else { - throw new Error('Something went wrong!'); - } - })(); - }, - { - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: [GENERIC_FETCHER, 'escrow'] }); - reset({ - [LOCKING_AMOUNT]: '0.0', - [LOCK_TIME]: '0' - }); - } + const existingStakeTransaction = useTransaction({ + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [GENERIC_FETCHER, 'escrow'] }); + reset({ + [LOCKING_AMOUNT]: '0.0', + [LOCK_TIME]: '0' + }); } - ); + }); React.useEffect(() => { if (isValidating || !isValid || !estimatedRewardAmountAndAPYRefetch) return; @@ -409,15 +365,30 @@ const Staking = (): JSX.Element => { const numberTime = parseInt(lockTimeWithFallback); if (votingBalanceGreaterThanZero) { - moreStakeMutation.mutate({ - amount: monetaryAmount, - time: numberTime - }); + if (stakedAmountAndEndBlock === undefined) { + throw new Error('Something went wrong!'); + } + + if (checkIncreaseLockAmountAndExtendLockTime(numberTime, monetaryAmount)) { + const unlockHeight = stakedAmountAndEndBlock.endBlock + convertWeeksToBlockNumbers(numberTime); + + existingStakeTransaction.execute( + Transaction.ESCROW_INCREASE_LOOKED_TIME_AND_AMOUNT, + monetaryAmount.toString(true), + unlockHeight + ); + } else if (checkOnlyIncreaseLockAmount(numberTime, monetaryAmount)) { + existingStakeTransaction.execute(Transaction.ESCROW_INCREASE_LOCKED_AMOUNT, monetaryAmount); + } else if (checkOnlyExtendLockTime(numberTime, monetaryAmount)) { + const unlockHeight = stakedAmountAndEndBlock.endBlock + convertWeeksToBlockNumbers(numberTime); + + existingStakeTransaction.execute(Transaction.ESCROW_INCREASE_LOCKED_TIME, unlockHeight); + } else { + throw new Error('Something went wrong!'); + } } else { - initialStakeMutation.mutate({ - amount: monetaryAmount, - time: numberTime - }); + const unlockHeight = currentBlockNumber + convertWeeksToBlockNumbers(numberTime); + initialStakeTransaction.execute(monetaryAmount, unlockHeight); } }; @@ -856,7 +827,7 @@ const Staking = (): JSX.Element => { size='large' type='submit' disabled={initializing || unlockFirst || !isValid} - loading={initialStakeMutation.isLoading || moreStakeMutation.isLoading} + loading={initialStakeTransaction.isLoading || existingStakeTransaction.isLoading} > {submitButtonLabel}{' '} {unlockFirst ? ( @@ -866,15 +837,15 @@ const Staking = (): JSX.Element => { - {(initialStakeMutation.isError || moreStakeMutation.isError) && ( + {(initialStakeTransaction.isError || existingStakeTransaction.isError) && ( { - initialStakeMutation.reset(); - moreStakeMutation.reset(); + initialStakeTransaction.reset(); + existingStakeTransaction.reset(); }} title='Error' - description={initialStakeMutation.error?.message || moreStakeMutation.error?.message || ''} + description={initialStakeTransaction.error?.message || existingStakeTransaction.error?.message || ''} /> )} diff --git a/src/pages/Transfer/TransferForm/index.tsx b/src/pages/Transfer/TransferForm/index.tsx index e588456dc1..a488cd288f 100644 --- a/src/pages/Transfer/TransferForm/index.tsx +++ b/src/pages/Transfer/TransferForm/index.tsx @@ -18,8 +18,8 @@ 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 { submitExtrinsic } from '@/utils/helpers/extrinsic'; import isValidPolkadotAddress from '@/utils/helpers/is-valid-polkadot-address'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import TokenAmountField from '../TokenAmountField'; @@ -50,6 +50,8 @@ const TransferForm = (): JSX.Element => { const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE); const [submitError, setSubmitError] = React.useState(null); + const transaction = useTransaction(Transaction.TOKENS_TRANSFER); + const onSubmit = async (data: TransferFormData) => { if (!activeToken) return; if (data[TRANSFER_AMOUNT] === undefined) return; @@ -57,11 +59,9 @@ const TransferForm = (): JSX.Element => { try { setSubmitStatus(STATUSES.PENDING); - await submitExtrinsic( - window.bridge.tokens.transfer( - data[RECIPIENT_ADDRESS], - newMonetaryAmount(data[TRANSFER_AMOUNT], activeToken.token, true) - ) + await transaction.executeAsync( + data[RECIPIENT_ADDRESS], + newMonetaryAmount(data[TRANSFER_AMOUNT], activeToken.token, true) ); setSubmitStatus(STATUSES.RESOLVED); diff --git a/src/pages/Vaults/Vault/RequestIssueModal/index.tsx b/src/pages/Vaults/Vault/RequestIssueModal/index.tsx index 91c08d48cb..4e9215ce82 100644 --- a/src/pages/Vaults/Vault/RequestIssueModal/index.tsx +++ b/src/pages/Vaults/Vault/RequestIssueModal/index.tsx @@ -44,11 +44,11 @@ import SubmittedIssueRequestModal from '@/pages/Bridge/IssueForm/SubmittedIssueR import { ForeignAssetIdLiteral } from '@/types/currency'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import STATUSES from '@/utils/constants/statuses'; -import { getExtrinsicStatus, submitExtrinsic } from '@/utils/helpers/extrinsic'; import { getExchangeRate } from '@/utils/helpers/oracle'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import useAccountId from '@/utils/hooks/use-account-id'; const WRAPPED_TOKEN_AMOUNT = 'amount'; @@ -108,6 +108,8 @@ const RequestIssueModal = ({ onClose, open, collateralToken, vaultAddress }: Pro const vaultAccountId = useAccountId(vaultAddress); + const transaction = useTransaction(Transaction.ISSUE_REQUEST); + React.useEffect(() => { if (!bridgeLoaded) return; if (!handleError) return; @@ -180,17 +182,14 @@ const RequestIssueModal = ({ onClose, open, collateralToken, vaultAddress }: Pro const vaults = await window.bridge.vaults.getVaultsWithIssuableTokens(); - const extrinsicData = await window.bridge.issue.request( + const extrinsicResult = await transaction.executeAsync( wrappedTokenAmount, vaultAccountId, collateralToken, false, // default vaults ); - // When requesting an issue, 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 - const finalizedStatus = getExtrinsicStatus('Finalized'); - const extrinsicResult = await submitExtrinsic(extrinsicData, finalizedStatus); + const issueRequests = await getIssueRequestsFromExtrinsicResult(window.bridge, extrinsicResult); // TODO: handle issue aggregation diff --git a/src/pages/Vaults/Vault/RequestRedeemModal/index.tsx b/src/pages/Vaults/Vault/RequestRedeemModal/index.tsx index 300211d7ec..dde21974e5 100644 --- a/src/pages/Vaults/Vault/RequestRedeemModal/index.tsx +++ b/src/pages/Vaults/Vault/RequestRedeemModal/index.tsx @@ -17,7 +17,7 @@ import ErrorMessage from '@/legacy-components/ErrorMessage'; import NumberInput from '@/legacy-components/NumberInput'; import TextField from '@/legacy-components/TextField'; import InterlayModal, { InterlayModalInnerWrapper, InterlayModalTitle } from '@/legacy-components/UI/InterlayModal'; -import { getExtrinsicStatus, submitExtrinsic } from '@/utils/helpers/extrinsic'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; const WRAPPED_TOKEN_AMOUNT = 'amount'; const BTC_ADDRESS = 'btc-address'; @@ -47,6 +47,8 @@ const RequestRedeemModal = ({ onClose, open, collateralToken, vaultAddress, lock const { t } = useTranslation(); const focusRef = React.useRef(null); + const transaction = useTransaction(Transaction.REDEEM_REQUEST); + const onSubmit = handleSubmit(async (data) => { setRequestPending(true); try { @@ -61,11 +63,7 @@ const RequestRedeemModal = ({ onClose, open, collateralToken, vaultAddress, lock } const vaultId = newVaultId(window.bridge.api, vaultAddress, collateralToken, WRAPPED_TOKEN); - const extrinsicData = await window.bridge.redeem.request(amountPolkaBtc, data[BTC_ADDRESS], vaultId); - // When requesting a redeem, 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 - const finalizedStatus = getExtrinsicStatus('Finalized'); - await submitExtrinsic(extrinsicData, finalizedStatus); + await transaction.executeAsync(amountPolkaBtc, data[BTC_ADDRESS], vaultId); queryClient.invalidateQueries(['vaultsOverview', vaultAddress, collateralToken.ticker]); diff --git a/src/pages/Vaults/Vault/RequestReplacementModal/index.tsx b/src/pages/Vaults/Vault/RequestReplacementModal/index.tsx index b296f04bf0..a92acc73b2 100644 --- a/src/pages/Vaults/Vault/RequestReplacementModal/index.tsx +++ b/src/pages/Vaults/Vault/RequestReplacementModal/index.tsx @@ -25,9 +25,9 @@ import PrimaryColorEllipsisLoader from '@/legacy-components/PrimaryColorEllipsis import InterlayModal, { InterlayModalInnerWrapper, InterlayModalTitle } from '@/legacy-components/UI/InterlayModal'; import { GENERIC_FETCHER } from '@/services/fetchers/generic-fetcher'; import STATUSES from '@/utils/constants/statuses'; -import { getExtrinsicStatus, submitExtrinsic } from '@/utils/helpers/extrinsic'; import { getExchangeRate } from '@/utils/helpers/oracle'; import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; const AMOUNT = 'amount'; @@ -78,6 +78,8 @@ const RequestReplacementModal = ({ ); const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE); + const transaction = useTransaction(Transaction.REPLACE_REQUEST); + useEffect(() => { if (!bridgeLoaded) return; if (!handleError) return; @@ -105,10 +107,8 @@ const RequestReplacementModal = ({ try { setSubmitStatus(STATUSES.PENDING); const amountPolkaBtc = new BitcoinAmount(data[AMOUNT]); - // 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 - const finalizedStatus = getExtrinsicStatus('Finalized'); - submitExtrinsic(window.bridge.replace.request(amountPolkaBtc, collateralToken), finalizedStatus); + + await transaction.executeAsync(amountPolkaBtc, collateralToken); const vaultId = window.bridge.api.createType(ACCOUNT_ID_TYPE_NAME, vaultAddress); queryClient.invalidateQueries([GENERIC_FETCHER, 'mapReplaceRequests', vaultId]); diff --git a/src/pages/Vaults/Vault/UpdateCollateralModal/index.tsx b/src/pages/Vaults/Vault/UpdateCollateralModal/index.tsx index 772fa93e3d..dad669da97 100644 --- a/src/pages/Vaults/Vault/UpdateCollateralModal/index.tsx +++ b/src/pages/Vaults/Vault/UpdateCollateralModal/index.tsx @@ -21,10 +21,10 @@ 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 { submitExtrinsic, submitExtrinsicPromise } from '@/utils/helpers/extrinsic'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; enum CollateralUpdateStatus { Close, @@ -129,6 +129,8 @@ const UpdateCollateralModal = ({ ); useErrorHandler(vaultCollateralizationError); + const transaction = useTransaction(); + const handleClose = chain(() => resetField(COLLATERAL_TOKEN_AMOUNT), onClose); const onSubmit = async (data: UpdateCollateralFormData) => { @@ -142,9 +144,9 @@ const UpdateCollateralModal = ({ true ) as MonetaryAmount; if (collateralUpdateStatus === CollateralUpdateStatus.Deposit) { - await submitExtrinsic(window.bridge.vaults.depositCollateral(collateralTokenAmount)); + await transaction.executeAsync(Transaction.VAULTS_DEPOSIT_COLLATERAL, collateralTokenAmount); } else if (collateralUpdateStatus === CollateralUpdateStatus.Withdraw) { - await submitExtrinsicPromise(window.bridge.vaults.withdrawCollateral(collateralTokenAmount)); + await transaction.executeAsync(Transaction.VAULTS_WITHDRAW_COLLATERAL, collateralTokenAmount); } else { throw new Error('Something went wrong!'); } diff --git a/src/pages/Vaults/Vault/components/Rewards/Rewards.tsx b/src/pages/Vaults/Vault/components/Rewards/Rewards.tsx index e2bc840f77..2b8cedbe6b 100644 --- a/src/pages/Vaults/Vault/components/Rewards/Rewards.tsx +++ b/src/pages/Vaults/Vault/components/Rewards/Rewards.tsx @@ -1,7 +1,6 @@ import { CollateralCurrencyExt, newVaultId, WrappedCurrency, WrappedIdLiteral } from '@interlay/interbtc-api'; -import { ISubmittableResult } from '@polkadot/types/types'; import Big from 'big.js'; -import { useMutation, useQueryClient } from 'react-query'; +import { useQueryClient } from 'react-query'; import { toast } from 'react-toastify'; import { formatNumber, formatUSD } from '@/common/utils/utils'; @@ -10,8 +9,8 @@ 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 { submitExtrinsicPromise } from '@/utils/helpers/extrinsic'; import { VaultData } from '@/utils/hooks/api/vaults/get-vault-data'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import useAccountId from '@/utils/hooks/use-account-id'; import { InsightListItem, InsightsList } from '../InsightsList'; @@ -49,31 +48,24 @@ const Rewards = ({ const queryClient = useQueryClient(); const vaultAccountId = useAccountId(vaultAddress); - const claimRewardsMutation = useMutation( - () => { - if (vaultAccountId === undefined) { - throw new Error('Something went wrong!'); - } - - const vaultId = newVaultId( - window.bridge.api, - vaultAccountId.toString(), - collateralToken, - WRAPPED_TOKEN as WrappedCurrency - ); - - return submitExtrinsicPromise(window.bridge.rewards.withdrawRewards(vaultId)); - }, - { - onSuccess: () => { - queryClient.invalidateQueries(['vaultsOverview', vaultAddress, collateralToken.ticker]); - toast.success('Your rewards were successfully withdrawn.'); - } + const transaction = useTransaction(Transaction.REWARDS_WITHDRAW, { + onSuccess: () => { + queryClient.invalidateQueries(['vaultsOverview', vaultAddress, collateralToken.ticker]); + toast.success('Your rewards were successfully withdrawn.'); } - ); + }); const handleClickWithdrawRewards = () => { - claimRewardsMutation.mutate(); + if (vaultAccountId === undefined) return; + + const vaultId = newVaultId( + window.bridge.api, + vaultAccountId.toString(), + collateralToken, + WRAPPED_TOKEN as WrappedCurrency + ); + + transaction.execute(vaultId); }; const hasWithdrawableRewards = @@ -87,11 +79,11 @@ const Rewards = ({ size='small' variant='outlined' onClick={handleClickWithdrawRewards} - disabled={!hasWithdrawableRewards || claimRewardsMutation.isLoading} - $loading={claimRewardsMutation.isLoading} + disabled={!hasWithdrawableRewards || transaction.isLoading} + $loading={transaction.isLoading} > {/* TODO: temporary approach. Loading spinner should be added to the CTA itself */} - {claimRewardsMutation.isLoading && ( + {transaction.isLoading && ( @@ -99,12 +91,12 @@ const Rewards = ({ Withdraw all rewards )} - {claimRewardsMutation.isError && ( + {transaction.isError && ( claimRewardsMutation.reset()} + open={transaction.isError} + onClose={() => transaction.reset()} title='Error' - description={claimRewardsMutation.error?.message || ''} + description={transaction.error?.message || ''} /> )} diff --git a/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/DespositCollateralStep.tsx b/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/DespositCollateralStep.tsx index 32f28166d1..4fbe4efd89 100644 --- a/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/DespositCollateralStep.tsx +++ b/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/DespositCollateralStep.tsx @@ -1,9 +1,7 @@ import { CollateralCurrencyExt, newMonetaryAmount } from '@interlay/interbtc-api'; import { MonetaryAmount } from '@interlay/monetary-js'; -import { ISubmittableResult } from '@polkadot/types/types'; import { useId } from '@react-aria/utils'; import { useTranslation } from 'react-i18next'; -import { useMutation } from 'react-query'; import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/common/utils/utils'; import { CTA, ModalBody, ModalDivider, ModalFooter, ModalHeader, Span, Stack, TokenInput } from '@/component-library'; @@ -16,8 +14,8 @@ import { isFormDisabled, useForm } from '@/lib/form'; -import { submitExtrinsic } from '@/utils/helpers/extrinsic'; import { StepComponentProps, withStep } from '@/utils/hocs/step'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import { useDepositCollateral } from '../../utils/use-deposit-collateral'; import { StyledDd, StyledDItem, StyledDl, StyledDt, StyledHr } from './CreateVaultWizard.styles'; @@ -39,6 +37,10 @@ const DepositCollateralStep = ({ const { t } = useTranslation(); const { collateral, fee, governance } = useDepositCollateral(collateralCurrency, minCollateralAmount); + const transaction = useTransaction(Transaction.VAULTS_REGISTER_NEW_COLLATERAL, { + onSuccess: onSuccessfulDeposit + }); + const validationParams = { minAmount: collateral.min.raw, maxAmount: collateral.balance.raw, @@ -50,7 +52,7 @@ const DepositCollateralStep = ({ if (!data.deposit) return; const amount = newMonetaryAmount(data.deposit || 0, collateral.currency, true); - registerNewVaultMutation.mutate(amount); + transaction.execute(amount); }; const form = useForm({ @@ -59,13 +61,6 @@ const DepositCollateralStep = ({ onSubmit: handleSubmit }); - const registerNewVaultMutation = useMutation>( - (collateralAmount) => submitExtrinsic(window.bridge.vaults.registerNewCollateralVault(collateralAmount)), - { - onSuccess: onSuccessfulDeposit - } - ); - const inputCollateralAmount = newSafeMonetaryAmount(form.values.deposit || 0, collateral.currency, true); const isBtnDisabled = isFormDisabled(form); @@ -108,17 +103,17 @@ const DepositCollateralStep = ({ - + {t('vault.deposit_collateral')} - {registerNewVaultMutation.isError && ( + {transaction.isError && ( registerNewVaultMutation.reset()} + open={transaction.isError} + onClose={() => transaction.reset()} title='Error' - description={registerNewVaultMutation.error?.message || ''} + description={transaction.error?.message || ''} /> )} diff --git a/src/utils/hooks/api/loans/use-loan-mutation.tsx b/src/utils/hooks/api/loans/use-loan-mutation.tsx deleted file mode 100644 index 0057369b36..0000000000 --- a/src/utils/hooks/api/loans/use-loan-mutation.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { CurrencyExt } from '@interlay/interbtc-api'; -import { MonetaryAmount } from '@interlay/monetary-js'; -import { ISubmittableResult } from '@polkadot/types/types'; -import { useMutation, UseMutationResult } from 'react-query'; - -import { LoanAction } from '@/types/loans'; -import { submitExtrinsicPromise } from '@/utils/helpers/extrinsic'; - -type CreateLoanVariables = { loanType: LoanAction; amount: MonetaryAmount; isMaxAmount: boolean }; - -const mutateLoan = ({ loanType, amount, isMaxAmount }: CreateLoanVariables) => { - const extrinsicData = (() => { - switch (loanType) { - case 'lend': - return window.bridge.loans.lend(amount.currency, amount); - case 'withdraw': - if (isMaxAmount) { - return window.bridge.loans.withdrawAll(amount.currency); - } else { - return window.bridge.loans.withdraw(amount.currency, amount); - } - case 'borrow': - return window.bridge.loans.borrow(amount.currency, amount); - case 'repay': - if (isMaxAmount) { - return window.bridge.loans.repayAll(amount.currency); - } else { - return window.bridge.loans.repay(amount.currency, amount); - } - } - })(); - - return submitExtrinsicPromise(extrinsicData); -}; - -type UseLoanMutation = { onSuccess: () => void; onError: (error: Error) => void }; - -const useLoanMutation = ({ - onSuccess, - onError -}: UseLoanMutation): UseMutationResult => { - return useMutation(mutateLoan, { - onSuccess, - onError - }); -}; - -export { useLoanMutation }; -export type { UseLoanMutation }; diff --git a/src/utils/hooks/transaction/index.ts b/src/utils/hooks/transaction/index.ts new file mode 100644 index 0000000000..3a845f06ae --- /dev/null +++ b/src/utils/hooks/transaction/index.ts @@ -0,0 +1,2 @@ +export { Transaction } from './types'; +export { useTransaction } from './use-transaction'; diff --git a/src/utils/hooks/transaction/types/amm.ts b/src/utils/hooks/transaction/types/amm.ts new file mode 100644 index 0000000000..7b6cc56af9 --- /dev/null +++ b/src/utils/hooks/transaction/types/amm.ts @@ -0,0 +1,28 @@ +import { InterBtcApi } from '@interlay/interbtc-api'; + +import { Transaction } from '../types'; +import { TransactionAction } from '.'; + +interface SwapAction extends TransactionAction { + type: Transaction.AMM_SWAP; + args: Parameters; +} + +interface PoolAddLiquidityAction extends TransactionAction { + type: Transaction.AMM_ADD_LIQUIDITY; + args: Parameters; +} + +interface PoolRemoveLiquidityAction extends TransactionAction { + type: Transaction.AMM_REMOVE_LIQUIDITY; + args: Parameters; +} + +interface PoolClaimRewardsAction extends TransactionAction { + type: Transaction.AMM_CLAIM_REWARDS; + args: Parameters; +} + +type AMMActions = SwapAction | PoolAddLiquidityAction | PoolRemoveLiquidityAction | PoolClaimRewardsAction; + +export type { AMMActions }; diff --git a/src/utils/hooks/transaction/types/escrow.ts b/src/utils/hooks/transaction/types/escrow.ts new file mode 100644 index 0000000000..7003d1f796 --- /dev/null +++ b/src/utils/hooks/transaction/types/escrow.ts @@ -0,0 +1,46 @@ +import { InterBtcApi } from '@interlay/interbtc-api'; + +import { Transaction } from '../types'; +import { TransactionAction } from '.'; + +interface EscrowCreateLockAction extends TransactionAction { + type: Transaction.ESCROW_CREATE_LOCK; + args: Parameters; +} + +interface EscrowInscreaseLookedTimeAndAmountAction extends TransactionAction { + type: Transaction.ESCROW_INCREASE_LOOKED_TIME_AND_AMOUNT; + args: [ + ...Parameters, + ...Parameters + ]; +} +interface EscrowIncreaseLockAmountAction extends TransactionAction { + type: Transaction.ESCROW_INCREASE_LOCKED_AMOUNT; + args: Parameters; +} + +interface EscrowIncreaseLockTimeAction extends TransactionAction { + type: Transaction.ESCROW_INCREASE_LOCKED_TIME; + args: Parameters; +} + +interface EscrowWithdrawRewardsAction extends TransactionAction { + type: Transaction.ESCROW_WITHDRAW_REWARDS; + args: Parameters; +} + +interface EscrowWithdrawAction extends TransactionAction { + type: Transaction.ESCROW_WITHDRAW; + args: Parameters; +} + +type EscrowActions = + | EscrowCreateLockAction + | EscrowInscreaseLookedTimeAndAmountAction + | EscrowIncreaseLockAmountAction + | EscrowIncreaseLockTimeAction + | EscrowWithdrawRewardsAction + | EscrowWithdrawAction; + +export type { EscrowActions }; diff --git a/src/utils/hooks/transaction/types/index.ts b/src/utils/hooks/transaction/types/index.ts new file mode 100644 index 0000000000..538f820678 --- /dev/null +++ b/src/utils/hooks/transaction/types/index.ts @@ -0,0 +1,79 @@ +import { ExtrinsicStatus } from '@polkadot/types/interfaces'; + +import { AMMActions } from './amm'; +import { EscrowActions } from './escrow'; +import { IssueActions } from './issue'; +import { LoansActions } from './loans'; +import { RedeemActions } from './redeem'; +import { ReplaceActions } from './replace'; +import { RewardsActions } from './rewards'; +import { TokensActions } from './tokens'; +import { VaultsActions } from './vaults'; + +enum Transaction { + // Issue + ISSUE_REQUEST = 'ISSUE_REQUEST', + ISSUE_EXECUTE = 'ISSUE_EXECUTE', + // Redeem + REDEEM_REQUEST = 'REDEEM_REQUEST', + REDEEM_CANCEL = 'REDEEM_CANCEL', + REDEEM_BURN = 'REDEEM_BURN', + // Replace + REPLACE_REQUEST = 'REPLACE_REQUEST', + // Escrow + ESCROW_CREATE_LOCK = 'ESCROW_CREATE_LOCK', + ESCROW_INCREASE_LOCKED_TIME = 'ESCROW_INCREASE_LOCKED_TIME', + ESCROW_INCREASE_LOCKED_AMOUNT = 'ESCROW_INCREASE_LOCKED_AMOUNT', + ESCROW_INCREASE_LOOKED_TIME_AND_AMOUNT = 'ESCROW_INCREASE_LOOKED_TIME_AND_AMOUNT', + ESCROW_WITHDRAW_REWARDS = 'ESCROW_WITHDRAW_REWARDS', + ESCROW_WITHDRAW = 'ESCROW_WITHDRAW', + // Tokens + TOKENS_TRANSFER = 'TOKENS_TRANSFER', + // Vaults + VAULTS_DEPOSIT_COLLATERAL = 'VAULTS_DEPOSIT_COLLATERAL', + VAULTS_WITHDRAW_COLLATERAL = 'VAULTS_WITHDRAW_COLLATERAL', + VAULTS_REGISTER_NEW_COLLATERAL = 'VAULTS_REGISTER_NEW_COLLATERAL', + // Rewards + REWARDS_WITHDRAW = 'REWARDS_WITHDRAW', + // Loans + LOANS_CLAIM_REWARDS = 'LOANS_CLAIM_REWARDS', + LOANS_ENABLE_COLLATERAL = 'LOANS_ENABLE_COLLATERAL', + LOANS_DISABLE_COLLATERAL = 'LOANS_DISABLE_COLLATERAL', + LOANS_LEND = 'LOANS_LEND', + LOANS_WITHDRAW = 'LOANS_WITHDRAW', + LOANS_WITHDRAW_ALL = 'LOANS_WITHDRAW_ALL', + LOANS_BORROW = 'LOANS_BORROW', + LOANS_REPAY = 'LOANS_REPAY', + LOANS_REPAY_ALL = 'LOANS_REPAY_ALL', + // AMM + AMM_SWAP = 'AMM_SWAP', + AMM_ADD_LIQUIDITY = 'AMM_ADD_LIQUIDITY', + AMM_REMOVE_LIQUIDITY = 'AMM_REMOVE_LIQUIDITY', + AMM_CLAIM_REWARDS = 'AMM_CLAIM_REWARDS' +} + +type TransactionEvents = { + onReady?: () => void; +}; + +interface TransactionAction { + accountAddress: string; + events: TransactionEvents; + customStatus?: ExtrinsicStatus['type']; +} + +type TransactionActions = + | EscrowActions + | IssueActions + | RedeemActions + | ReplaceActions + | TokensActions + | LoansActions + | AMMActions + | VaultsActions + | RewardsActions; + +type TransactionArgs = Extract['args']; + +export { Transaction }; +export type { TransactionAction, TransactionActions, TransactionArgs, TransactionEvents }; diff --git a/src/utils/hooks/transaction/types/issue.ts b/src/utils/hooks/transaction/types/issue.ts new file mode 100644 index 0000000000..dfa3b9d5a3 --- /dev/null +++ b/src/utils/hooks/transaction/types/issue.ts @@ -0,0 +1,18 @@ +import { InterBtcApi } from '@interlay/interbtc-api'; + +import { Transaction } from '../types'; +import { TransactionAction } from '.'; + +interface IssueRequestAction extends TransactionAction { + type: Transaction.ISSUE_REQUEST; + args: Parameters; +} + +interface IssueExecuteAction extends TransactionAction { + type: Transaction.ISSUE_EXECUTE; + args: Parameters; +} + +type IssueActions = IssueRequestAction | IssueExecuteAction; + +export type { IssueActions }; diff --git a/src/utils/hooks/transaction/types/loans.ts b/src/utils/hooks/transaction/types/loans.ts new file mode 100644 index 0000000000..27797c68d9 --- /dev/null +++ b/src/utils/hooks/transaction/types/loans.ts @@ -0,0 +1,62 @@ +import { InterBtcApi } from '@interlay/interbtc-api'; + +import { Transaction } from '../types'; +import { TransactionAction } from '.'; + +interface LoansClaimRewardsAction extends TransactionAction { + type: Transaction.LOANS_CLAIM_REWARDS; + args: Parameters; +} + +interface LoansEnabledCollateralAction extends TransactionAction { + type: Transaction.LOANS_ENABLE_COLLATERAL; + args: Parameters; +} + +interface LoansDisabledCollateralAction extends TransactionAction { + type: Transaction.LOANS_DISABLE_COLLATERAL; + args: Parameters; +} + +interface LoansLendAction extends TransactionAction { + type: Transaction.LOANS_LEND; + args: Parameters; +} + +interface LoansWithdrawAction extends TransactionAction { + type: Transaction.LOANS_WITHDRAW; + args: Parameters; +} + +interface LoansWithdrawAllAction extends TransactionAction { + type: Transaction.LOANS_WITHDRAW_ALL; + args: Parameters; +} + +interface LoansBorrowAction extends TransactionAction { + type: Transaction.LOANS_BORROW; + args: Parameters; +} + +interface LoansRepayAction extends TransactionAction { + type: Transaction.LOANS_REPAY; + args: Parameters; +} + +interface LoansRepayAllAction extends TransactionAction { + type: Transaction.LOANS_REPAY_ALL; + args: Parameters; +} + +type LoansActions = + | LoansClaimRewardsAction + | LoansEnabledCollateralAction + | LoansDisabledCollateralAction + | LoansLendAction + | LoansWithdrawAction + | LoansWithdrawAllAction + | LoansBorrowAction + | LoansRepayAction + | LoansRepayAllAction; + +export type { LoansActions }; diff --git a/src/utils/hooks/transaction/types/redeem.ts b/src/utils/hooks/transaction/types/redeem.ts new file mode 100644 index 0000000000..1282278693 --- /dev/null +++ b/src/utils/hooks/transaction/types/redeem.ts @@ -0,0 +1,23 @@ +import { InterBtcApi } from '@interlay/interbtc-api'; + +import { Transaction } from '../types'; +import { TransactionAction } from '.'; + +interface RedeemCancelAction extends TransactionAction { + type: Transaction.REDEEM_CANCEL; + args: Parameters; +} + +interface RedeemBurnAction extends TransactionAction { + type: Transaction.REDEEM_BURN; + args: Parameters; +} + +interface RedeemRequestAction extends TransactionAction { + type: Transaction.REDEEM_REQUEST; + args: Parameters; +} + +type RedeemActions = RedeemRequestAction | RedeemCancelAction | RedeemBurnAction; + +export type { RedeemActions }; diff --git a/src/utils/hooks/transaction/types/replace.ts b/src/utils/hooks/transaction/types/replace.ts new file mode 100644 index 0000000000..4fab08e0e7 --- /dev/null +++ b/src/utils/hooks/transaction/types/replace.ts @@ -0,0 +1,13 @@ +import { InterBtcApi } from '@interlay/interbtc-api'; + +import { Transaction } from '../types'; +import { TransactionAction } from '.'; + +interface ReplaceRequestAction extends TransactionAction { + type: Transaction.REPLACE_REQUEST; + args: Parameters; +} + +type ReplaceActions = ReplaceRequestAction; + +export type { ReplaceActions }; diff --git a/src/utils/hooks/transaction/types/rewards.ts b/src/utils/hooks/transaction/types/rewards.ts new file mode 100644 index 0000000000..f77f61f7c4 --- /dev/null +++ b/src/utils/hooks/transaction/types/rewards.ts @@ -0,0 +1,13 @@ +import { InterBtcApi } from '@interlay/interbtc-api'; + +import { Transaction } from '.'; +import { TransactionAction } from '.'; + +interface RewardsWithdrawAction extends TransactionAction { + type: Transaction.REWARDS_WITHDRAW; + args: Parameters; +} + +type RewardsActions = RewardsWithdrawAction; + +export type { RewardsActions }; diff --git a/src/utils/hooks/transaction/types/tokens.ts b/src/utils/hooks/transaction/types/tokens.ts new file mode 100644 index 0000000000..a1c1e0da64 --- /dev/null +++ b/src/utils/hooks/transaction/types/tokens.ts @@ -0,0 +1,13 @@ +import { InterBtcApi } from '@interlay/interbtc-api'; + +import { Transaction } from '../types'; +import { TransactionAction } from '.'; + +interface TokensTransferAction extends TransactionAction { + type: Transaction.TOKENS_TRANSFER; + args: Parameters; +} + +type TokensActions = TokensTransferAction; + +export type { TokensActions }; diff --git a/src/utils/hooks/transaction/types/vaults.ts b/src/utils/hooks/transaction/types/vaults.ts new file mode 100644 index 0000000000..1c4040fd17 --- /dev/null +++ b/src/utils/hooks/transaction/types/vaults.ts @@ -0,0 +1,23 @@ +import { InterBtcApi } from '@interlay/interbtc-api'; + +import { Transaction } from '../types'; +import { TransactionAction } from '.'; + +interface VaultsDepositCollateralAction extends TransactionAction { + type: Transaction.VAULTS_DEPOSIT_COLLATERAL; + args: Parameters; +} + +interface VaultsWithdrawCollateralAction extends TransactionAction { + type: Transaction.VAULTS_WITHDRAW_COLLATERAL; + args: Parameters; +} + +interface VaultsRegisterNewCollateralAction extends TransactionAction { + type: Transaction.VAULTS_REGISTER_NEW_COLLATERAL; + args: Parameters; +} + +type VaultsActions = VaultsDepositCollateralAction | VaultsWithdrawCollateralAction | VaultsRegisterNewCollateralAction; + +export type { VaultsActions }; diff --git a/src/utils/hooks/transaction/use-transaction.ts b/src/utils/hooks/transaction/use-transaction.ts new file mode 100644 index 0000000000..d18291f94c --- /dev/null +++ b/src/utils/hooks/transaction/use-transaction.ts @@ -0,0 +1,122 @@ +import { ExtrinsicStatus } from '@polkadot/types/interfaces'; +import { ISubmittableResult } from '@polkadot/types/types'; +import { useCallback } from 'react'; +import { MutationFunction, useMutation, UseMutationOptions, UseMutationResult } from 'react-query'; + +import { useSubstrate } from '@/lib/substrate'; + +import { Transaction, TransactionActions, TransactionArgs } from './types'; +import { getExtrinsic, getStatus } from './utils/extrinsic'; +import { submitTransaction } from './utils/submit'; + +type UseTransactionOptions = Omit< + UseMutationOptions, + 'mutationFn' +> & { + customStatus?: ExtrinsicStatus['type']; +}; + +// 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; +}; + +// TODO: add feeEstimate and feeEstimateAsync +type ExecuteTypeArgs = { + execute(type: D, ...args: TransactionArgs): void; + executeAsync(type: D, ...args: TransactionArgs): Promise; +}; + +type InheritAttrs = Omit< + UseMutationResult, + 'mutate' | 'mutateAsync' +>; + +type UseTransactionResult = InheritAttrs & (ExecuteArgs | ExecuteTypeArgs); + +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); +}; + +// The three declared functions are use to infer types on diferent implementations +// TODO: missing xcm transaction +function useTransaction( + type: T, + options?: UseTransactionOptions +): Exclude, ExecuteTypeArgs>; +function useTransaction( + options?: UseTransactionOptions +): Exclude, ExecuteArgs>; +function useTransaction( + typeOrOptions?: T | UseTransactionOptions, + options?: UseTransactionOptions +): UseTransactionResult { + const { state } = useSubstrate(); + + const hasOnlyOptions = typeof typeOrOptions !== 'string'; + + const { mutate, mutateAsync, ...transactionMutation } = useMutation( + mutateTransaction, + (hasOnlyOptions ? typeOrOptions : options) as UseTransactionOptions + ); + + // Handles params for both type of implementations + const getParams = useCallback( + (args: Parameters['execute']>) => { + let params = {}; + + // Assign correct params for when transaction type is declared on hook params + if (typeof typeOrOptions === 'string') { + params = { type: typeOrOptions, args }; + } else { + // Assign correct params for when transaction type is declared on execution level + const [type, ...restArgs] = args; + params = { type, args: restArgs }; + } + + // Execution should only ran when authenticated + const accountAddress = state.selectedAccount?.address; + + // TODO: add event `onReady` + return { + ...params, + accountAddress, + customStatus: options?.customStatus + } as TransactionActions; + }, + [options?.customStatus, state.selectedAccount?.address, typeOrOptions] + ); + + const handleExecute = useCallback( + (...args: Parameters['execute']>) => { + const params = getParams(args); + + return mutate(params); + }, + [getParams, mutate] + ); + + const handleExecuteAsync = useCallback( + (...args: Parameters['executeAsync']>) => { + const params = getParams(args); + + return mutateAsync(params); + }, + [getParams, mutateAsync] + ); + + return { + ...transactionMutation, + execute: handleExecute, + executeAsync: handleExecuteAsync + }; +} + +export { useTransaction }; +export type { UseTransactionResult }; diff --git a/src/utils/hooks/transaction/utils/extrinsic.ts b/src/utils/hooks/transaction/utils/extrinsic.ts new file mode 100644 index 0000000000..23346819db --- /dev/null +++ b/src/utils/hooks/transaction/utils/extrinsic.ts @@ -0,0 +1,133 @@ +import { ExtrinsicData } from '@interlay/interbtc-api'; +import { ExtrinsicStatus } from '@polkadot/types/interfaces'; + +import { Transaction, TransactionActions } 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 => { + switch (params.type) { + /* START - AMM */ + case Transaction.AMM_SWAP: + return window.bridge.amm.swap(...params.args); + case Transaction.AMM_ADD_LIQUIDITY: + return window.bridge.amm.addLiquidity(...params.args); + case Transaction.AMM_REMOVE_LIQUIDITY: + return window.bridge.amm.removeLiquidity(...params.args); + case Transaction.AMM_CLAIM_REWARDS: + return window.bridge.amm.claimFarmingRewards(...params.args); + /* END - AMM */ + + /* START - ISSUE */ + case Transaction.ISSUE_REQUEST: + return window.bridge.issue.request(...params.args); + case Transaction.ISSUE_EXECUTE: + return window.bridge.issue.execute(...params.args); + /* END - ISSUE */ + + /* START - REDEEM */ + case Transaction.REDEEM_CANCEL: + return window.bridge.redeem.cancel(...params.args); + case Transaction.REDEEM_BURN: + return window.bridge.redeem.burn(...params.args); + case Transaction.REDEEM_REQUEST: + return window.bridge.redeem.request(...params.args); + /* END - REDEEM */ + + /* START - REPLACE */ + case Transaction.REPLACE_REQUEST: + return window.bridge.replace.request(...params.args); + /* END - REPLACE */ + + /* START - TOKENS */ + case Transaction.TOKENS_TRANSFER: + return window.bridge.tokens.transfer(...params.args); + /* END - TOKENS */ + + /* START - LOANS */ + case Transaction.LOANS_CLAIM_REWARDS: + return window.bridge.loans.claimAllSubsidyRewards(); + case Transaction.LOANS_BORROW: + return window.bridge.loans.borrow(...params.args); + case Transaction.LOANS_LEND: + return window.bridge.loans.lend(...params.args); + case Transaction.LOANS_REPAY: + return window.bridge.loans.repay(...params.args); + case Transaction.LOANS_REPAY_ALL: + return window.bridge.loans.repayAll(...params.args); + case Transaction.LOANS_WITHDRAW: + return window.bridge.loans.withdraw(...params.args); + case Transaction.LOANS_WITHDRAW_ALL: + return window.bridge.loans.withdrawAll(...params.args); + case Transaction.LOANS_DISABLE_COLLATERAL: + return window.bridge.loans.disableAsCollateral(...params.args); + case Transaction.LOANS_ENABLE_COLLATERAL: + return window.bridge.loans.enableAsCollateral(...params.args); + /* END - LOANS */ + + /* START - LOANS */ + 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); + /* START - REWARDS */ + case Transaction.REWARDS_WITHDRAW: + return window.bridge.rewards.withdrawRewards(...params.args); + /* START - REWARDS */ + /* END - LOANS */ + + /* START - ESCROW */ + case Transaction.ESCROW_CREATE_LOCK: + return window.bridge.escrow.createLock(...params.args); + case Transaction.ESCROW_INCREASE_LOCKED_AMOUNT: + return window.bridge.escrow.increaseAmount(...params.args); + case Transaction.ESCROW_INCREASE_LOCKED_TIME: + return window.bridge.escrow.increaseUnlockHeight(...params.args); + case Transaction.ESCROW_WITHDRAW: + return window.bridge.escrow.withdraw(...params.args); + case Transaction.ESCROW_WITHDRAW_REWARDS: + return window.bridge.escrow.withdrawRewards(...params.args); + case Transaction.ESCROW_INCREASE_LOOKED_TIME_AND_AMOUNT: { + const [amount, unlockHeight] = params.args; + const txs = [ + window.bridge.api.tx.escrow.increaseAmount(amount), + window.bridge.api.tx.escrow.increaseUnlockHeight(unlockHeight) + ]; + const batch = window.bridge.api.tx.utility.batchAll(txs); + + 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'; + } +}; + +export { getExtrinsic, getStatus }; diff --git a/src/utils/hooks/transaction/utils/submit.ts b/src/utils/hooks/transaction/utils/submit.ts new file mode 100644 index 0000000000..d1c832b023 --- /dev/null +++ b/src/utils/hooks/transaction/utils/submit.ts @@ -0,0 +1,107 @@ +import { ExtrinsicData } from '@interlay/interbtc-api'; +import { ApiPromise } from '@polkadot/api'; +import { AddressOrPair, SubmittableExtrinsic } from '@polkadot/api/types'; +import { DispatchError } from '@polkadot/types/interfaces'; +import { ExtrinsicStatus } from '@polkadot/types/interfaces/author'; +import { ISubmittableResult } from '@polkadot/types/types'; + +import { TransactionEvents } from '../types'; + +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, + expectedStatus?: ExtrinsicStatus['type'], + callbacks?: TransactionEvents +) => { + let isComplete = false; + + // Extrinsic status + let isReady = false; + + return new Promise((resolve, reject) => { + let unsubscribe: () => void; + + (extrinsicData.extrinsic as SubmittableExtrinsic<'promise'>) + .signAndSend(account, transactionOptions, callback) + .then((unsub) => (unsubscribe = unsub)) + .catch((error) => reject(error)); + + function callback(result: ISubmittableResult): void { + const { onReady } = callbacks || {}; + + if (!isReady && result.status.isReady) { + onReady?.(); + isReady = true; + } + + if (!isComplete) { + isComplete = expectedStatus === result.status.type; + } + + if (isComplete) { + resolve({ unsubscribe, result }); + } + } + }); +}; + +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(' ')}`); + } + + // Bad origin + if (isBadOrigin) { + return message.concat( + ` The error is caused by using an incorrect account. The error code is BadOrigin ${dispatchError}.` + ); + } + + return message.concat(` The error is ${dispatchError}.`); +}; + +/** + * Handles transaction submittion and error + * @param {ApiPromise} api polkadot api wrapper + * @param {AddressOrPair} account account address + * @param {ExtrinsicData} extrinsicData transaction extrinsic data + * @param {ExtrinsicStatus.type} expectedStatus status where the transaction is counted as fulfilled + * @param {TransactionEvents} callbacks a set of events emitted accross the lifecycle of the transaction (i.e Bro) + * @return {Promise} transaction data that also can contain meta data in case of error + */ +const submitTransaction = async ( + api: ApiPromise, + account: AddressOrPair, + extrinsicData: ExtrinsicData, + expectedStatus?: ExtrinsicStatus['type'], + callbacks?: TransactionEvents +): Promise => { + const { result, unsubscribe } = await handleTransaction(account, extrinsicData, expectedStatus, callbacks); + + unsubscribe(); + + const { dispatchError } = result; + + if (dispatchError) { + const message = getErrorMessage(api, dispatchError); + throw new Error(message); + } + + return result; +}; + +export { submitTransaction };