From 182fba62d4ab69435c90ed84838be9d47f9338bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Slan=C3=BD?= <47864599+peterslany@users.noreply.github.com> Date: Fri, 19 May 2023 11:15:10 +0200 Subject: [PATCH 001/241] feat: redirect when access from forbidden country is detected (#1209) --- src/components/Geoblock/Geoblock.tsx | 14 +++++++++ src/config/links.ts | 5 ++++ src/index.tsx | 43 +++++++++++++++------------- src/utils/hooks/use-geoblocking.ts | 22 ++++++++++++++ 4 files changed, 64 insertions(+), 20 deletions(-) create mode 100644 src/components/Geoblock/Geoblock.tsx create mode 100644 src/utils/hooks/use-geoblocking.ts diff --git a/src/components/Geoblock/Geoblock.tsx b/src/components/Geoblock/Geoblock.tsx new file mode 100644 index 0000000000..43493562d3 --- /dev/null +++ b/src/components/Geoblock/Geoblock.tsx @@ -0,0 +1,14 @@ +import React, { ReactNode } from 'react'; + +import { useGeoblocking } from '@/utils/hooks/use-geoblocking'; + +type Props = { + children: ReactNode; +}; + +const GeoblockingWrapper = ({ children }: Props): JSX.Element => { + useGeoblocking(); + return <>{children}; +}; + +export { GeoblockingWrapper }; diff --git a/src/config/links.ts b/src/config/links.ts index 8f1d7557b1..b2d84b6a02 100644 --- a/src/config/links.ts +++ b/src/config/links.ts @@ -21,8 +21,13 @@ const INTERLAY_DOS_AND_DONTS_DOCS_LINK = 'https://docs.interlay.io/#/vault/insta const BANXA_LINK = 'http://talisman.banxa.com/'; +const GEOBLOCK_API_ENDPOINT = '/check_access'; +const GEOBLOCK_REDIRECTION_LINK = 'https://www.interlay.io/geoblock'; + export { BANXA_LINK, + GEOBLOCK_API_ENDPOINT, + GEOBLOCK_REDIRECTION_LINK, INTERLAY_COMPANY_LINK, INTERLAY_CROWDLOAN_LINK, INTERLAY_DISCORD_LINK, diff --git a/src/index.tsx b/src/index.tsx index 4a8ef1ba46..4901f7d9a1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -18,6 +18,7 @@ import ThemeWrapper from '@/parts/ThemeWrapper'; import { Subscriptions } from '@/utils/hooks/api/tokens/use-balances-subscription'; import App from './App'; +import { GeoblockingWrapper } from './components/Geoblock/Geoblock'; import reportWebVitals from './reportWebVitals'; import { store } from './store'; @@ -30,26 +31,28 @@ const queryClient = new QueryClient(); // MEMO: temporarily removed React.StrictMode. We should add back when react-spectrum handles // it across their library. (Issue: https://github.com/adobe/react-spectrum/issues/779#issuecomment-1353734729) ReactDOM.render( - - - - - - - - - - - - - - - - - - - - , + + + + + + + + + + + + + + + + + + + + + + , document.getElementById('root') ); diff --git a/src/utils/hooks/use-geoblocking.ts b/src/utils/hooks/use-geoblocking.ts new file mode 100644 index 0000000000..6ca37d3a02 --- /dev/null +++ b/src/utils/hooks/use-geoblocking.ts @@ -0,0 +1,22 @@ +import { useEffect } from 'react'; + +import { GEOBLOCK_API_ENDPOINT, GEOBLOCK_REDIRECTION_LINK } from '@/config/links'; + +const useGeoblocking = (): void => { + useEffect(() => { + const checkCountry = async () => { + try { + const response = await fetch(GEOBLOCK_API_ENDPOINT); + if (response.status === 403) { + console.log('Access from forbidden country detected, user will be redirected.'); + window.location.replace(GEOBLOCK_REDIRECTION_LINK); + } + } catch (error) { + console.log(error); + } + }; + checkCountry(); + }, []); +}; + +export { useGeoblocking }; From ab6a5549984e92641bfe0701027dd25edeb4a10b Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Mon, 22 May 2023 09:15:34 +0100 Subject: [PATCH 002/241] Feature/updated transfer UI (#876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- package.json | 3 +- src/assets/icons/ArrowRightCircle.tsx | 13 + src/assets/icons/index.ts | 1 + src/assets/locales/en/translation.json | 1 + src/component-library/Label/Label.style.tsx | 1 + .../Select/SelectTrigger.tsx | 2 +- src/component-library/index.tsx | 2 + src/component-library/theme/theme.ts | 10 +- src/components/AccountSelect/AccountLabel.tsx | 34 ++ src/components/AccountSelect/AccountList.tsx | 58 +++ .../AccountSelect/AccountListModal.tsx | 34 ++ .../AccountSelect/AccountSelect.style.tsx | 68 +++ .../AccountSelect/AccountSelect.tsx | 99 +++++ src/components/AccountSelect/index.tsx | 2 + src/components/index.tsx | 2 + src/config/relay-chains.tsx | 14 +- .../Chains/ChainSelector/index.tsx | 64 --- src/legacy-components/Chains/index.tsx | 24 -- src/lib/form/index.tsx | 6 + src/lib/form/schemas/index.ts | 9 + src/lib/form/schemas/transfers.ts | 54 +++ src/lib/form/use-form.tsx | 4 +- .../CrossChainTransferForm.styles.tsx | 42 ++ .../CrossChainTransferForm.tsx | 293 +++++++++++++ .../components/ChainIcon/ChainIcon.tsx | 6 +- .../components/ChainIcon/icons/Bifrost.tsx | 38 ++ .../components/ChainIcon/icons/Heiko.tsx | 47 ++ .../components/ChainIcon/icons/Hydra.tsx | 32 ++ .../components/ChainIcon/icons/Karura.tsx | 51 +++ .../components/ChainIcon/icons/index.ts | 4 + .../ChainSelect/ChainSelect.style.tsx | 29 ++ .../components/ChainSelect/ChainSelect.tsx | 53 +++ .../components/ChainSelect/index.tsx | 2 + .../components/index.tsx | 6 +- .../Transfer/CrossChainTransferForm/index.tsx | 378 +--------------- src/pages/Transfer/Transfer.style.tsx | 9 + src/pages/Transfer/index.tsx | 112 +---- src/types/chains.d.ts | 10 + src/types/chains.types.ts | 3 - src/utils/hooks/api/xcm/use-xcm-bridge.ts | 182 +++++--- src/utils/hooks/api/xcm/xcm-endpoints.ts | 33 ++ yarn.lock | 402 ++++++++++++------ 42 files changed, 1478 insertions(+), 759 deletions(-) create mode 100644 src/assets/icons/ArrowRightCircle.tsx create mode 100644 src/components/AccountSelect/AccountLabel.tsx create mode 100644 src/components/AccountSelect/AccountList.tsx create mode 100644 src/components/AccountSelect/AccountListModal.tsx create mode 100644 src/components/AccountSelect/AccountSelect.style.tsx create mode 100644 src/components/AccountSelect/AccountSelect.tsx create mode 100644 src/components/AccountSelect/index.tsx delete mode 100644 src/legacy-components/Chains/ChainSelector/index.tsx delete mode 100644 src/legacy-components/Chains/index.tsx create mode 100644 src/lib/form/schemas/transfers.ts create mode 100644 src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.styles.tsx create mode 100644 src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx create mode 100644 src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Bifrost.tsx create mode 100644 src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Heiko.tsx create mode 100644 src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Hydra.tsx create mode 100644 src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Karura.tsx create mode 100644 src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/ChainSelect.style.tsx create mode 100644 src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/ChainSelect.tsx create mode 100644 src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/index.tsx create mode 100644 src/pages/Transfer/Transfer.style.tsx create mode 100644 src/types/chains.d.ts delete mode 100644 src/types/chains.types.ts create mode 100644 src/utils/hooks/api/xcm/xcm-endpoints.ts diff --git a/package.json b/package.json index 973c99347b..1362974961 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,12 @@ "@craco/craco": "^6.1.1", "@headlessui/react": "^1.1.1", "@heroicons/react": "^2.0.0", - "@interlay/bridge": "^0.2.4", + "@interlay/bridge": "^0.3.9", "@interlay/interbtc-api": "2.2.2", "@interlay/monetary-js": "0.7.2", "@polkadot/api": "9.14.2", "@polkadot/extension-dapp": "0.44.1", + "@polkadot/react-identicon": "^2.11.1", "@polkadot/ui-keyring": "^2.9.7", "@reach/tooltip": "^0.16.0", "@react-aria/accordion": "^3.0.0-alpha.14", diff --git a/src/assets/icons/ArrowRightCircle.tsx b/src/assets/icons/ArrowRightCircle.tsx new file mode 100644 index 0000000000..8cef0e7841 --- /dev/null +++ b/src/assets/icons/ArrowRightCircle.tsx @@ -0,0 +1,13 @@ +import { forwardRef } from 'react'; + +import { Icon, IconProps } from '@/component-library/Icon'; + +const ArrowRightCircle = forwardRef((props, ref) => ( + + + +)); + +ArrowRightCircle.displayName = 'ArrowRightCircle'; + +export { ArrowRightCircle }; diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index 6393c7b057..bf537097cc 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -1,4 +1,5 @@ export { ArrowRight } from './ArrowRight'; +export { ArrowRightCircle } from './ArrowRightCircle'; export { ArrowsUpDown } from './ArrowsUpDown'; export { ArrowTopRightOnSquare } from './ArrowTopRightOnSquare'; export { ChevronDown } from './ChevronDown'; diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index 439791e0dd..2959b1bd8d 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -602,6 +602,7 @@ }, "forms": { "please_enter_your_field": "Please enter your {{field}}", + "please_select_your_field": "Please select your {{field}}", "please_enter_the_amount_to": "Please enter the amount to {{field}}", "amount_must_be_at_least": "Amount to {{action}} must be at least {{amount}} {{token}}", "amount_must_be_at_most": "Amount to {{action}} must be at most {{amount}}", diff --git a/src/component-library/Label/Label.style.tsx b/src/component-library/Label/Label.style.tsx index 55c1a22cb1..d3209b0dee 100644 --- a/src/component-library/Label/Label.style.tsx +++ b/src/component-library/Label/Label.style.tsx @@ -8,6 +8,7 @@ const StyledLabel = styled.label` font-size: ${theme.text.xs}; color: ${theme.colors.textTertiary}; padding: ${theme.spacing.spacing1} 0; + align-self: flex-start; `; export { StyledLabel }; diff --git a/src/component-library/Select/SelectTrigger.tsx b/src/component-library/Select/SelectTrigger.tsx index 2229fd8f1c..8c2b52e27e 100644 --- a/src/component-library/Select/SelectTrigger.tsx +++ b/src/component-library/Select/SelectTrigger.tsx @@ -8,7 +8,7 @@ import { Sizes } from '../utils/prop-types'; import { StyledChevronDown, StyledTrigger, StyledTriggerValue } from './Select.style'; type Props = { - as: any; + as?: any; size?: Sizes; isOpen?: boolean; hasError?: boolean; diff --git a/src/component-library/index.tsx b/src/component-library/index.tsx index b91ec169da..d385b075d1 100644 --- a/src/component-library/index.tsx +++ b/src/component-library/index.tsx @@ -32,6 +32,8 @@ export type { ModalBodyProps, ModalDividerProps, ModalFooterProps, ModalHeaderPr export { Modal, ModalBody, ModalDivider, ModalFooter, ModalHeader } from './Modal'; export type { NumberInputProps } from './NumberInput'; export { NumberInput } from './NumberInput'; +export type { SelectProps } from './Select'; +export { Item, Select } from './Select'; export type { StackProps } from './Stack'; export { Stack } from './Stack'; export type { SwitchProps } from './Switch'; diff --git a/src/component-library/theme/theme.ts b/src/component-library/theme/theme.ts index 86e1295e36..c7a21c8791 100644 --- a/src/component-library/theme/theme.ts +++ b/src/component-library/theme/theme.ts @@ -510,15 +510,19 @@ const theme = { size: { small: { padding: 'var(--spacing-1)', - text: 'var(--text-s)' + text: 'var(--text-s)', + // TODO: to be determined + maxHeight: 'calc(var(--spacing-6) - 1px)' }, medium: { padding: 'var(--spacing-2)', - text: 'var(--text-base)' + text: 'var(--text-base)', + maxHeight: 'calc(var(--spacing-10) - 1px)' }, large: { padding: 'var(--spacing-5) var(--spacing-2)', - text: 'var(--text-lg)' + text: 'var(--text-lg)', + maxHeight: 'calc(var(--spacing-16) - 1px)' } } } diff --git a/src/components/AccountSelect/AccountLabel.tsx b/src/components/AccountSelect/AccountLabel.tsx new file mode 100644 index 0000000000..965d1128c2 --- /dev/null +++ b/src/components/AccountSelect/AccountLabel.tsx @@ -0,0 +1,34 @@ +import Identicon from '@polkadot/react-identicon'; + +import { FlexProps } from '@/component-library/Flex'; + +import { StyledAccountLabelAddress, StyledAccountLabelName, StyledAccountLabelWrapper } from './AccountSelect.style'; + +type Props = { + isSelected?: boolean; + address: string; + name?: string; +}; + +type InheritAttrs = Omit; + +type AccountLabelProps = Props & InheritAttrs; + +const AccountLabel = ({ isSelected, address, name, ...props }: AccountLabelProps): JSX.Element => ( + + + + {name && ( + + {name} + + )} + + {address} + + + +); + +export { AccountLabel }; +export type { AccountLabelProps }; diff --git a/src/components/AccountSelect/AccountList.tsx b/src/components/AccountSelect/AccountList.tsx new file mode 100644 index 0000000000..ca12d8b20e --- /dev/null +++ b/src/components/AccountSelect/AccountList.tsx @@ -0,0 +1,58 @@ +import { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; + +import { ListItem, ListProps } from '@/component-library/List'; + +import { AccountLabel } from './AccountLabel'; +import { StyledList } from './AccountSelect.style'; + +type Props = { + items: InjectedAccountWithMeta[]; + selectedAccount?: string; + onSelectionChange?: (account: string) => void; +}; + +type InheritAttrs = Omit; + +type AccountListProps = Props & InheritAttrs; + +const AccountList = ({ items, selectedAccount, onSelectionChange, ...props }: AccountListProps): JSX.Element => { + const handleSelectionChange: ListProps['onSelectionChange'] = (key) => { + const [selectedKey] = [...key]; + + if (!selectedKey) return; + + onSelectionChange?.(selectedKey as string); + }; + + return ( + + {items.map((item) => { + const accountText = item.address; + + const isSelected = selectedAccount === accountText; + + return ( + + + + ); + })} + + ); +}; + +export { AccountList }; +export type { AccountListProps }; diff --git a/src/components/AccountSelect/AccountListModal.tsx b/src/components/AccountSelect/AccountListModal.tsx new file mode 100644 index 0000000000..07b211e2a9 --- /dev/null +++ b/src/components/AccountSelect/AccountListModal.tsx @@ -0,0 +1,34 @@ +import { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; + +import { Modal, ModalBody, ModalHeader, ModalProps } from '@/component-library/Modal'; + +import { AccountList } from './AccountList'; + +type Props = { + accounts: InjectedAccountWithMeta[]; + onSelectionChange?: (account: string) => void; + selectedAccount?: string; +}; + +type InheritAttrs = Omit; + +type AccountListModalProps = Props & InheritAttrs; + +const AccountListModal = ({ + selectedAccount, + accounts, + onSelectionChange, + ...props +}: AccountListModalProps): JSX.Element => ( + + + Select Account + + + + + +); + +export { AccountListModal }; +export type { AccountListModalProps }; diff --git a/src/components/AccountSelect/AccountSelect.style.tsx b/src/components/AccountSelect/AccountSelect.style.tsx new file mode 100644 index 0000000000..850c26e0fc --- /dev/null +++ b/src/components/AccountSelect/AccountSelect.style.tsx @@ -0,0 +1,68 @@ +import styled from 'styled-components'; + +import { ChevronDown } from '@/assets/icons'; +import { Flex } from '@/component-library/Flex'; +import { List } from '@/component-library/List'; +import { Span } from '@/component-library/Text'; +import { theme } from '@/component-library/theme'; + +type StyledClickableProps = { + $isClickable: boolean; +}; + +type StyledListItemSelectedLabelProps = { + $isSelected: boolean; +}; + +const StyledAccount = styled.span` + font-size: ${theme.text.s}; + color: ${theme.colors.textPrimary}; + overflow: hidden; + text-overflow: ellipsis; +`; + +const StyledAccountSelect = styled(Flex)` + background-color: ${theme.tokenInput.endAdornment.bg}; + border-radius: ${theme.rounded.md}; + font-size: ${theme.text.xl2}; + padding: ${theme.spacing.spacing3}; + cursor: ${({ $isClickable }) => $isClickable && 'pointer'}; + height: 3rem; + width: auto; + overflow: hidden; +`; + +const StyledChevronDown = styled(ChevronDown)` + margin-left: ${theme.spacing.spacing1}; +`; + +const StyledAccountLabelAddress = styled(Span)` + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +`; + +const StyledAccountLabelName = styled(StyledAccountLabelAddress)` + color: ${({ $isSelected }) => + $isSelected ? theme.tokenInput.list.item.selected.text : theme.tokenInput.list.item.default.text}; +`; + +const StyledList = styled(List)` + overflow: auto; + padding: 0 ${theme.modal.body.paddingX} ${theme.modal.body.paddingY} ${theme.modal.body.paddingX}; +`; + +const StyledAccountLabelWrapper = styled(Flex)` + flex-grow: 1; + overflow: hidden; +`; + +export { + StyledAccount, + StyledAccountLabelAddress, + StyledAccountLabelName, + StyledAccountLabelWrapper, + StyledAccountSelect, + StyledChevronDown, + StyledList +}; diff --git a/src/components/AccountSelect/AccountSelect.tsx b/src/components/AccountSelect/AccountSelect.tsx new file mode 100644 index 0000000000..b9712491b5 --- /dev/null +++ b/src/components/AccountSelect/AccountSelect.tsx @@ -0,0 +1,99 @@ +import { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; +import { useLabel } from '@react-aria/label'; +import { chain, mergeProps } from '@react-aria/utils'; +import { VisuallyHidden } from '@react-aria/visually-hidden'; +import { forwardRef, InputHTMLAttributes, ReactNode, useEffect, useState } from 'react'; + +import { Flex, Label } from '@/component-library'; +import { SelectTrigger } from '@/component-library/Select'; +import { useDOMRef } from '@/component-library/utils/dom'; +import { triggerChangeEvent } from '@/component-library/utils/input'; + +import { AccountLabel } from './AccountLabel'; +import { AccountListModal } from './AccountListModal'; + +const getAccount = (accountValue?: string, accounts?: InjectedAccountWithMeta[]) => + accounts?.find((account) => account.address === accountValue); + +type Props = { + value: string; + defaultValue?: string; + icons?: string[]; + isDisabled?: boolean; + label?: ReactNode; + accounts?: InjectedAccountWithMeta[]; +}; + +type NativeAttrs = Omit & { ref?: any }, keyof Props>; + +type AccountSelectProps = Props & NativeAttrs; + +const AccountSelect = forwardRef( + ({ value: valueProp, defaultValue = '', accounts, disabled, label, className, ...props }, ref): JSX.Element => { + const inputRef = useDOMRef(ref); + + const [isOpen, setOpen] = useState(false); + const [value, setValue] = useState(defaultValue); + + const { fieldProps, labelProps } = useLabel({ ...props, label }); + + useEffect(() => { + if (valueProp === undefined) return; + + setValue(valueProp); + }, [valueProp]); + + const handleAccount = (account: string) => { + triggerChangeEvent(inputRef, account); + setValue(account); + }; + + const handleClose = () => setOpen(false); + + const isDisabled = !accounts?.length || disabled; + + const selectedAccount = getAccount(value, accounts); + + return ( + <> + + {label && } + setOpen(true)} + disabled={isDisabled} + {...mergeProps(fieldProps, { + // MEMO: when the button is blurred, a focus and blur is executed on the input + // so that validation gets triggered. + onBlur: () => { + if (!isOpen) { + inputRef.current?.focus(); + inputRef.current?.blur(); + } + } + })} + > + {selectedAccount && } + + + + + + {accounts && ( + + )} + + ); + } +); + +AccountSelect.displayName = 'AccountSelect'; + +export { AccountSelect }; +export type { AccountSelectProps }; diff --git a/src/components/AccountSelect/index.tsx b/src/components/AccountSelect/index.tsx new file mode 100644 index 0000000000..83aa80ccbd --- /dev/null +++ b/src/components/AccountSelect/index.tsx @@ -0,0 +1,2 @@ +export type { AccountSelectProps } from './AccountSelect'; +export { AccountSelect } from './AccountSelect'; diff --git a/src/components/index.tsx b/src/components/index.tsx index 51545514ef..5149bf3220 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -1,3 +1,5 @@ +export type { AccountSelectProps } from './AccountSelect'; +export { AccountSelect } from './AccountSelect'; export type { AuthCTAProps } from './AuthCTA'; export { AuthCTA } from './AuthCTA'; export type { AssetCellProps, BalanceCellProps, CellProps, TableProps } from './DataGrid'; diff --git a/src/config/relay-chains.tsx b/src/config/relay-chains.tsx index d358c50be6..de9302d73f 100644 --- a/src/config/relay-chains.tsx +++ b/src/config/relay-chains.tsx @@ -1,4 +1,9 @@ +import { AcalaAdapter, KaruraAdapter } from '@interlay/bridge/build/adapters/acala'; +import { AstarAdapter } from '@interlay/bridge/build/adapters/astar'; +import { BifrostAdapter } from '@interlay/bridge/build/adapters/bifrost'; +import { HydraAdapter } from '@interlay/bridge/build/adapters/hydradx'; import { InterlayAdapter, KintsugiAdapter } from '@interlay/bridge/build/adapters/interlay'; +import { HeikoAdapter, ParallelAdapter } from '@interlay/bridge/build/adapters/parallel'; import { KusamaAdapter, PolkadotAdapter } from '@interlay/bridge/build/adapters/polkadot'; import { StatemineAdapter, StatemintAdapter } from '@interlay/bridge/build/adapters/statemint'; import { BaseCrossChainAdapter } from '@interlay/bridge/build/base-chain-adapter'; @@ -156,6 +161,10 @@ switch (process.env.REACT_APP_RELAY_CHAIN_NAME) { TRANSACTION_FEE_AMOUNT = newMonetaryAmount(0.2, GOVERNANCE_TOKEN, true); XCM_ADAPTERS = { interlay: new InterlayAdapter(), + acala: new AcalaAdapter(), + astar: new AstarAdapter(), + hydra: new HydraAdapter(), + parallel: new ParallelAdapter(), polkadot: new PolkadotAdapter(), statemint: new StatemintAdapter() }; @@ -199,7 +208,10 @@ switch (process.env.REACT_APP_RELAY_CHAIN_NAME) { XCM_ADAPTERS = { kintsugi: new KintsugiAdapter(), kusama: new KusamaAdapter(), - statemine: new StatemineAdapter() + karura: new KaruraAdapter(), + statemine: new StatemineAdapter(), + bifrost: new BifrostAdapter(), + heiko: new HeikoAdapter() }; SS58_PREFIX = 2; break; diff --git a/src/legacy-components/Chains/ChainSelector/index.tsx b/src/legacy-components/Chains/ChainSelector/index.tsx deleted file mode 100644 index aa3c8d0e7c..0000000000 --- a/src/legacy-components/Chains/ChainSelector/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import clsx from 'clsx'; - -import Select, { - SELECT_VARIANTS, - SelectBody, - SelectButton, - SelectCheck, - SelectLabel, - SelectOption, - SelectOptions, - SelectText -} from '@/legacy-components/Select'; -import { XCMChains } from '@/types/chains.types'; - -interface ChainOption { - type: XCMChains; - name: string; - icon: JSX.Element; -} - -interface Props { - chainOptions: Array; - selectedChain: ChainOption | undefined; - label: string; - onChange?: (chain: ChainOption) => void; -} - -const ChainSelector = ({ chainOptions, selectedChain, label, onChange }: Props): JSX.Element => ( - -); - -export type { ChainOption }; -export default ChainSelector; diff --git a/src/legacy-components/Chains/index.tsx b/src/legacy-components/Chains/index.tsx deleted file mode 100644 index 16873ecdbf..0000000000 --- a/src/legacy-components/Chains/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import ChainSelector, { ChainOption } from './ChainSelector'; - -interface Props { - label: string; - chainOptions: Array | undefined; - onChange?: (chain: ChainOption) => void; - selectedChain: ChainOption | undefined; -} - -const Chains = ({ onChange, chainOptions, label, selectedChain }: Props): JSX.Element | null => { - if (!selectedChain || !chainOptions) { - return null; - } - - return ( -
- -
- ); -}; - -export type { ChainOption }; - -export default Chains; diff --git a/src/lib/form/index.tsx b/src/lib/form/index.tsx index 60fbf3b63b..af76026d2e 100644 --- a/src/lib/form/index.tsx +++ b/src/lib/form/index.tsx @@ -1,3 +1,9 @@ +export type { + CreateVaultFormData, + CrossChainTransferFormData, + DepositLiquidityPoolFormData, + LoanFormData +} from './schemas'; export * from './schemas'; export type { FormErrors } from './use-form'; export { useForm } from './use-form'; diff --git a/src/lib/form/schemas/index.ts b/src/lib/form/schemas/index.ts index 1d584fcd5d..a792ef0fbe 100644 --- a/src/lib/form/schemas/index.ts +++ b/src/lib/form/schemas/index.ts @@ -15,5 +15,14 @@ export { SwapErrorMessage, swapSchema } from './swap'; +export type { CrossChainTransferFormData, CrossChainTransferValidationParams } from './transfers'; +export { + CROSS_CHAIN_TRANSFER_AMOUNT_FIELD, + CROSS_CHAIN_TRANSFER_FROM_FIELD, + CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD, + CROSS_CHAIN_TRANSFER_TO_FIELD, + CROSS_CHAIN_TRANSFER_TOKEN_FIELD, + crossChainTransferSchema +} from './transfers'; export type { CreateVaultFormData } from './vaults'; export { CREATE_VAULT_DEPOSIT_FIELD, createVaultSchema } from './vaults'; diff --git a/src/lib/form/schemas/transfers.ts b/src/lib/form/schemas/transfers.ts new file mode 100644 index 0000000000..a6fe5c5f47 --- /dev/null +++ b/src/lib/form/schemas/transfers.ts @@ -0,0 +1,54 @@ +import { ChainName } from '@interlay/bridge'; +import { TFunction } from 'react-i18next'; + +import yup, { MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; + +const CROSS_CHAIN_TRANSFER_FROM_FIELD = 'transfer-from'; +const CROSS_CHAIN_TRANSFER_TO_FIELD = 'transfer-to'; +const CROSS_CHAIN_TRANSFER_AMOUNT_FIELD = 'transfer-amount'; +const CROSS_CHAIN_TRANSFER_TOKEN_FIELD = 'transfer-token'; +const CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD = 'transfer-account'; + +type CrossChainTransferFormData = { + [CROSS_CHAIN_TRANSFER_FROM_FIELD]?: ChainName; + [CROSS_CHAIN_TRANSFER_TO_FIELD]?: ChainName; + [CROSS_CHAIN_TRANSFER_AMOUNT_FIELD]?: string; + [CROSS_CHAIN_TRANSFER_TOKEN_FIELD]?: string; + [CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD]?: string; +}; + +type CrossChainTransferValidationParams = { + [CROSS_CHAIN_TRANSFER_AMOUNT_FIELD]: Partial & Partial; +}; + +// MEMO: until now, only CROSS_CHAIN_TRANSFER_AMOUNT_FIELD needs validation +const crossChainTransferSchema = (params: CrossChainTransferValidationParams, t: TFunction): yup.ObjectSchema => + yup.object().shape({ + [CROSS_CHAIN_TRANSFER_AMOUNT_FIELD]: yup + .string() + .requiredAmount('transfer') + .maxAmount(params[CROSS_CHAIN_TRANSFER_AMOUNT_FIELD] as MaxAmountValidationParams) + .minAmount(params[CROSS_CHAIN_TRANSFER_AMOUNT_FIELD] as MinAmountValidationParams, 'transfer'), + [CROSS_CHAIN_TRANSFER_FROM_FIELD]: yup + .string() + .required(t('forms.please_enter_your_field', { field: 'source chain' })), + [CROSS_CHAIN_TRANSFER_TO_FIELD]: yup + .string() + .required(t('forms.please_enter_your_field', { field: 'destination chain' })), + [CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD]: yup + .string() + .required(t('forms.please_enter_your_field', { field: 'destination' })), + [CROSS_CHAIN_TRANSFER_TOKEN_FIELD]: yup + .string() + .required(t('forms.please_select_your_field', { field: 'transfer token' })) + }); + +export { + CROSS_CHAIN_TRANSFER_AMOUNT_FIELD, + CROSS_CHAIN_TRANSFER_FROM_FIELD, + CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD, + CROSS_CHAIN_TRANSFER_TO_FIELD, + CROSS_CHAIN_TRANSFER_TOKEN_FIELD, + crossChainTransferSchema +}; +export type { CrossChainTransferFormData, CrossChainTransferValidationParams }; diff --git a/src/lib/form/use-form.tsx b/src/lib/form/use-form.tsx index 48c668f13e..7e62e97bd9 100644 --- a/src/lib/form/use-form.tsx +++ b/src/lib/form/use-form.tsx @@ -15,7 +15,7 @@ type GetFieldProps = ( withErrorMessage?: boolean ) => FieldInputProps & { errorMessage?: string | string[] }; -type UseFormAgrs = FormikConfig & { +type UseFormArgs = FormikConfig & { disableValidation?: boolean; getFieldProps?: GetFieldProps; }; @@ -25,7 +25,7 @@ const useForm = ({ validationSchema, disableValidation, ...args -}: UseFormAgrs) => { +}: UseFormArgs) => { const { t } = useTranslation(); const { validateForm, values, getFieldProps: getFormikFieldProps, ...formik } = useFormik({ ...args, diff --git a/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.styles.tsx b/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.styles.tsx new file mode 100644 index 0000000000..1c1ee33b0b --- /dev/null +++ b/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.styles.tsx @@ -0,0 +1,42 @@ +import styled from 'styled-components'; + +import { ArrowRightCircle } from '@/assets/icons'; +import { Dl, Flex, theme } from '@/component-library'; + +import { ChainSelect } from './components'; + +const StyledDl = styled(Dl)` + background-color: ${theme.card.bg.secondary}; + padding: ${theme.spacing.spacing4}; + font-size: ${theme.text.xs}; + border-radius: ${theme.rounded.rg}; +`; + +const StyledArrowRightCircle = styled(ArrowRightCircle)` + transform: rotate(90deg); + align-self: center; + + @media (min-width: 30em) { + transform: rotate(0deg); + margin-top: 1.75rem; + } +`; + +const ChainSelectSection = styled(Flex)` + flex-direction: column; + + @media (min-width: 30em) { + flex-direction: row; + gap: ${theme.spacing.spacing4}; + } +`; + +const StyledSourceChainSelect = styled(ChainSelect)` + margin-bottom: ${theme.spacing.spacing3}; + + @media (min-width: 30em) { + margin-bottom: 0; + } +`; + +export { ChainSelectSection, StyledArrowRightCircle, StyledDl, StyledSourceChainSelect }; diff --git a/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx b/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx new file mode 100644 index 0000000000..85b5cd0eb1 --- /dev/null +++ b/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx @@ -0,0 +1,293 @@ +import { FixedPointNumber } from '@acala-network/sdk-core'; +import { ChainName, CrossChainTransferParams } 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'; +import { AccountSelect, AuthCTA } from '@/components'; +import { + CROSS_CHAIN_TRANSFER_AMOUNT_FIELD, + CROSS_CHAIN_TRANSFER_FROM_FIELD, + CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD, + CROSS_CHAIN_TRANSFER_TO_FIELD, + CROSS_CHAIN_TRANSFER_TOKEN_FIELD, + CrossChainTransferFormData, + crossChainTransferSchema, + CrossChainTransferValidationParams, + isFormDisabled, + useForm +} 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 useAccountId from '@/utils/hooks/use-account-id'; + +import { ChainSelect } from './components'; +import { + ChainSelectSection, + StyledArrowRightCircle, + StyledDl, + StyledSourceChainSelect +} from './CrossChainTransferForm.styles'; + +const CrossChainTransferForm = (): JSX.Element => { + const [destinationChains, setDestinationChains] = useState([]); + const [transferableTokens, setTransferableTokens] = useState([]); + const [currentToken, setCurrentToken] = useState(); + + const prices = useGetPrices(); + const { t } = useTranslation(); + const { getCurrencyFromTicker } = useGetCurrencies(true); + + const accountId = useAccountId(); + const { accounts } = useSubstrateSecureState(); + + const { data, getDestinationChains, originatingChains, getAvailableTokens } = useXCMBridge(); + + const schema: CrossChainTransferValidationParams = { + [CROSS_CHAIN_TRANSFER_AMOUNT_FIELD]: { + minAmount: currentToken + ? newMonetaryAmount(currentToken.minTransferAmount, getCurrencyFromTicker(currentToken.value), true) + : undefined, + maxAmount: currentToken + ? newMonetaryAmount(currentToken.balance, getCurrencyFromTicker(currentToken.value), true) + : undefined + } + }; + + const mutateXcmTransfer = async (formData: CrossChainTransferFormData) => { + if (!data || !formData || !currentToken) return; + + const { signer } = await web3FromAddress(formData[CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD] as string); + 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 transferAmount = newMonetaryAmount( + form.values[CROSS_CHAIN_TRANSFER_AMOUNT_FIELD] || 0, + getCurrencyFromTicker(currentToken.value), + 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 handleSubmit = (formData: CrossChainTransferFormData) => { + xcmTransferMutation.mutate(formData); + }; + + const form = useForm({ + initialValues: { + [CROSS_CHAIN_TRANSFER_AMOUNT_FIELD]: '', + [CROSS_CHAIN_TRANSFER_TOKEN_FIELD]: '', + [CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD]: accountId?.toString() || '' + }, + onSubmit: handleSubmit, + 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); + + const destinationChains = getDestinationChains(chain); + + setDestinationChains(destinationChains); + form.setFieldValue(CROSS_CHAIN_TRANSFER_TO_FIELD, destinationChains[0].id); + }; + + const handleDestinationChainChange = async (chain: ChainName, name: string) => { + if (!accountId) return; + + form.setFieldValue(name, chain); + + setTokenData(chain); + }; + + const handleTickerChange = (ticker: string, name: string) => { + form.setFieldValue(name, ticker); + setCurrentToken(transferableTokens.find((token) => token.value === ticker)); + }; + + const handleDestinationAccountChange: ChangeEventHandler = (e) => { + form.setFieldValue(CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD, e.target.value); + }; + + const setTokenData = useCallback( + async (destination: ChainName) => { + if (!accountId || !form) return; + + const tokens = await getAvailableTokens( + form.values[CROSS_CHAIN_TRANSFER_FROM_FIELD] as ChainName, + destination, + accountId.toString(), + form.values[CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD] as string + ); + + if (!tokens) return; + + setTransferableTokens(tokens); + + // Update token data if selected token exists in new data + const token = tokens.find((token) => token.value === currentToken?.value) || tokens[0]; + + setCurrentToken(token); + form.setFieldValue(CROSS_CHAIN_TRANSFER_TOKEN_FIELD, token.value); + }, + [accountId, currentToken, form, getAvailableTokens] + ); + + const transferMonetaryAmount = currentToken + ? newSafeMonetaryAmount( + form.values[CROSS_CHAIN_TRANSFER_AMOUNT_FIELD] || 0, + getCurrencyFromTicker(currentToken.value), + true + ) + : 0; + + const valueUSD = transferMonetaryAmount + ? convertMonetaryAmountToValueInUSD( + transferMonetaryAmount, + getTokenPrice(prices, currentToken?.value as string)?.usd + ) + : 0; + + const isCTADisabled = isFormDisabled(form) || form.values[CROSS_CHAIN_TRANSFER_AMOUNT_FIELD] === ''; + const amountShouldValidate = form.values[CROSS_CHAIN_TRANSFER_AMOUNT_FIELD] !== ''; + + useEffect(() => { + if (!originatingChains?.length) return; + + // This prevents a render loop caused by setFieldValue + if (form.values[CROSS_CHAIN_TRANSFER_FROM_FIELD]) return; + + const destinationChains = getDestinationChains(originatingChains[0].id); + + form.setFieldValue(CROSS_CHAIN_TRANSFER_FROM_FIELD, originatingChains[0].id); + form.setFieldValue(CROSS_CHAIN_TRANSFER_TO_FIELD, destinationChains[0].id); + + setDestinationChains(destinationChains); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [originatingChains]); + + useEffect(() => { + if (!destinationChains?.length) return; + if (!accountId) return; + + setTokenData(destinationChains[0].id); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [accountId, destinationChains]); + + if (!originatingChains || !destinationChains || !transferableTokens.length) { + return ( + + + + ); + } + + return ( +
+ + + + handleOriginatingChainChange(chain as ChainName, CROSS_CHAIN_TRANSFER_FROM_FIELD) + } + {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_FROM_FIELD, false), { + onChange: handleOriginatingChainChange + })} + /> + + + handleDestinationChainChange(chain as ChainName, CROSS_CHAIN_TRANSFER_TO_FIELD) + } + {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_TO_FIELD, false), { + onChange: handleDestinationChainChange + })} + /> + +
+ + handleTickerChange(ticker as string, CROSS_CHAIN_TRANSFER_TOKEN_FIELD), + items: transferableTokens + })} + {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_AMOUNT_FIELD, amountShouldValidate))} + /> +
+ + + +
+ Origin chain transfer fee +
+
{currentToken?.originFee}
+
+ +
+ Destination chain transfer fee estimate +
+
{`${currentToken?.destFee.toString()} ${currentToken?.value}`}
+
+
+ + {isCTADisabled ? 'Enter transfer amount' : t('transfer')} + +
+
+ ); +}; + +export default CrossChainTransferForm; diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/ChainIcon.tsx b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/ChainIcon.tsx index 3a6611d6e2..339e83d336 100644 --- a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/ChainIcon.tsx +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/ChainIcon.tsx @@ -3,12 +3,16 @@ import { forwardRef, ForwardRefExoticComponent, RefAttributes } from 'react'; import { IconProps } from '@/component-library/Icon'; import { StyledFallbackIcon } from './ChainIcon.style'; -import { INTERLAY, KINTSUGI, KUSAMA, POLKADOT, STATEMINE, STATEMINT } from './icons'; +import { BIFROST, HEIKO, HYDRA, INTERLAY, KARURA, KINTSUGI, KUSAMA, POLKADOT, STATEMINE, STATEMINT } from './icons'; type ChainComponent = ForwardRefExoticComponent>; const chainsIcon: Record = { + BIFROST, + HEIKO, + HYDRA, INTERLAY, + KARURA, KINTSUGI, KUSAMA, POLKADOT, diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Bifrost.tsx b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Bifrost.tsx new file mode 100644 index 0000000000..6abf1e24bd --- /dev/null +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Bifrost.tsx @@ -0,0 +1,38 @@ +import { forwardRef } from 'react'; + +import { Icon, IconProps } from '@/component-library/Icon'; + +const BIFROST = forwardRef((props, ref) => ( + + BIFROST + + + + + + + + + + + + + + + + + + + + + + +)); + +BIFROST.displayName = 'KINTSUGI'; + +export { BIFROST }; diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Heiko.tsx b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Heiko.tsx new file mode 100644 index 0000000000..39afe777c3 --- /dev/null +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Heiko.tsx @@ -0,0 +1,47 @@ +import { forwardRef } from 'react'; + +import { Icon, IconProps } from '@/component-library/Icon'; + +const HEIKO = forwardRef((props, ref) => ( + + HEIKO + + + + + + + + + + + + + + + + + +)); + +HEIKO.displayName = 'HEIKO'; + +export { HEIKO }; diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Hydra.tsx b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Hydra.tsx new file mode 100644 index 0000000000..58f99aa0e3 --- /dev/null +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Hydra.tsx @@ -0,0 +1,32 @@ +import { forwardRef } from 'react'; + +import { Icon, IconProps } from '@/component-library/Icon'; + +const HYDRA = forwardRef((props, ref) => ( + + HYDRA + + + + + + + + + + + + + + + +)); + +HYDRA.displayName = 'INTERLAY'; + +export { HYDRA }; diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Karura.tsx b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Karura.tsx new file mode 100644 index 0000000000..2306754c2f --- /dev/null +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Karura.tsx @@ -0,0 +1,51 @@ +import { forwardRef } from 'react'; + +import { Icon, IconProps } from '@/component-library/Icon'; + +const KARURA = forwardRef((props, ref) => ( + + KARURA + + + + + + + + + + + + + + + + + + + +)); + +KARURA.displayName = 'KINTSUGI'; + +export { KARURA }; diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/index.ts b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/index.ts index df6ed50da9..e5d13a7014 100644 --- a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/index.ts +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/index.ts @@ -1,4 +1,8 @@ +export { BIFROST } from './Bifrost'; +export { HEIKO } from './Heiko'; +export { HYDRA } from './Hydra'; export { INTERLAY } from './Interlay'; +export { KARURA } from './Karura'; export { KINTSUGI } from './Kintsugi'; export { KUSAMA } from './Kusama'; export { POLKADOT } from './Polkadot'; diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/ChainSelect.style.tsx b/src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/ChainSelect.style.tsx new file mode 100644 index 0000000000..fe740a65eb --- /dev/null +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/ChainSelect.style.tsx @@ -0,0 +1,29 @@ +import styled from 'styled-components'; + +import { Flex } from '@/component-library/Flex'; +import { Span } from '@/component-library/Text'; +import { theme } from '@/component-library/theme'; + +type StyledListItemSelectedLabelProps = { + $isSelected: boolean; +}; + +const StyledChain = styled.span` + font-size: ${theme.text.s}; + color: ${theme.colors.textPrimary}; + overflow: hidden; + text-overflow: ellipsis; +`; + +const StyledListItemLabel = styled(Span)` + color: ${({ $isSelected }) => + $isSelected ? theme.tokenInput.list.item.selected.text : theme.tokenInput.list.item.default.text}; + text-overflow: ellipsis; + overflow: hidden; +`; + +const StyledListChainWrapper = styled(Flex)` + overflow: hidden; +`; + +export { StyledChain, StyledListChainWrapper, StyledListItemLabel }; diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/ChainSelect.tsx b/src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/ChainSelect.tsx new file mode 100644 index 0000000000..1506d894e6 --- /dev/null +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/ChainSelect.tsx @@ -0,0 +1,53 @@ +import { Flex } from '@/component-library'; +import { Item, Select, SelectProps } from '@/component-library'; +import { useSelectModalContext } from '@/component-library/Select/SelectModalContext'; +import { ChainData } from '@/types/chains'; + +import { ChainIcon } from '../ChainIcon'; +import { StyledChain, StyledListChainWrapper, StyledListItemLabel } from './ChainSelect.style'; + +type ChainSelectProps = Omit, 'children' | 'type'>; + +const ListItem = ({ data }: { data: ChainData }) => { + const isSelected = useSelectModalContext().selectedItem?.key === data.id; + + return ( + + + + {data.display} + + + ); +}; + +const Value = ({ data }: { data: ChainData }) => ( + + + {data.display} + +); + +const ChainSelect = ({ ...props }: ChainSelectProps): JSX.Element => { + return ( + + + {...props} + type='modal' + renderValue={(item) => } + modalTitle='Select Token' + > + {(data: ChainData) => ( + + + + )} + + + ); +}; + +ChainSelect.displayName = 'ChainSelect'; + +export { ChainSelect }; +export type { ChainSelectProps }; diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/index.tsx b/src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/index.tsx new file mode 100644 index 0000000000..2e2851d120 --- /dev/null +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/index.tsx @@ -0,0 +1,2 @@ +export type { ChainSelectProps } from './ChainSelect'; +export { ChainSelect } from './ChainSelect'; diff --git a/src/pages/Transfer/CrossChainTransferForm/components/index.tsx b/src/pages/Transfer/CrossChainTransferForm/components/index.tsx index 98ff9e319b..6cb7e0e8e5 100644 --- a/src/pages/Transfer/CrossChainTransferForm/components/index.tsx +++ b/src/pages/Transfer/CrossChainTransferForm/components/index.tsx @@ -1,4 +1,4 @@ -import { ChainIcon, ChainIconProps } from './ChainIcon'; +import { ChainSelect, ChainSelectProps } from './ChainSelect'; -export { ChainIcon }; -export type { ChainIconProps }; +export { ChainSelect }; +export type { ChainSelectProps }; diff --git a/src/pages/Transfer/CrossChainTransferForm/index.tsx b/src/pages/Transfer/CrossChainTransferForm/index.tsx index 0996fe956d..b2417c4fb7 100644 --- a/src/pages/Transfer/CrossChainTransferForm/index.tsx +++ b/src/pages/Transfer/CrossChainTransferForm/index.tsx @@ -1,377 +1,3 @@ -import { FixedPointNumber } from '@acala-network/sdk-core'; -import { BasicToken, CrossChainTransferParams } from '@interlay/bridge'; -import { CurrencyExt, DefaultTransactionAPI, newMonetaryAmount } from '@interlay/interbtc-api'; -import { MonetaryAmount } from '@interlay/monetary-js'; -import { ApiPromise } from '@polkadot/api'; -import { web3FromAddress } from '@polkadot/extension-dapp'; -import Big from 'big.js'; -import * as React from 'react'; -import { useEffect } from 'react'; -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 { firstValueFrom } from 'rxjs'; +import CrossChainTransferForm from './CrossChainTransferForm'; -import { ParachainStatus, StoreType } from '@/common/types/util.types'; -import { displayMonetaryAmountInUSDFormat } from '@/common/utils/utils'; -import { AuthCTA } from '@/components'; -import Accounts from '@/legacy-components/Accounts'; -import AvailableBalanceUI from '@/legacy-components/AvailableBalanceUI'; -import Chains, { ChainOption } from '@/legacy-components/Chains'; -import ErrorFallback from '@/legacy-components/ErrorFallback'; -import ErrorModal from '@/legacy-components/ErrorModal'; -import FormTitle from '@/legacy-components/FormTitle'; -import PrimaryColorEllipsisLoader from '@/legacy-components/PrimaryColorEllipsisLoader'; -import TokenField from '@/legacy-components/TokenField'; -import { KeyringPair, useSubstrateSecureState } from '@/lib/substrate'; -import STATUSES from '@/utils/constants/statuses'; -import { getExtrinsicStatus } from '@/utils/helpers/extrinsic'; -import { getTokenPrice } from '@/utils/helpers/prices'; -import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; -import { useXCMBridge } from '@/utils/hooks/api/xcm/use-xcm-bridge'; - -import { ChainIcon } from './components'; - -const TRANSFER_AMOUNT = 'transfer-amount'; - -type CrossChainTransferFormData = { - [TRANSFER_AMOUNT]: string; -}; - -const CrossChainTransferForm = (): JSX.Element => { - const [fromChains, setFromChains] = React.useState | undefined>(undefined); - const [fromChain, setFromChain] = React.useState(undefined); - const [toChains, setToChains] = React.useState | undefined>(undefined); - const [toChain, setToChain] = React.useState(undefined); - const [transferableBalance, setTransferableBalance] = React.useState(undefined); - const [destination, setDestination] = React.useState(undefined); - const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE); - const [submitError, setSubmitError] = React.useState(null); - const [approxUsdValue, setApproxUsdValue] = React.useState('0'); - const [currency, setCurrency] = React.useState(undefined); - - // TODO: this will need to be refactored when we support multiple currencies - // per channel, but so will the UI so better to handle this then. - const { t } = useTranslation(); - const prices = useGetPrices(); - - const { XCMBridge, XCMProvider } = useXCMBridge(); - - const { - register, - handleSubmit, - formState: { errors }, - reset, - setValue, - trigger - } = useForm({ - mode: 'onChange' - }); - - const { selectedAccount } = useSubstrateSecureState(); - const { parachainStatus } = useSelector((state: StoreType) => state.general); - - useEffect(() => { - if (!XCMBridge) return; - if (!fromChain) return; - if (!toChain) return; - // TODO: This handles a race condition. Will need to be fixed properly - // when supporting USDT - if (fromChain.name === toChain.name) return; - - const tokens = XCMBridge.router.getAvailableTokens({ from: fromChain.type, to: toChain.type }); - - const supportedCurrency = XCMBridge.findAdapter(fromChain.type).getToken(tokens[0], fromChain.type); - - setCurrency(supportedCurrency); - }, [fromChain, toChain, XCMBridge]); - - useEffect(() => { - if (!XCMBridge) return; - if (!fromChain) return; - if (!toChain) return; - if (!selectedAccount) return; - if (!currency) return; - if (!destination) return; - // TODO: This handles a race condition. Will need to be fixed properly - // when supporting USDT - if (toChain.type === fromChain.type) return; - - const getMaxTransferrable = async () => { - // TODO: Resolve type issue caused by version mismatch - // and remove casting to `any` - const inputConfigs: any = await firstValueFrom( - XCMBridge.findAdapter(fromChain.type).subscribeInputConfigs({ - to: toChain?.type, - token: currency.symbol, - address: destination.address, - signer: selectedAccount.address - }) as any - ); - - const maxInputToBig = Big(inputConfigs.maxInput.toString()); - - // Never show less than zero - const transferableBalance = inputConfigs.maxInput < inputConfigs.minInput ? 0 : maxInputToBig; - - setTransferableBalance(newMonetaryAmount(transferableBalance, (currency as unknown) as CurrencyExt, true)); - }; - - getMaxTransferrable(); - }, [currency, fromChain, toChain, selectedAccount, destination, XCMBridge]); - - useEffect(() => { - if (!XCMBridge) return; - if (!XCMProvider) return; - - const availableFromChains: Array = XCMBridge.adapters.map((adapter: any) => { - return { - type: adapter.chain.id, - name: adapter.chain.display, - icon: - }; - }); - - setFromChains(availableFromChains); - setFromChain(availableFromChains[0]); - }, [XCMBridge, XCMProvider]); - - useEffect(() => { - if (!XCMBridge) return; - if (!fromChain) return; - - const destinationChains = XCMBridge.router.getDestinationChains({ from: fromChain.type }); - - const availableToChains = destinationChains.map((chain: any) => { - return { - type: chain.id, - name: chain.display, - icon: - }; - }); - - setToChains(availableToChains); - setToChain(availableToChains[0]); - }, [fromChain, XCMBridge]); - - const onSubmit = async (data: CrossChainTransferFormData) => { - if (!selectedAccount) return; - if (!destination) return; - - try { - setSubmitStatus(STATUSES.PENDING); - - if (!XCMBridge || !fromChain || !toChain) return; - - const sendTransaction = async () => { - const { signer } = await web3FromAddress(selectedAccount.address.toString()); - - const adapter = XCMBridge.findAdapter(fromChain.type); - - const apiPromise = (XCMProvider.getApiPromise(fromChain.type) as unknown) as ApiPromise; - - apiPromise.setSigner(signer); - - // TODO: Version mismatch with ApiPromise type. This should be inferred. - adapter.setApi(apiPromise as any); - - const transferAmount = new MonetaryAmount((currency as unknown) as CurrencyExt, data[TRANSFER_AMOUNT]); - const transferAmountString = transferAmount.toString(true); - const transferAmountDecimals = transferAmount.currency.decimals; - - // TODO: Transaction is in promise form - const tx: any = adapter.createTx({ - amount: FixedPointNumber.fromInner(transferAmountString, transferAmountDecimals), - to: toChain.type, - token: currency?.symbol, - address: destination.address - } as CrossChainTransferParams); - - const inBlockStatus = getExtrinsicStatus('InBlock'); - - await DefaultTransactionAPI.sendLogged(apiPromise, selectedAccount.address, tx, undefined, inBlockStatus); - }; - - await sendTransaction(); - - setSubmitStatus(STATUSES.RESOLVED); - } catch (error) { - setSubmitStatus(STATUSES.REJECTED); - setSubmitError(error); - } - }; - - const handleUpdateUsdAmount = (value: string) => { - if (!value) return; - - const tokenAmount = newMonetaryAmount(value, (currency as unknown) as CurrencyExt, true); - - const usd = currency - ? displayMonetaryAmountInUSDFormat(tokenAmount, getTokenPrice(prices, currency.symbol)?.usd) - : '0'; - - setApproxUsdValue(usd); - }; - - const validateTransferAmount = async (value: string) => { - if (!toChain) return; - if (!fromChain) return; - if (!destination) return; - if (!selectedAccount) return; - if (!currency) return; - - const balanceMonetaryAmount = newMonetaryAmount(transferableBalance, (currency as unknown) as CurrencyExt, true); - const transferAmount = newMonetaryAmount(value, (currency as unknown) as CurrencyExt, true); - - // TODO: Resolve type issue caused by version mismatch - // and remove casting to `any` - const inputConfigs: any = await firstValueFrom( - XCMBridge.findAdapter(fromChain.type).subscribeInputConfigs({ - to: toChain?.type, - token: currency?.symbol, - address: destination.address, - signer: selectedAccount.address - }) as any - ); - - const minInputToBig = Big(inputConfigs.minInput.toString()); - const maxInputToBig = Big(inputConfigs.maxInput.toString()); - - if (balanceMonetaryAmount.lt(transferAmount)) { - return t('xcm_transfer.validation.insufficient_funds'); - } else if (minInputToBig.gt(transferableBalance)) { - return t('xcm_transfer.validation.balance_lower_minimum'); - } else if (minInputToBig.gt(transferAmount.toBig())) { - return t('xcm_transfer.validation.transfer_more_than_minimum', { - amount: `${inputConfigs.minInput.toString()} ${currency.symbol}` - }); - } else if (maxInputToBig.lt(transferAmount.toBig())) { - return t('xcm_transfer.validation.transfer_less_than_maximum', { - amount: `${inputConfigs.maxInput.toString()} ${currency.symbol}` - }); - } else { - return undefined; - } - }; - - const handleSetFromChain = (chain: ChainOption) => { - // Return from function is user clicks on current chain option - if (chain === fromChain) return; - - // Note: this is a workaround but ok for now. Component will be refactored - // when we introduce support for multiple currencies per channel - setCurrency(undefined); - setToChain(undefined); - setValue(TRANSFER_AMOUNT, ''); - setFromChain(chain); - }; - - const handleSetToChain = (chain: ChainOption) => { - // Return from function is user clicks on current chain option - if (chain === toChain) return; - - // Note: this is a workaround but ok for now. Component will be refactored - // when we introduce support for multiple currencies per channel - setCurrency(undefined); - setValue(TRANSFER_AMOUNT, ''); - setToChain(chain); - }; - - const handleClickBalance = () => { - setValue(TRANSFER_AMOUNT, transferableBalance.toString()); - handleUpdateUsdAmount(transferableBalance); - trigger(TRANSFER_AMOUNT); - }; - - // 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]: '' - }); - }, [submitStatus, reset, t]); - - if (!XCMBridge || !toChain || !fromChain || !currency) { - return ; - } - - return ( - <> -
- {t('transfer_page.cross_chain_transfer_form.title')} -
- - handleUpdateUsdAmount(e.target.value), - required: { - value: true, - message: t('transfer_page.cross_chain_transfer_form.please_enter_amount') - }, - validate: (value) => validateTransferAmount(value) - })} - error={!!errors[TRANSFER_AMOUNT]} - helperText={errors[TRANSFER_AMOUNT]?.message} - label={currency.symbol} - approxUSD={`≈ ${approxUsdValue}`} - /> -
- - - - - {t('transfer')} - - - {submitStatus === STATUSES.REJECTED && submitError && ( - { - setSubmitStatus(STATUSES.IDLE); - setSubmitError(null); - }} - title='Error' - description={typeof submitError === 'string' ? submitError : submitError.message} - /> - )} - - ); -}; - -export default withErrorBoundary(CrossChainTransferForm, { - FallbackComponent: ErrorFallback, - onReset: () => { - window.location.reload(); - } -}); +export default CrossChainTransferForm; diff --git a/src/pages/Transfer/Transfer.style.tsx b/src/pages/Transfer/Transfer.style.tsx new file mode 100644 index 0000000000..4ec3066518 --- /dev/null +++ b/src/pages/Transfer/Transfer.style.tsx @@ -0,0 +1,9 @@ +import styled from 'styled-components'; + +import { theme } from '@/component-library'; + +const StyledWrapper = styled.div` + margin-top: ${theme.spacing.spacing6}; +`; + +export { StyledWrapper }; diff --git a/src/pages/Transfer/index.tsx b/src/pages/Transfer/index.tsx index ed2ea96777..2dbbc7ac0b 100644 --- a/src/pages/Transfer/index.tsx +++ b/src/pages/Transfer/index.tsx @@ -1,111 +1,31 @@ import clsx from 'clsx'; -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import Hr1 from '@/legacy-components/hrs/Hr1'; +import { Flex, Tabs, TabsItem } from '@/component-library'; import Panel from '@/legacy-components/Panel'; -import InterlayRouterLink from '@/legacy-components/UI/InterlayRouterLink'; -import InterlayTabGroup, { - InterlayTab, - InterlayTabList, - InterlayTabPanel, - InterlayTabPanels -} from '@/legacy-components/UI/InterlayTabGroup'; -import WarningBanner from '@/legacy-components/WarningBanner'; import MainContainer from '@/parts/MainContainer'; -import { QUERY_PARAMETERS } from '@/utils/constants/links'; -import { POLKADOT } from '@/utils/constants/relay-chain-names'; -import useQueryParams from '@/utils/hooks/use-query-params'; -import useUpdateQueryParameters, { QueryParameters } from '@/utils/hooks/use-update-query-parameters'; import CrossChainTransferForm from './CrossChainTransferForm'; +import { StyledWrapper } from './Transfer.style'; import TransferForm from './TransferForm'; -const TAB_IDS = Object.freeze({ - transfer: 'transfer', - crossChainTransfer: 'crossChainTransfer' -}); - -const TAB_ITEMS = [ - { - id: TAB_IDS.transfer, - label: 'transfer' - }, - { - id: TAB_IDS.crossChainTransfer, - label: 'cross chain transfer' - } -]; - const Transfer = (): JSX.Element | null => { - const queryParams = useQueryParams(); - const selectedTabId = queryParams.get(QUERY_PARAMETERS.TAB); - const updateQueryParameters = useUpdateQueryParameters(); - - const { t } = useTranslation(); - - const updateQueryParametersRef = React.useRef<(newQueryParameters: QueryParameters) => void>(); - - React.useLayoutEffect(() => { - updateQueryParametersRef.current = updateQueryParameters; - }); - - React.useEffect(() => { - if (!updateQueryParametersRef.current) return; - - const tabIdValues = Object.values(TAB_IDS); - switch (true) { - case selectedTabId === null: - case selectedTabId && !tabIdValues.includes(selectedTabId): - updateQueryParametersRef.current({ - [QUERY_PARAMETERS.TAB]: TAB_IDS.transfer - }); - } - }, [selectedTabId]); - - const selectedTabIndex = TAB_ITEMS.findIndex((tabItem) => tabItem.id === selectedTabId); - - const handleTabSelect = (index: number) => { - updateQueryParameters({ - [QUERY_PARAMETERS.TAB]: TAB_ITEMS[index].id - }); - }; - return ( - {process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT && ( - -

- In order to transfer Interlay tokens to Acala or Moonbeam, please use their respective dApps. Send tokens to{' '} - - Acala - {' '} - |{' '} - - Moonbeam - -

-
- )} - - - {TAB_ITEMS.map((tabItem) => ( - - {t(tabItem.label)} - - ))} - - - - - - - - - - - + + + + + + + + + + + + + +
); diff --git a/src/types/chains.d.ts b/src/types/chains.d.ts new file mode 100644 index 0000000000..4f3fdfb322 --- /dev/null +++ b/src/types/chains.d.ts @@ -0,0 +1,10 @@ +import { ChainName } from '@interlay/bridge'; + +type ChainData = { + display: string; + id: ChainName; +}; + +type Chains = ChainData[]; + +export type { ChainData, Chains }; diff --git a/src/types/chains.types.ts b/src/types/chains.types.ts deleted file mode 100644 index 74ee07fd8b..0000000000 --- a/src/types/chains.types.ts +++ /dev/null @@ -1,3 +0,0 @@ -type XCMChains = 'polkadot' | 'interlay'; - -export type { XCMChains }; diff --git a/src/utils/hooks/api/xcm/use-xcm-bridge.ts b/src/utils/hooks/api/xcm/use-xcm-bridge.ts index 4b086c55c8..32a6845d77 100644 --- a/src/utils/hooks/api/xcm/use-xcm-bridge.ts +++ b/src/utils/hooks/api/xcm/use-xcm-bridge.ts @@ -1,70 +1,150 @@ +import { FixedPointNumber } from '@acala-network/sdk-core'; import { ApiProvider, Bridge, ChainName } from '@interlay/bridge/build'; -import { useEffect, useState } from 'react'; +import { BaseCrossChainAdapter } from '@interlay/bridge/build/base-chain-adapter'; +import { atomicToBaseAmount, CurrencyExt, newMonetaryAmount } from '@interlay/interbtc-api'; +import Big from 'big.js'; +import { useCallback } from 'react'; +import { useErrorHandler } from 'react-error-boundary'; +import { useQuery, UseQueryResult } from 'react-query'; import { firstValueFrom } from 'rxjs'; +import { convertMonetaryAmountToValueInUSD, formatUSD } from '@/common/utils/utils'; import { XCM_ADAPTERS } from '@/config/relay-chains'; -import { BITCOIN_NETWORK } from '@/constants'; +import { Chains } from '@/types/chains'; +import { getTokenPrice } from '@/utils/helpers/prices'; +import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; -// MEMO: BitcoinNetwork type is not available on XCM bridge -const XCMNetwork = BITCOIN_NETWORK === 'mainnet' ? 'mainnet' : 'testnet'; +import { XCMEndpoints } from './xcm-endpoints'; const XCMBridge = new Bridge({ adapters: Object.values(XCM_ADAPTERS) }); -// TODO: This config needs to be pushed higher up the app. -// Not sure how this will look: something to decide when -// adding USDT support. -const getEndpoints = (chains: ChainName[]) => { - switch (true) { - case chains.includes('kusama'): - return { - kusama: ['wss://kusama-rpc.polkadot.io', 'wss://kusama.api.onfinality.io/public-ws'], - kintsugi: ['wss://api-kusama.interlay.io/parachain', 'wss://kintsugi.api.onfinality.io/public-ws'], - statemine: ['wss://statemine-rpc.polkadot.io', 'wss://statemine.api.onfinality.io/public-ws'] - }; - case chains.includes('polkadot'): - return { - polkadot: ['wss://rpc.polkadot.io', 'wss://polkadot.api.onfinality.io/public-ws'], - interlay: ['wss://api.interlay.io/parachain', 'wss://interlay.api.onfinality.io/public-ws'], - statemint: ['wss://statemint-rpc.polkadot.io', 'wss://statemint.api.onfinality.io/public-ws'] - }; - - default: - return undefined; - } +type XCMBridgeData = { + bridge: Bridge; + provider: ApiProvider; }; -// const useXCMBridge = (): { XCMProvider: ApiProvider; XCMBridge: Bridge } => { -const useXCMBridge = (): { XCMProvider: ApiProvider; XCMBridge: Bridge } => { - const [XCMProvider, setXCMProvider] = useState(); - - useEffect(() => { - const createBridge = async () => { - const XCMProvider = new ApiProvider(XCMNetwork); - const chains = Object.keys(XCM_ADAPTERS) as ChainName[]; - - // Check connection - // TODO: Get rid of any casting - mismatch between ApiRx types - await firstValueFrom(XCMProvider.connectFromChain(chains, getEndpoints(chains)) as any); - - // Set Apis - await Promise.all( - chains.map((chain: ChainName) => - // TODO: Get rid of any casting - mismatch between ApiRx types - XCMBridge.findAdapter(chain).setApi(XCMProvider.getApi(chain) as any) - ) - ); +type XCMTokenData = { + balance: string; + balanceUSD: string; + destFee: FixedPointNumber; + originFee: string; + minTransferAmount: Big; + value: string; +}; + +type UseXCMBridge = UseQueryResult & { + originatingChains: Chains | undefined; + getDestinationChains: (chain: ChainName) => Chains; + getAvailableTokens: ( + from: ChainName, + to: ChainName, + originAddress: string, + destinationAddress: string + ) => Promise; +}; + +const initXCMBridge = async () => { + const XCMProvider = new ApiProvider(); + const chains = Object.keys(XCM_ADAPTERS) as ChainName[]; + + await firstValueFrom(XCMProvider.connectFromChain(chains, XCMEndpoints)); + + // Set Apis + await Promise.all(chains.map((chain: ChainName) => XCMBridge.findAdapter(chain).setApi(XCMProvider.getApi(chain)))); + + return { provider: XCMProvider, bridge: XCMBridge }; +}; + +const useXCMBridge = (): UseXCMBridge => { + const queryKey = ['available-xcm-channels']; + + const queryResult = useQuery({ + queryKey, + queryFn: initXCMBridge, + refetchInterval: false + }); + + const { data, error } = queryResult; + const prices = useGetPrices(); - setXCMProvider(XCMProvider); + const originatingChains = data?.bridge.adapters.map((adapter: BaseCrossChainAdapter) => { + return { + display: adapter.chain.display, + id: adapter.chain.id as ChainName }; + }); + + const getDestinationChains = useCallback( + (chain: ChainName): Chains => { + return XCMBridge.router + .getDestinationChains({ from: chain }) + .filter((destinationChain) => + originatingChains?.some((originatingChain) => originatingChain.id === destinationChain.id) + ) as Chains; + }, + [originatingChains] + ); + + const getAvailableTokens = useCallback( + async (from, to, originAddress, destinationAddress) => { + if (!data) return; + + const tokens = XCMBridge.router.getAvailableTokens({ from, to }); + + const inputConfigs = await Promise.all( + tokens.map(async (token) => { + const inputConfig = await firstValueFrom( + data.bridge.findAdapter(from).subscribeInputConfigs({ + to, + token, + address: destinationAddress, + signer: originAddress + }) + ); + + // TODO: resolve type mismatch with BaseCrossChainAdapter and remove `any` + const originAdapter = data.bridge.findAdapter(from) as any; + + const maxInputToBig = Big(inputConfig.maxInput.toString()); + const minInputToBig = Big(inputConfig.minInput.toString()); + + // Never show less than zero + const transferableBalance = inputConfig.maxInput < inputConfig.minInput ? 0 : maxInputToBig; + const currency = XCMBridge.findAdapter(from).getToken(token, from); + + const nativeToken = originAdapter.getNativeToken(); + + const amount = newMonetaryAmount(transferableBalance, (currency as unknown) as CurrencyExt, true); + const balanceUSD = convertMonetaryAmountToValueInUSD(amount, getTokenPrice(prices, token)?.usd); + const originFee = atomicToBaseAmount(inputConfig.estimateFee, nativeToken as CurrencyExt); + + return { + balance: transferableBalance.toString(), + balanceUSD: formatUSD(balanceUSD || 0, { compact: true }), + destFee: inputConfig.destFee.balance, + originFee: `${originFee.toString()} ${nativeToken.symbol}`, + minTransferAmount: minInputToBig, + value: token + }; + }) + ); + + return inputConfigs; + }, + [data, prices] + ); - if (!XCMProvider) { - createBridge(); - } - }, [XCMProvider]); + useErrorHandler(error); - return { XCMProvider, XCMBridge }; + return { + ...queryResult, + originatingChains, + getDestinationChains, + getAvailableTokens + }; }; export { useXCMBridge }; +export type { UseXCMBridge, XCMTokenData }; diff --git a/src/utils/hooks/api/xcm/xcm-endpoints.ts b/src/utils/hooks/api/xcm/xcm-endpoints.ts new file mode 100644 index 0000000000..6b8407c0df --- /dev/null +++ b/src/utils/hooks/api/xcm/xcm-endpoints.ts @@ -0,0 +1,33 @@ +import { ChainName } from '@interlay/bridge'; + +type XCMEndpointsRecord = Record; + +const XCMEndpoints: XCMEndpointsRecord = { + acala: [ + 'wss://acala-rpc-0.aca-api.network', + 'wss://acala-rpc-1.aca-api.network', + 'wss://acala-rpc-3.aca-api.network/ws', + 'wss://acala-rpc.dwellir.com' + ], + astar: ['wss://rpc.astar.network', 'wss://astar-rpc.dwellir.com'], + bifrost: ['wss://bifrost-rpc.dwellir.com'], + heiko: ['wss://heiko-rpc.parallel.fi'], + hydra: ['wss://rpc.hydradx.cloud', 'wss://hydradx-rpc.dwellir.com'], + interlay: ['wss://api.interlay.io/parachain'], + karura: [ + 'wss://karura-rpc-0.aca-api.network', + 'wss://karura-rpc-1.aca-api.network', + 'wss://karura-rpc-2.aca-api.network/ws', + 'wss://karura-rpc-3.aca-api.network/ws', + 'wss://karura-rpc.dwellir.com' + ], + kintsugi: ['wss://api-kusama.interlay.io/parachain'], + kusama: ['wss://kusama-rpc.polkadot.io', 'wss://kusama-rpc.dwellir.com'], + parallel: ['wss://rpc.parallel.fi'], + polkadot: ['wss://rpc.polkadot.io', 'wss://polkadot-rpc.dwellir.com'], + statemine: ['wss://statemine-rpc.polkadot.io', 'wss://statemine-rpc.dwellir.com'], + statemint: ['wss://statemint-rpc.polkadot.io', 'wss://statemint-rpc.dwellir.com'] +}; + +export { XCMEndpoints }; +export type { XCMEndpointsRecord }; diff --git a/yarn.lock b/yarn.lock index 6e1717a6aa..c887f3b4fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,16 @@ # yarn lockfile v1 +"@acala-network/api-derive@4.1.8-13": + version "4.1.8-13" + resolved "https://registry.yarnpkg.com/@acala-network/api-derive/-/api-derive-4.1.8-13.tgz#0ac02da5494c9f6ea8d52235836ecb369dea443d" + integrity sha512-Bm7005fPvFMcohvlpbGJMpm0Vm/63PTkRcg0shZvcjuMak3YSR0NhceZRnMoHz+I0Ond5XGRjZVZA/eyRMbSsg== + dependencies: + "@acala-network/types" "4.1.8-13" + "@babel/runtime" "^7.10.2" + "@open-web3/orml-types" "^1.1.4" + "@polkadot/api-derive" "^8.5.1" + "@acala-network/api-derive@4.1.8-9": version "4.1.8-9" resolved "https://registry.yarnpkg.com/@acala-network/api-derive/-/api-derive-4.1.8-9.tgz#f4d3969665fe2e92d2fca73d2c403e4f26b519bd" @@ -12,7 +22,19 @@ "@open-web3/orml-types" "^1.1.4" "@polkadot/api-derive" "^8.5.1" -"@acala-network/api@4.1.8-9", "@acala-network/api@~4.1.8-9": +"@acala-network/api@4.1.8-13": + version "4.1.8-13" + resolved "https://registry.yarnpkg.com/@acala-network/api/-/api-4.1.8-13.tgz#8127edaba9802eaa6a20678e823f43f2affb6067" + integrity sha512-+m032NiYPAvbOHeaJrCKQuACe9hykNTpQpDKeKkg0RME9JnFKeR7TYLkWtInhbmql6b8LxAAdpy2gdQctrsCRA== + dependencies: + "@acala-network/api-derive" "4.1.8-13" + "@acala-network/types" "4.1.8-13" + "@babel/runtime" "^7.10.2" + "@open-web3/orml-api-derive" "^1.1.4" + "@polkadot/api" "^9.9.1" + "@polkadot/rpc-core" "^9.9.1" + +"@acala-network/api@~4.1.8-9": version "4.1.8-9" resolved "https://registry.yarnpkg.com/@acala-network/api/-/api-4.1.8-9.tgz#9213e09b7c43b3df95eaf47fe78c989ddfe4207e" integrity sha512-9kpYQYe5vBCKWlyABh+Q2sjONDdtNfdv0PL0Tek3bpt00a3VjNIZvQro5ZSwzdpGJs5YcsiWPRMBq3iMgJNtGQ== @@ -29,7 +51,7 @@ resolved "https://registry.yarnpkg.com/@acala-network/contracts/-/contracts-4.3.4.tgz#f37cf54894c72b762df539042a61f90b10b68600" integrity sha512-oBgXGUjRW+lRo9TWGtCB1+OpEOFfhxW//wReb7V/YdbEElVvYuKw3lmfly/eZ/mdBgqxA3eXxNW0AgXiyOn2NQ== -"@acala-network/eth-providers@^2.5.4": +"@acala-network/eth-providers@^2.5.9": version "2.6.5" resolved "https://registry.yarnpkg.com/@acala-network/eth-providers/-/eth-providers-2.6.5.tgz#9087abe44a0686de5188ea962961519ecff20e66" integrity sha512-Y0hi0LRN8pJ144dv9WcSi9nPn5Wez0h745EGa1/6NFtU7jsua0jg25WYJ53s17rXIMz8GUKdln9SAIeShQiEtw== @@ -79,10 +101,10 @@ "@ethersproject/wallet" "~5.7.0" "@polkadot/util-crypto" "^10.2.1" -"@acala-network/sdk-core@4.1.8-9": - version "4.1.8-9" - resolved "https://registry.yarnpkg.com/@acala-network/sdk-core/-/sdk-core-4.1.8-9.tgz#47de650483f74aa9320d9ff9a8cdcf0e48b4a192" - integrity sha512-hjJ4Qs20aacg9vUnt2xZne3nN+c73zS7sBklVwtzXLlW87QWKDHdvkRkGZyeeKujaGRnqODhYIPtGtPqd0t+ag== +"@acala-network/sdk-core@4.1.8-13": + version "4.1.8-13" + resolved "https://registry.yarnpkg.com/@acala-network/sdk-core/-/sdk-core-4.1.8-13.tgz#ff69ef993f5a36caa31744384c389765ede7cc96" + integrity sha512-4q9lksLJ/8lXA/f/t9GHQqv8ePIT2vId7rkaoqE/jASq6ngRFg2heV/6eScCKudr2aJN68YX3Jf0hwH6eazVLQ== dependencies: "@polkadot/api" "^9.9.1" "@polkadot/types" "^9.9.1" @@ -91,14 +113,14 @@ events "^3.2.0" lodash "^4.17.20" -"@acala-network/sdk@4.1.8-9": - version "4.1.8-9" - resolved "https://registry.yarnpkg.com/@acala-network/sdk/-/sdk-4.1.8-9.tgz#3501296ec663346e2118dcfcc72e31afcdf63fbb" - integrity sha512-/e624PRyzwUJUEW4g7y4kVjs4WsDU2S6KPvn2Nbojl0bz0wrm05ghjD3lW98m8CcLLLv4wa4hldegFzx79LYgw== +"@acala-network/sdk@4.1.8-13": + version "4.1.8-13" + resolved "https://registry.yarnpkg.com/@acala-network/sdk/-/sdk-4.1.8-13.tgz#f603a6c84c4654971495676345f4a24041ff1c02" + integrity sha512-3apYrmQ+WZWzEYd0sdLCpTYe8SagMMK2+0vj35ANVvD92FHUUkHTtJAEiCu81y0ujFuFbtx/VxA0uGVb/fBZ6A== dependencies: - "@acala-network/api" "4.1.8-9" - "@acala-network/eth-providers" "^2.5.4" - "@acala-network/type-definitions" "4.1.8-9" + "@acala-network/api" "4.1.8-13" + "@acala-network/eth-providers" "^2.5.9" + "@acala-network/type-definitions" "4.1.8-13" "@ethersproject/bignumber" "^5.7.0" "@polkadot/api" "^9.9.1" "@polkadot/types" "^9.9.1" @@ -114,6 +136,13 @@ lru-cache "^7.14.1" rxjs "^7.5.7" +"@acala-network/type-definitions@4.1.8-13": + version "4.1.8-13" + resolved "https://registry.yarnpkg.com/@acala-network/type-definitions/-/type-definitions-4.1.8-13.tgz#a295d3f3feb1d36cadbda634c180f53eb90cca61" + integrity sha512-AMXbqsJehhDcwEngSB173eQvuCAsXEm/7rNZMQ8KLG56a8FrNAgrEz+83foogLuTcehCPUPfC0R1Ef/+874rRw== + dependencies: + "@open-web3/orml-type-definitions" "^1.1.4" + "@acala-network/type-definitions@4.1.8-9": version "4.1.8-9" resolved "https://registry.yarnpkg.com/@acala-network/type-definitions/-/type-definitions-4.1.8-9.tgz#be238e2e269cd701b79b0af5f9ed4d9c168d94c0" @@ -121,13 +150,23 @@ dependencies: "@open-web3/orml-type-definitions" "^1.1.4" -"@acala-network/type-definitions@^4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@acala-network/type-definitions/-/type-definitions-4.1.5.tgz#c02624ba9bb637588ddd184a4ce35ab7d9de2bf6" - integrity sha512-XwXtKf5ESfzGk32N1sE3MlBtnamz2JZYtjB6KcKe9eOyv+3lowQvRn4Z347rNSEp+tpenZWnLwBXk4XWhdiSoQ== +"@acala-network/type-definitions@^4.1.8-1": + version "4.1.8-14" + resolved "https://registry.yarnpkg.com/@acala-network/type-definitions/-/type-definitions-4.1.8-14.tgz#f0d1dd5f0e50c5b16e19fc222d351b4ec4524928" + integrity sha512-3PDYFaT8s9PYgZZNNtOEco5Oyn/oQlnuYrBe6WQX1bQBhAbUQjMDhuaqoqRF61CFtxYTgw/6kiFRf/aUNhigGQ== dependencies: "@open-web3/orml-type-definitions" "^1.1.4" +"@acala-network/types@4.1.8-13": + version "4.1.8-13" + resolved "https://registry.yarnpkg.com/@acala-network/types/-/types-4.1.8-13.tgz#919fc5ad818f535caba0fc2ea0477085e570d93b" + integrity sha512-XBIupGrNyY1xSptC59GNE89C4wJ2pb/QwRiRkQUNzDSTfLbjUSCOpDqjSfZIxj21+/zhZtw+6+uS+HnoTpsQeg== + dependencies: + "@acala-network/type-definitions" "4.1.8-13" + "@babel/runtime" "^7.10.2" + "@open-web3/api-mobx" "^1.1.4" + "@open-web3/orml-types" "^1.1.4" + "@acala-network/types@4.1.8-9", "@acala-network/types@~4.1.8-9": version "4.1.8-9" resolved "https://registry.yarnpkg.com/@acala-network/types/-/types-4.1.8-9.tgz#afc11f555dc900149eff132857f456500dcfb892" @@ -1270,7 +1309,7 @@ core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.18.9", "@babel/runtime@^7.20.1", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.18.9", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== @@ -1321,10 +1360,10 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@bifrost-finance/type-definitions@1.7.1": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@bifrost-finance/type-definitions/-/type-definitions-1.7.1.tgz#d64e89eebf5d325ecca636261373945e14c4c508" - integrity sha512-9AJIFFtlTKUGNJ8ITkgDUUJD+Iodb2Cp6qbVl5mAKuaws9QrLpgKYTT09GoKltQTg5bbDc8+ygbcabntUeTZGw== +"@bifrost-finance/type-definitions@1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@bifrost-finance/type-definitions/-/type-definitions-1.7.2.tgz#13139a69e3e98d175a4751d7fd78dcfebac29943" + integrity sha512-JL19CHFL4DxO29LRrv9o7r7Au9TtY+8pwG4fMP8M6jq2/MkvWd7OQFn1lmEy58akntNrVReIkZPuP81MFKv9jg== dependencies: "@open-web3/orml-type-definitions" "^0.9.4-38" @@ -1552,10 +1591,10 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@docknetwork/node-types@0.13.0": - version "0.13.0" - resolved "https://registry.yarnpkg.com/@docknetwork/node-types/-/node-types-0.13.0.tgz#8b643f9cb52c3563d3db91ac84e06836b0f6f199" - integrity sha512-k+NZksUGqc1Cz8eG+EzCPRyRalgho/xy4fh5Dqsbe9LwLeklrrtfAMaklPRtkt0yja8ueg1DGnCtHq00e99j4Q== +"@docknetwork/node-types@0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@docknetwork/node-types/-/node-types-0.15.0.tgz#eed5c719380865bf989ccd2550844dadb7abdd19" + integrity sha512-ACIHUIiAt82nhYxtwHSyS4JaJ28UbWS+fAwbTblKcsQBe7YRM2tjbLmkaqQjGPjxJS+wmh/xf7/PnA8PfboNZg== "@edgeware/node-types@3.6.2-wako": version "3.6.2-wako" @@ -1584,10 +1623,10 @@ resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== -"@equilab/definitions@1.4.14": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@equilab/definitions/-/definitions-1.4.14.tgz#c384f3eca003293d5f2c0a42235bdbe0a60626dd" - integrity sha512-F8jDESrhUpapqGSTXWND+5/DOqFlnh/oEejIYVIzF2WeUreHJzUPpI8a8Hb9plCLxj5sxYJ+3JL/5epMcrXDaQ== +"@equilab/definitions@1.4.18": + version "1.4.18" + resolved "https://registry.yarnpkg.com/@equilab/definitions/-/definitions-1.4.18.tgz#e544951b50278705af3d9fa4ba91e04df53a3d06" + integrity sha512-rFEPaHmdn5I1QItbQun9H/x+o3hgjA6kLYLrNN6nl/ndtQMY2tqx/mQfcGIlKA1xVmyn9mUAqD8G0P/nBHD3yA== "@eslint/eslintrc@^0.4.3": version "0.4.3" @@ -1985,6 +2024,15 @@ dependencies: tslib "2.4.0" +"@frequency-chain/api-augment@^1.0.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@frequency-chain/api-augment/-/api-augment-1.6.0.tgz#a611d191328e11ccf24aff82fe2d165b9b6a0eb8" + integrity sha512-OkyLC4ttgkB+6PpTN94NIWPgi6rEclzK7pBSULtfl6ZhgjW9IalykbJmispG3Ntgwdb69TMUU0wSdDPBS15r9A== + dependencies: + "@polkadot/api" "^10.3.2" + "@polkadot/rpc-provider" "^10.3.2" + "@polkadot/types" "^10.3.2" + "@gar/promisify@^1.0.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" @@ -2051,18 +2099,17 @@ resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg== -"@interlay/bridge@^0.2.4": - version "0.2.4" - resolved "https://registry.yarnpkg.com/@interlay/bridge/-/bridge-0.2.4.tgz#83f446575d1b66cac7601bc4b771c3b19b9137b5" - integrity sha512-XYgLhd4anvoaLL9C+Su/BDATd0K6rQipZXjQW3wuqTYyy+Pr7ItNGu4FbSGLqid1osn7b7No4sXQ5WwFJsZSQA== - dependencies: - "@acala-network/api" "4.1.8-9" - "@acala-network/sdk" "4.1.8-9" - "@acala-network/sdk-core" "4.1.8-9" - "@polkadot/api" "^9.11.1" - "@polkadot/apps-config" "^0.122.2" - "@polkadot/types" "^9.11.1" - "@polkadot/types-augment" "^9.11.1" +"@interlay/bridge@^0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@interlay/bridge/-/bridge-0.3.9.tgz#fc39c64708eab2f55cb0bbb970f2f0e72583cb37" + integrity sha512-QCeTux1f3LwLJ/dcfHmOTOuF8ocfpo9WDNV7Z1GWTHX/I8lspidj4xh8c/g2+jNZnHMiINXCSvHGPPr05lTnQg== + dependencies: + "@acala-network/api" "4.1.8-13" + "@acala-network/sdk" "4.1.8-13" + "@acala-network/sdk-core" "4.1.8-13" + "@polkadot/api" "^9.14.2" + "@polkadot/apps-config" "^0.124.1" + "@polkadot/types" "^9.14.2" axios "^0.27.2" lodash "^4.17.20" @@ -2090,16 +2137,16 @@ isomorphic-fetch "^3.0.0" regtest-client "^0.2.0" +"@interlay/interbtc-types@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@interlay/interbtc-types/-/interbtc-types-1.11.0.tgz#5b94066ddee1fd677de928531db36e6ae439e08f" + integrity sha512-bn3XjyRlXyhe1QKUHx5IEQJDNC6LoSCJJIkTnSp5xm52GRBEWgHOvLAnfJi3gyj7A3lV/yA2Xjqf294bZgMmfw== + "@interlay/interbtc-types@1.12.0": version "1.12.0" resolved "https://registry.yarnpkg.com/@interlay/interbtc-types/-/interbtc-types-1.12.0.tgz#07dc8e15690292387124dbc2bbb7bf5bc8b68001" integrity sha512-ELJa2ftIbe8Ds2ejS7kO5HumN9EB5l2OBi3Qsy5iHJsHKq2HtXfFoKnW38HarM6hADrWG+e/yNGHSKJIJzEZuA== -"@interlay/interbtc-types@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@interlay/interbtc-types/-/interbtc-types-1.9.0.tgz#beffd3b04bc1d9dba49f3ddc338b5867b81dec3d" - integrity sha512-G/jOHXM6lqoFAPquESAxsjt5ETrmcPTDC36WFRWYpoRUfFxcjq6TkWGxUC5/RS6jIoWMQ6lEJZupmlm/QNdbAg== - "@interlay/monetary-js@0.7.2": version "0.7.2" resolved "https://registry.yarnpkg.com/@interlay/monetary-js/-/monetary-js-0.7.2.tgz#a54a315b60be12f5b1a9c31f0d71d5e8ee7ba174" @@ -2434,10 +2481,10 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@kiltprotocol/type-definitions@^0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@kiltprotocol/type-definitions/-/type-definitions-0.2.1.tgz#0469b0bcc58063be0b02ffbf6f779176c6b0a00d" - integrity sha512-By09MH20P+rXadiZDnw2XeAw7bQLgNazOyNS3gPdU1L4Jx+lU9OtvIgZEA+T/TY/KM5nTv32s3c4wZ7v1s2znw== +"@kiltprotocol/type-definitions@^0.30.0": + version "0.30.0" + resolved "https://registry.yarnpkg.com/@kiltprotocol/type-definitions/-/type-definitions-0.30.0.tgz#00e99636a1c4405071021242cd509090c8f14287" + integrity sha512-1UpPDjX8PFqTFm3lRRfYUPEY9M8KrbpRinf4q4K843lY5GdTxQaevrVdK9/WCHKywLyDa4tSrlUv9KQjrTP4bg== "@laminar/type-definitions@0.3.1": version "0.3.1" @@ -2446,22 +2493,22 @@ dependencies: "@open-web3/orml-type-definitions" "^0.8.2-9" -"@logion/node-api@^0.7.0": - version "0.7.2" - resolved "https://registry.yarnpkg.com/@logion/node-api/-/node-api-0.7.2.tgz#af164f13831f1b89b130597ca70bf0e863f0b79f" - integrity sha512-EAyRp1MAAS4bGuxnuAYExssfwvFHLsmCyPWH0wMIik5eLHqNiPA1LwylYH5AQx8P1w11/nHVqRc5ly7dlf5E+w== +"@logion/node-api@^0.9.0-3": + version "0.9.0-3" + resolved "https://registry.yarnpkg.com/@logion/node-api/-/node-api-0.9.0-3.tgz#b02741acbf30517d537d48b75ffc83b366f09b87" + integrity sha512-6m2My8yI9jmhqP6FHPJdrsqdg/1vyJtY1/4cnuqCByJaVgNDGrcdtcmzW4BXCww+hJMrdm3PeLthKHCrwpo0gA== dependencies: - "@polkadot/api" "^9.8.1" - "@polkadot/util" "^10.1.12" - "@polkadot/util-crypto" "^10.1.12" + "@polkadot/api" "^9.10.1" + "@polkadot/util" "^10.2.1" + "@polkadot/util-crypto" "^10.2.1" "@types/uuid" "^8.3.4" fast-sha256 "^1.3.0" uuid "^8.3.2" -"@mangata-finance/types@^0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@mangata-finance/types/-/types-0.9.0.tgz#8272a01d87243f693d0e0889070afb0d00b4f91f" - integrity sha512-37yIP9xh+6kTt+UQJsbPt0OCrDIZsqGRh3f7sEtK7zX4G8uWrg/GHcHtGZQRruNIOAO8+1PLXuMfLokSzojVBw== +"@mangata-finance/types@^0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@mangata-finance/types/-/types-0.17.0.tgz#299b0bd21e30e17ee65c25f18d4a89871f521930" + integrity sha512-v0o7rePG4P2fDH1yVvSfuHpHQCA7Xki9IwPMTu51Y4FoQdvD1zHUOI4mIOc3ssjOAJsCePNdsTm+/xj3DeiSxQ== "@mdx-js/mdx@^1.6.22": version "1.6.22" @@ -2732,17 +2779,17 @@ dependencies: "@open-web3/orml-type-definitions" "1.1.4" -"@parallel-finance/type-definitions@1.7.13": - version "1.7.13" - resolved "https://registry.yarnpkg.com/@parallel-finance/type-definitions/-/type-definitions-1.7.13.tgz#08c92e07496d2757d9a89879e7d3d08b2bf49744" - integrity sha512-v2M1uCfBnQ2wiYEk/2ftTdSB4OV8nTL38qhB6ApykxHCdXu4Nz1lJKFjW8OW1DTXB8xCfkj8VX8eY0UDez535g== +"@parallel-finance/type-definitions@1.7.14": + version "1.7.14" + resolved "https://registry.yarnpkg.com/@parallel-finance/type-definitions/-/type-definitions-1.7.14.tgz#02ca0d8a8d2894fa1d22c8625bd4edfcfbeffc4c" + integrity sha512-64cIrOcS5z2SSzTAITg3qDdQReoBLCZhAGHzR1VnYQzF0u59Ow6XnWmg0/R4EuyhnsqW4aMhnrmlVE7RhG9kPg== dependencies: "@open-web3/orml-type-definitions" "^1.1.4" -"@phala/typedefs@0.2.32": - version "0.2.32" - resolved "https://registry.yarnpkg.com/@phala/typedefs/-/typedefs-0.2.32.tgz#4c66dce9b5a975226bbbdbdef09bccfde2e54878" - integrity sha512-G1ifICDNW6NtixqCVfJHBI82Detwzzmzs4gpE1RrMsTxfoKIbxkX8nx3DxZUhFYqikGEkQVxNmEi3jpC0zDrsw== +"@phala/typedefs@0.2.33": + version "0.2.33" + resolved "https://registry.yarnpkg.com/@phala/typedefs/-/typedefs-0.2.33.tgz#6f18d73b5104db6a594d08be571954385b3e509b" + integrity sha512-CaRzIGfU6CUIKLPswYtOw/xbtTttqmJZpr3fhkxLvkBQMXIH14iISD763OFXtWui7DrAMBKo/bHawvFNgWGKTg== "@pmmmwh/react-refresh-webpack-plugin@0.4.3", "@pmmmwh/react-refresh-webpack-plugin@^0.4.3": version "0.4.3" @@ -2829,7 +2876,7 @@ "@polkadot/util-crypto" "^10.4.2" rxjs "^7.8.0" -"@polkadot/api-derive@9.10.3", "@polkadot/api-derive@9.14.2", "@polkadot/api-derive@^8.5.1", "@polkadot/api-derive@^9.14.2", "@polkadot/api-derive@^9.7.1": +"@polkadot/api-derive@9.10.3", "@polkadot/api-derive@9.14.2", "@polkadot/api-derive@^8.5.1", "@polkadot/api-derive@^9.13.2", "@polkadot/api-derive@^9.14.2": version "9.14.2" resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-9.14.2.tgz#e8fcd4ee3f2b80b9fe34d4dec96169c3bdb4214d" integrity sha512-yw9OXucmeggmFqBTMgza0uZwhNjPxS7MaT7lSCUIRKckl1GejdV+qMhL3XFxPFeYzXwzFpdPG11zWf+qJlalqw== @@ -2845,7 +2892,7 @@ "@polkadot/util-crypto" "^10.4.2" rxjs "^7.8.0" -"@polkadot/api@9.10.3", "@polkadot/api@9.14.2", "@polkadot/api@^7.2.1", "@polkadot/api@^9.11.1", "@polkadot/api@^9.14.2", "@polkadot/api@^9.4.2", "@polkadot/api@^9.7.1", "@polkadot/api@^9.8.1", "@polkadot/api@^9.9.1", "@polkadot/api@latest": +"@polkadot/api@9.10.3", "@polkadot/api@9.14.2", "@polkadot/api@^10.3.2", "@polkadot/api@^7.2.1", "@polkadot/api@^9.10.1", "@polkadot/api@^9.13.2", "@polkadot/api@^9.14.2", "@polkadot/api@^9.4.2", "@polkadot/api@^9.9.1", "@polkadot/api@latest": version "9.14.2" resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-9.14.2.tgz#d5cee02236654c6063d7c4b70c78c290db5aba8d" integrity sha512-R3eYFj2JgY1zRb+OCYQxNlJXCs2FA+AU4uIEiVcXnVLmR3M55tkRNEwYAZmiFxx0pQmegGgPMc33q7TWGdw24A== @@ -2868,48 +2915,49 @@ eventemitter3 "^5.0.0" rxjs "^7.8.0" -"@polkadot/apps-config@^0.122.2": - version "0.122.2" - resolved "https://registry.yarnpkg.com/@polkadot/apps-config/-/apps-config-0.122.2.tgz#b15d2dbfc43b0e8bc32fc14bd56b97de3abb83e3" - integrity sha512-EBINVhOe4w5gzjOJ/lIzYJDP1DiZY8SWBf8Jp25DOwdSvUsyV1AYyrGAgmz+kE+jtakEMKOZpXdRt3OwGYLPqw== +"@polkadot/apps-config@^0.124.1": + version "0.124.1" + resolved "https://registry.yarnpkg.com/@polkadot/apps-config/-/apps-config-0.124.1.tgz#4d993fcc198118dfe4aa9200ce6055b48cab96b3" + integrity sha512-SqDLf0ksU5WkU96L3nIiICwaBDLj4APYjKkwpSUAWk1NcvXDWZQQG56obgaLPHZ2If6GZrQge/fUmItuRBIZrg== dependencies: - "@acala-network/type-definitions" "^4.1.5" - "@babel/runtime" "^7.20.1" - "@bifrost-finance/type-definitions" "1.7.1" + "@acala-network/type-definitions" "^4.1.8-1" + "@babel/runtime" "^7.20.13" + "@bifrost-finance/type-definitions" "1.7.2" "@crustio/type-definitions" "1.3.0" "@darwinia/types" "2.8.10" "@darwinia/types-known" "2.8.10" "@digitalnative/type-definitions" "1.1.27" - "@docknetwork/node-types" "0.13.0" + "@docknetwork/node-types" "0.15.0" "@edgeware/node-types" "3.6.2-wako" - "@equilab/definitions" "1.4.14" - "@interlay/interbtc-types" "1.9.0" - "@kiltprotocol/type-definitions" "^0.2.1" + "@equilab/definitions" "1.4.18" + "@frequency-chain/api-augment" "^1.0.0" + "@interlay/interbtc-types" "1.11.0" + "@kiltprotocol/type-definitions" "^0.30.0" "@laminar/type-definitions" "0.3.1" - "@logion/node-api" "^0.7.0" - "@mangata-finance/types" "^0.9.0" + "@logion/node-api" "^0.9.0-3" + "@mangata-finance/types" "^0.17.0" "@metaverse-network-sdk/type-definitions" "^0.0.1-13" - "@parallel-finance/type-definitions" "1.7.13" - "@phala/typedefs" "0.2.32" - "@polkadot/api" "^9.7.1" - "@polkadot/api-derive" "^9.7.1" - "@polkadot/networks" "^10.1.11" - "@polkadot/types" "^9.7.1" - "@polkadot/util" "^10.1.11" - "@polkadot/x-fetch" "^10.1.11" + "@parallel-finance/type-definitions" "1.7.14" + "@phala/typedefs" "0.2.33" + "@polkadot/api" "^9.13.2" + "@polkadot/api-derive" "^9.13.2" + "@polkadot/networks" "^10.3.1" + "@polkadot/types" "^9.13.2" + "@polkadot/util" "^10.3.1" + "@polkadot/x-fetch" "^10.3.1" "@polymathnetwork/polymesh-types" "0.0.2" "@snowfork/snowbridge-types" "0.2.7" - "@sora-substrate/type-definitions" "1.10.21" - "@subsocial/definitions" "^0.7.8-dev.0" - "@unique-nft/opal-testnet-types" "930.31.0" - "@unique-nft/quartz-mainnet-types" "930.31.0" - "@unique-nft/unique-mainnet-types" "930.31.0" - "@zeitgeistpm/type-defs" "0.9.0" + "@sora-substrate/type-definitions" "1.12.4" + "@subsocial/definitions" "^0.7.9" + "@unique-nft/opal-testnet-types" "930.34.0" + "@unique-nft/quartz-mainnet-types" "930.34.0" + "@unique-nft/unique-mainnet-types" "930.33.0" + "@zeitgeistpm/type-defs" "0.10.0" "@zeroio/type-definitions" "0.0.14" lodash "^4.17.21" moonbeam-types-bundle "2.0.9" pontem-types-bundle "1.0.15" - rxjs "^7.5.7" + rxjs "^7.8.0" "@polkadot/extension-dapp@0.44.1": version "0.44.1" @@ -2942,6 +2990,15 @@ "@polkadot/util" "10.4.2" "@polkadot/util-crypto" "10.4.2" +"@polkadot/keyring@^10.3.1": + version "10.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-10.3.1.tgz#f13fed33686ff81b1e486721e52299eba9e6c4a6" + integrity sha512-xBkUtyQ766NVS1ccSYbQssWpxAhSf0uwkw9Amj8TFhu++pnZcVm+EmM2VczWqgOkmWepO7MGRjEXeOIw1YUGiw== + dependencies: + "@babel/runtime" "^7.20.13" + "@polkadot/util" "10.3.1" + "@polkadot/util-crypto" "10.3.1" + "@polkadot/keyring@^6.9.1": version "6.11.1" resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-6.11.1.tgz#2510c349c965c74cc2f108f114f1048856940604" @@ -2969,7 +3026,7 @@ "@polkadot/util" "8.7.1" "@polkadot/util-crypto" "8.7.1" -"@polkadot/networks@10.4.2", "@polkadot/networks@^10.1.11", "@polkadot/networks@^10.1.6", "@polkadot/networks@^10.4.2": +"@polkadot/networks@10.4.2", "@polkadot/networks@^10.1.6", "@polkadot/networks@^10.4.2": version "10.4.2" resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-10.4.2.tgz#d7878c6aad8173c800a21140bfe5459261724456" integrity sha512-FAh/znrEvWBiA/LbcT5GXHsCFUl//y9KqxLghSr/CreAmAergiJNT0MVUezC7Y36nkATgmsr4ylFwIxhVtuuCw== @@ -2978,6 +3035,32 @@ "@polkadot/util" "10.4.2" "@substrate/ss58-registry" "^1.38.0" +"@polkadot/networks@^10.3.1": + version "10.3.1" + resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-10.3.1.tgz#097a2c4cd25eff59fe6c11299f58feedd4335042" + integrity sha512-W9E1g6zRbIVyF7sGqbpxH0P6caxtBHNEwvDa5/8ZQi9UsLj6mUs0HdwZtAdIo3KcSO4uAyV9VYJjY/oAWWcnXg== + dependencies: + "@babel/runtime" "^7.20.13" + "@polkadot/util" "10.3.1" + "@substrate/ss58-registry" "^1.38.0" + +"@polkadot/react-identicon@^2.11.1": + version "2.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/react-identicon/-/react-identicon-2.11.1.tgz#8f81f142f7c7763fe2d499580b85b3879f4eb081" + integrity sha512-pqEsiXuKOXDrXNnsFB1JyBbFqedbiDwtP0yewIQ9vtwJwm01o7oE0yGfYS88hnPN1mLDc++MMcqvrHBsxYr2Lw== + dependencies: + "@babel/runtime" "^7.20.13" + "@polkadot/keyring" "^10.3.1" + "@polkadot/ui-settings" "2.11.1" + "@polkadot/ui-shared" "2.11.1" + "@polkadot/util" "^10.3.1" + "@polkadot/util-crypto" "^10.3.1" + color "^3.2.1" + ethereum-blockies-base64 "^1.0.2" + jdenticon "3.2.0" + react-copy-to-clipboard "^5.1.0" + styled-components "^5.3.6" + "@polkadot/rpc-augment@9.14.2", "@polkadot/rpc-augment@^9.14.2": version "9.14.2" resolved "https://registry.yarnpkg.com/@polkadot/rpc-augment/-/rpc-augment-9.14.2.tgz#eb70d5511463dab8d995faeb77d4edfe4952fe26" @@ -3001,7 +3084,7 @@ "@polkadot/util" "^10.4.2" rxjs "^7.8.0" -"@polkadot/rpc-provider@9.14.2", "@polkadot/rpc-provider@^8.7.1", "@polkadot/rpc-provider@^9.14.2": +"@polkadot/rpc-provider@9.14.2", "@polkadot/rpc-provider@^10.3.2", "@polkadot/rpc-provider@^8.7.1", "@polkadot/rpc-provider@^9.14.2": version "9.14.2" resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-9.14.2.tgz#0dea667f3a03bf530f202cba5cd360df19b32e30" integrity sha512-YTSywjD5PF01V47Ru5tln2LlpUwJiSOdz6rlJXPpMaY53hUp7+xMU01FVAQ1bllSBNisSD1Msv/mYHq84Oai2g== @@ -3021,7 +3104,7 @@ optionalDependencies: "@substrate/connect" "0.7.19" -"@polkadot/types-augment@9.14.2", "@polkadot/types-augment@^9.11.1", "@polkadot/types-augment@^9.14.2": +"@polkadot/types-augment@9.14.2", "@polkadot/types-augment@^9.14.2": version "9.14.2" resolved "https://registry.yarnpkg.com/@polkadot/types-augment/-/types-augment-9.14.2.tgz#1a478e18e713b04038f3e171287ee5abe908f0aa" integrity sha512-WO9d7RJufUeY3iFgt2Wz762kOu1tjEiGBR5TT4AHtpEchVHUeosVTrN9eycC+BhleqYu52CocKz6u3qCT/jKLg== @@ -3069,7 +3152,7 @@ "@babel/runtime" "^7.20.13" "@polkadot/util" "^10.4.2" -"@polkadot/types@9.10.3", "@polkadot/types@9.14.2", "@polkadot/types@^4.13.1", "@polkadot/types@^6.0.5", "@polkadot/types@^7.2.1", "@polkadot/types@^8.7.1", "@polkadot/types@^9.11.1", "@polkadot/types@^9.14.2", "@polkadot/types@^9.7.1", "@polkadot/types@^9.9.1": +"@polkadot/types@9.10.3", "@polkadot/types@9.14.2", "@polkadot/types@^10.3.2", "@polkadot/types@^4.13.1", "@polkadot/types@^6.0.5", "@polkadot/types@^7.2.1", "@polkadot/types@^8.7.1", "@polkadot/types@^9.13.2", "@polkadot/types@^9.14.2", "@polkadot/types@^9.9.1": version "9.14.2" resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-9.14.2.tgz#5105f41eb9e8ea29938188d21497cbf1753268b8" integrity sha512-hGLddTiJbvowhhUZJ3k+olmmBc1KAjWIQxujIUIYASih8FQ3/YJDKxaofGOzh0VygOKW3jxQBN2VZPofyDP9KQ== @@ -3097,6 +3180,17 @@ rxjs "^7.5.6" store "^2.0.12" +"@polkadot/ui-settings@2.11.1": + version "2.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/ui-settings/-/ui-settings-2.11.1.tgz#e3474097a6f4246423731e9b5cce3a5bb9482349" + integrity sha512-7yZwb3VxGh7VPHkyygktL7Oep0c4XUyKkYGwSmgP2Gt2IcvnGXFUQVSEARKs9FCanl19f2CEU1m19+FFrjlUNQ== + dependencies: + "@babel/runtime" "^7.20.13" + "@polkadot/networks" "^10.3.1" + "@polkadot/util" "^10.3.1" + eventemitter3 "^4.0.7" + store "^2.0.12" + "@polkadot/ui-settings@2.9.7": version "2.9.7" resolved "https://registry.yarnpkg.com/@polkadot/ui-settings/-/ui-settings-2.9.7.tgz#c9fcd7dc8d1de36826e06c347f27d9a9df56810c" @@ -3108,7 +3202,15 @@ eventemitter3 "^4.0.7" store "^2.0.12" -"@polkadot/util-crypto@10.4.2", "@polkadot/util-crypto@6.11.1", "@polkadot/util-crypto@7.9.2", "@polkadot/util-crypto@8.7.1", "@polkadot/util-crypto@^10.1.12", "@polkadot/util-crypto@^10.1.6", "@polkadot/util-crypto@^10.2.1", "@polkadot/util-crypto@^10.2.4", "@polkadot/util-crypto@^10.4.2", "@polkadot/util-crypto@^9.4.1": +"@polkadot/ui-shared@2.11.1": + version "2.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/ui-shared/-/ui-shared-2.11.1.tgz#b4dfe2310003ce4621fcbb5e94daa8c76b45a028" + integrity sha512-+qCLPT3SEnHOG3WvO0iYSJ6zArPQGCz9nHx8X8rw9GhffdiEC20ae63jB6dQTjR5GppPQx0aLE/cOppWn/HpRg== + dependencies: + "@babel/runtime" "^7.20.13" + color "^3.2.1" + +"@polkadot/util-crypto@10.3.1", "@polkadot/util-crypto@10.4.2", "@polkadot/util-crypto@6.11.1", "@polkadot/util-crypto@7.9.2", "@polkadot/util-crypto@8.7.1", "@polkadot/util-crypto@^10.1.6", "@polkadot/util-crypto@^10.2.1", "@polkadot/util-crypto@^10.2.4", "@polkadot/util-crypto@^10.3.1", "@polkadot/util-crypto@^10.4.2", "@polkadot/util-crypto@^9.4.1": version "10.4.2" resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-10.4.2.tgz#871fb69c65768bd48c57bb5c1f76a85d979fb8b5" integrity sha512-RxZvF7C4+EF3fzQv8hZOLrYCBq5+wA+2LWv98nECkroChY3C2ZZvyWDqn8+aonNULt4dCVTWDZM0QIY6y4LUAQ== @@ -3125,7 +3227,7 @@ ed2curve "^0.3.0" tweetnacl "^1.0.3" -"@polkadot/util@10.4.2", "@polkadot/util@6.11.1", "@polkadot/util@7.9.2", "@polkadot/util@8.7.1", "@polkadot/util@^10.1.11", "@polkadot/util@^10.1.12", "@polkadot/util@^10.1.6", "@polkadot/util@^10.2.1", "@polkadot/util@^10.2.4", "@polkadot/util@^10.4.2", "@polkadot/util@^9.4.1": +"@polkadot/util@10.3.1", "@polkadot/util@10.4.2", "@polkadot/util@6.11.1", "@polkadot/util@7.9.2", "@polkadot/util@8.7.1", "@polkadot/util@^10.1.6", "@polkadot/util@^10.2.1", "@polkadot/util@^10.2.4", "@polkadot/util@^10.3.1", "@polkadot/util@^10.4.2", "@polkadot/util@^9.4.1": version "10.4.2" resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-10.4.2.tgz#df41805cb27f46b2b4dad24c371fa2a68761baa1" integrity sha512-0r5MGICYiaCdWnx+7Axlpvzisy/bi1wZGXgCSw5+ZTyPTOqvsYRqM2X879yxvMsGfibxzWqNzaiVjToz1jvUaA== @@ -3197,7 +3299,7 @@ "@babel/runtime" "^7.20.13" "@polkadot/x-global" "10.4.2" -"@polkadot/x-fetch@^10.1.11", "@polkadot/x-fetch@^10.4.2": +"@polkadot/x-fetch@^10.3.1", "@polkadot/x-fetch@^10.4.2": version "10.4.2" resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-10.4.2.tgz#bc6ba70de71a252472fbe36180511ed920e05f05" integrity sha512-Ubb64yaM4qwhogNP+4mZ3ibRghEg5UuCYRMNaCFoPgNAY8tQXuDKrHzeks3+frlmeH9YRd89o8wXLtWouwZIcw== @@ -4599,10 +4701,10 @@ "@polkadot/keyring" "^8.2.2" "@polkadot/types" "^7.2.1" -"@sora-substrate/type-definitions@1.10.21": - version "1.10.21" - resolved "https://registry.yarnpkg.com/@sora-substrate/type-definitions/-/type-definitions-1.10.21.tgz#1fb225cc8036729cdfb8fd2fcdc72bfa18251781" - integrity sha512-QPtJk6ZjPK9RwpMG+YdMI319dRbSr01C5D52TNOf9UAk6FA9fGTXtn6kH6pR185Ssu/Ww50LmU+NpDP45RPYVA== +"@sora-substrate/type-definitions@1.12.4": + version "1.12.4" + resolved "https://registry.yarnpkg.com/@sora-substrate/type-definitions/-/type-definitions-1.12.4.tgz#e4bfc1d5d58c20dd589cfcd73ab86f798684221f" + integrity sha512-G+s1DTKGfkncUXUPXQNNrbj/9ZnNxksEXBmqP/RQrmnfYE3C59P5Zkp+D98WsXobkWOnMxqBDlK+VbUQbvMoRA== dependencies: "@open-web3/orml-type-definitions" "0.9.4-26" @@ -5434,7 +5536,7 @@ regenerator-runtime "^0.13.7" resolve-from "^5.0.0" -"@subsocial/definitions@^0.7.8-dev.0": +"@subsocial/definitions@^0.7.9": version "0.7.14" resolved "https://registry.yarnpkg.com/@subsocial/definitions/-/definitions-0.7.14.tgz#1397f1ec806d60d9deb112b9f36d530400b711fe" integrity sha512-dor5S6/tbY09n40e/dh7qFcqF9slMihOMDTXWBM5hTe8nS/Pf5Zp4/r9WiZxxYLoY2v5MlSqyJxjiSCjTxxjUw== @@ -5465,9 +5567,9 @@ ws "^8.8.1" "@substrate/ss58-registry@^1.38.0": - version "1.39.0" - resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.39.0.tgz#eb916ff5fea7fa02e77745823fde21af979273d2" - integrity sha512-qZYpuE6n+mwew+X71dOur/CbMXj6rNW27o63JeJwdQH/GvcSKm3JLNhd+bGzwUKg0D/zD30Qc6p4JykArzM+tA== + version "1.38.0" + resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.38.0.tgz#b50cb28c77a0375fbf33dd29b7b28ee32871af9f" + integrity sha512-sHiVRWekGMRZAjPukN9/W166NM6D5wtHcK6RVyLy66kg3CHNZ1BXfpXcjOiXSwhbd7guQFDEwnOVaDrbk1XL1g== "@surma/rollup-plugin-off-main-thread@^1.1.1": version "1.4.2" @@ -6311,20 +6413,20 @@ "@typescript-eslint/types" "4.33.0" eslint-visitor-keys "^2.0.0" -"@unique-nft/opal-testnet-types@930.31.0": - version "930.31.0" - resolved "https://registry.yarnpkg.com/@unique-nft/opal-testnet-types/-/opal-testnet-types-930.31.0.tgz#dc989976b5e91b4d8358a7af624d6e5e2ebf0b87" - integrity sha512-IY4AxUx3uqjMEXy6iXrfVmu4oDTXOXaPjg5sb3WqnXpA7czjfSWZsQ/OtJFAWO+cbXUt8DM9ifs9/2hY2+O4RA== +"@unique-nft/opal-testnet-types@930.34.0": + version "930.34.0" + resolved "https://registry.yarnpkg.com/@unique-nft/opal-testnet-types/-/opal-testnet-types-930.34.0.tgz#e4274976ebc9614dbec6c1a074674a3620eacb6f" + integrity sha512-6N5MQC5o4V5J0PZ/JmhRfYOtJTSpCjxxM1pdGysh6aIu/rSey8ELa/9BnGwLIZsOPxW77PKwnt7NIRc01Sze3g== -"@unique-nft/quartz-mainnet-types@930.31.0": - version "930.31.0" - resolved "https://registry.yarnpkg.com/@unique-nft/quartz-mainnet-types/-/quartz-mainnet-types-930.31.0.tgz#009a37ac2dad085cfe0ebdca98de06f345fa35d6" - integrity sha512-oZdHnX2TfglJ43PjKwAnUhx5VIcCDpeeQtRC8cXSvVKE2CqkW5ry/rgyavAs+HeUrwsD5JXHtebgH9P1dZ4vOw== +"@unique-nft/quartz-mainnet-types@930.34.0": + version "930.34.0" + resolved "https://registry.yarnpkg.com/@unique-nft/quartz-mainnet-types/-/quartz-mainnet-types-930.34.0.tgz#d99a744b10575533441a0ca13f855eeca45a9047" + integrity sha512-YwJ3h7Q0crnvGsYfBXjxtPIpQnB9T5JY1LLAapLGvOO3A0iA1PWbSiqAgOdjZTt4zivYm3IbdhxQhyyY6d5jLA== -"@unique-nft/unique-mainnet-types@930.31.0": - version "930.31.0" - resolved "https://registry.yarnpkg.com/@unique-nft/unique-mainnet-types/-/unique-mainnet-types-930.31.0.tgz#ea79a6fd3d9e8c115b13ef3048a87bf0a853c269" - integrity sha512-acAKLL2TNS7X886SiNjMHo0dVmCECFd9vUSJxBZ1yjVVOhf6P6Nn+gA8jjgLKSg89VAqNQ9Op7a/vBN0Xo8+nw== +"@unique-nft/unique-mainnet-types@930.33.0": + version "930.33.0" + resolved "https://registry.yarnpkg.com/@unique-nft/unique-mainnet-types/-/unique-mainnet-types-930.33.0.tgz#196bbe704882ad826b709c5ec9cbbb8067e456ee" + integrity sha512-KlliDzrwcyl1igi/rjltue/T6DZQP5yAijcFzWtCsKfLzkCPxcplzYgd5S+VKRoAFrndOMVXleXTUgpPSYiL9Q== "@uphold/request-logger@^2.0.0": version "2.0.0" @@ -6614,10 +6716,10 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -"@zeitgeistpm/type-defs@0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@zeitgeistpm/type-defs/-/type-defs-0.9.0.tgz#95496d4c7984c87cf53eeed1b97283e5c4538362" - integrity sha512-H3EuEjrKtMlZBKEl08427Bda/c0t9BaUiwBNPn2T8ppM1RCEzfd1/3riHce6CyBCAQKR+w47Dylc+qK2VrBbNQ== +"@zeitgeistpm/type-defs@0.10.0": + version "0.10.0" + resolved "https://registry.yarnpkg.com/@zeitgeistpm/type-defs/-/type-defs-0.10.0.tgz#7f551f949b45b082541a254a9845ab15b2ff9148" + integrity sha512-nQBdyRbkIopPOVjRHk9c/RBWiQI6iYE8fs5rmtSNCXm6IxoXssk/1PtWE+UxXXq9mco7rPao9nJMeYXJ1Ro2kg== "@zeroio/type-definitions@0.0.14": version "0.0.14" @@ -8160,6 +8262,13 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, can resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001342.tgz#87152b1e3b950d1fbf0093e23f00b6c8e8f1da96" integrity sha512-bn6sOCu7L7jcbBbyNhLg0qzXdJ/PMbybZTH/BA6Roet9wxYRm6Tr9D0s0uhLkOZ6MSG+QU6txUgdpr3MXIVqjA== +canvas-renderer@~2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/canvas-renderer/-/canvas-renderer-2.2.1.tgz#c1d131f78a9799aca8af9679ad0a005052b65550" + integrity sha512-RrBgVL5qCEDIXpJ6NrzyRNoTnXxYarqm/cS/W6ERhUJts5UQtt/XPEosGN3rqUkZ4fjBArlnCbsISJ+KCFnIAg== + dependencies: + "@types/node" "*" + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -8547,7 +8656,7 @@ color-support@^1.1.2: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -color@^3.0.0: +color@^3.0.0, color@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== @@ -10464,6 +10573,13 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +ethereum-blockies-base64@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ethereum-blockies-base64/-/ethereum-blockies-base64-1.0.2.tgz#4aebca52142bf4d16a3144e6e2b59303e39ed2b3" + integrity sha512-Vg2HTm7slcWNKaRhCUl/L3b4KrB8ohQXdd5Pu3OI897EcR6tVRvUqdTwAyx+dnmoDzj8e2bwBLDQ50ByFmcz6w== + dependencies: + pnglib "0.0.1" + ethers@^5.6.2, ethers@~5.7.0: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" @@ -13086,6 +13202,13 @@ iterate-value@^1.0.2: es-get-iterator "^1.0.2" iterate-iterator "^1.0.1" +jdenticon@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jdenticon/-/jdenticon-3.2.0.tgz#b5b9ef413cb66f70c600d6e69a764c977f248a46" + integrity sha512-z6Iq3fTODUMSOiR2nNYrqigS6Y0GvdXfyQWrUby7htDHvX7GNEwaWR4hcaL+FmhEgBe08Xkup/BKxXQhDJByPA== + dependencies: + canvas-renderer "~2.2.0" + jest-changed-files@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" @@ -15843,6 +15966,11 @@ please-upgrade-node@^3.2.0: dependencies: semver-compare "^1.0.0" +pnglib@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pnglib/-/pnglib-0.0.1.tgz#f9ab6f9c688f4a9d579ad8be28878a716e30c096" + integrity sha512-95ChzOoYLOPIyVmL+Y6X+abKGXUJlvOVLkB1QQkyXl7Uczc6FElUy/x01NS7r2GX6GRezloO/ecCX9h4U9KadA== + pnp-webpack-plugin@1.6.4: version "1.6.4" resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149" @@ -17071,6 +17199,14 @@ react-chartjs-2@^2.11.1: lodash "^4.17.19" prop-types "^15.7.2" +react-copy-to-clipboard@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz#09aae5ec4c62750ccb2e6421a58725eabc41255c" + integrity sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A== + dependencies: + copy-to-clipboard "^3.3.1" + prop-types "^15.8.1" + react-dev-utils@^11.0.3: version "11.0.4" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.4.tgz#a7ccb60257a1ca2e0efe7a83e38e6700d17aa37a" @@ -19177,7 +19313,7 @@ style-to-object@0.3.0, style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" -styled-components@^5, styled-components@^5.3.5: +styled-components@^5, styled-components@^5.3.5, styled-components@^5.3.6: version "5.3.5" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.5.tgz#a750a398d01f1ca73af16a241dec3da6deae5ec4" integrity sha512-ndETJ9RKaaL6q41B69WudeqLzOpY1A/ET/glXkNZ2T7dPjPqpPCXXQjDFYZWwNnE5co0wX+gTCqx9mfxTmSIPg== From 65b0748a28359fedb8695c4999f8cfdd1bfd9443 Mon Sep 17 00:00:00 2001 From: Thomas Jeatt Date: Mon, 22 May 2023 10:24:18 +0100 Subject: [PATCH 003/241] chore: release v2.32.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1362974961..c87aea2430 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interbtc-ui", - "version": "2.31.3", + "version": "2.32.0", "private": true, "dependencies": { "@craco/craco": "^6.1.1", From 8ff52df66060022c67842aad20fb1c4b8ca58389 Mon Sep 17 00:00:00 2001 From: ns212 <73105077+ns212@users.noreply.github.com> Date: Mon, 22 May 2023 13:03:32 +0300 Subject: [PATCH 004/241] Update API healthchecks (#778) * Chore - add vault healthcheck * Chore - add vault healthcheck * Chore - add vault healthcheck --- api/health.py | 91 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 18 deletions(-) diff --git a/api/health.py b/api/health.py index 6638f2a4e8..ae977b2b53 100644 --- a/api/health.py +++ b/api/health.py @@ -2,7 +2,7 @@ import dateutil.parser from datetime import datetime from dateutil.tz import tzutc -from flask import Flask, jsonify +from flask import Flask, jsonify, abort class Oracle: @@ -80,30 +80,85 @@ def isHealthy(self): return status["chainHeightDiff"] < 3 and status["secondsDiff"] < 7200 # 2hrs -KSM_URL = "https://api-kusama.interlay.io/graphql/graphql" -INTR_URL = "https://api.interlay.io/graphql/graphql" - -app = Flask(__name__) - +class Vault: + def __init__(self, baseUrl) -> None: + self.baseUrl = baseUrl -@app.route("/_health/ksm/oracle", methods=["GET"]) -def get_ksm_oracle_health(): - return jsonify(Oracle(KSM_URL, "KSM").isHealthy()) + def _latestVaults(self): + q = """ + query MyQuery { + vaults(limit: 10, orderBy: registrationBlock_active_DESC) { + id + registrationTimestamp + } + } + """ + payload = {"query": q, "variables": None} + resp = requests.post(self.baseUrl, json=payload) + return resp.json()["data"]["vaults"] + def isHealthy(self): + vaults = self._latestVaults() + return len(self._latestVaults()) > 0 -@app.route("/_health/ksm/relay", methods=["GET"]) -def get_ksm_relayer_health(): - return jsonify(Relayer(KSM_URL).isHealthy()) +KSM_URL = "https://api-kusama.interlay.io/graphql/graphql" +INTR_URL = "https://api.interlay.io/graphql/graphql" +TESTNET_INTR = "https://api-testnet.interlay.io/graphql/graphql" +TESTNET_KINT = "https://api-dev-kintsugi.interlay.io/graphql/graphql" -@app.route("/_health/intr/oracle", methods=["GET"]) -def get_intr_oracle_health(): - return jsonify(Oracle(INTR_URL, "DOT").isHealthy()) +app = Flask(__name__) -@app.route("/_health/intr/relay", methods=["GET"]) -def get_intr_relayer_health(): - return jsonify(Relayer(INTR_URL).isHealthy()) +@app.route("/_health//oracle", methods=["GET"]) +def get_oracle_health(chain): + def oracle(): + if chain == "kint": + return Oracle(KSM_URL, "KSM") + elif chain == "intr": + return Oracle(INTR_URL, "DOT") + elif chain == "testnet_kint": + return Oracle(TESTNET_KINT, "KSM") + elif chain == "testnet_intr": + return Oracle(TESTNET_INTR, "DOT") + else: + abort(404) + + return jsonify(oracle().isHealthy()) + + +@app.route("/_health//relay", methods=["GET"]) +def get_relay_health(chain): + def relay(): + if chain == "kint": + return Relayer(KSM_URL) + elif chain == "intr": + return Relayer(INTR_URL) + elif chain == "testnet_kint": + return Relayer(TESTNET_KINT) + elif chain == "testnet_intr": + return Relayer(TESTNET_INTR) + else: + abort(404) + + return jsonify(relay().isHealthy()) + + +@app.route("/_health//vault", methods=["GET"]) +def get_vault_health(chain): + def vault(): + if chain == "kint": + return Vault(KSM_URL) + elif chain == "intr": + return Vault(INTR_URL) + elif chain == "testnet_kint": + return Vault(TESTNET_KINT) + elif chain == "testnet_intr": + return Vault(TESTNET_INTR) + else: + abort(404) + + return jsonify(vault().isHealthy()) if __name__ == "__main__": From e9157c74b80263b29d826b112fbf25029e9394c2 Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Mon, 22 May 2023 13:55:41 +0100 Subject: [PATCH 005/241] [earn strategies] placeholder page, nav and feature flag (#1216) * chore: bump icons dependency * feature: earn strategies placeholder page and feature flag --- .env.dev | 2 +- package.json | 2 +- src/App.tsx | 13 ++++++++++--- src/assets/locales/en/translation.json | 1 + src/pages/EarnStrategies/EarnStrategies.tsx | 14 ++++++++++++++ src/pages/EarnStrategies/index.tsx | 3 +++ .../Sidebar/SidebarContent/Navigation/index.tsx | 16 +++++++++++++++- src/utils/constants/links.ts | 1 + src/utils/hooks/use-feature-flag.ts | 6 ++++-- yarn.lock | 8 ++++---- 10 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 src/pages/EarnStrategies/EarnStrategies.tsx create mode 100644 src/pages/EarnStrategies/index.tsx diff --git a/.env.dev b/.env.dev index 11f16713a9..95c64b470e 100644 --- a/.env.dev +++ b/.env.dev @@ -4,7 +4,7 @@ REACT_APP_FEATURE_FLAG_LENDING=enabled REACT_APP_FEATURE_FLAG_AMM=enabled REACT_APP_FEATURE_FLAG_WALLET=enabled REACT_APP_FEATURE_FLAG_BANXA=enabled - +REACT_APP_FEATURE_FLAG_EARN_STRATEGIES=enabled /* DEVELOPMENT */ diff --git a/package.json b/package.json index c87aea2430..7e6cdc4d90 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "dependencies": { "@craco/craco": "^6.1.1", "@headlessui/react": "^1.1.1", - "@heroicons/react": "^2.0.0", + "@heroicons/react": "^2.0.18", "@interlay/bridge": "^0.3.9", "@interlay/interbtc-api": "2.2.2", "@interlay/monetary-js": "0.7.2", diff --git a/src/App.tsx b/src/App.tsx index 3a16fbf509..463f6f1528 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,7 @@ import TestnetBanner from './legacy-components/TestnetBanner'; import { FeatureFlags, useFeatureFlag } from './utils/hooks/use-feature-flag'; const Bridge = React.lazy(() => import(/* webpackChunkName: 'bridge' */ '@/pages/Bridge')); +const EarnStrategies = React.lazy(() => import(/* webpackChunkName: 'earn-strategies' */ '@/pages/EarnStrategies')); const Transfer = React.lazy(() => import(/* webpackChunkName: 'transfer' */ '@/pages/Transfer')); const Transactions = React.lazy(() => import(/* webpackChunkName: 'transactions' */ '@/pages/Transactions')); const TX = React.lazy(() => import(/* webpackChunkName: 'tx' */ '@/pages/TX')); @@ -35,9 +36,9 @@ const Vaults = React.lazy(() => import(/* webpackChunkName: 'vaults' */ '@/pages // TODO: last task will be to delete legacy dashboard and rename vault dashboard const Vault = React.lazy(() => import(/* webpackChunkName: 'vault' */ '@/pages/Vaults/Vault')); const Loans = React.lazy(() => import(/* webpackChunkName: 'loans' */ '@/pages/Loans')); -const Swap = React.lazy(() => import(/* webpackChunkName: 'loans' */ '@/pages/AMM')); -const Pools = React.lazy(() => import(/* webpackChunkName: 'loans' */ '@/pages/AMM/Pools')); -const Wallet = React.lazy(() => import(/* webpackChunkName: 'loans' */ '@/pages/Wallet')); +const Swap = React.lazy(() => import(/* webpackChunkName: 'amm' */ '@/pages/AMM')); +const Pools = React.lazy(() => import(/* webpackChunkName: 'amm/pools' */ '@/pages/AMM/Pools')); +const Wallet = React.lazy(() => import(/* webpackChunkName: 'wallet' */ '@/pages/Wallet')); const Actions = React.lazy(() => import(/* webpackChunkName: 'actions' */ '@/pages/Actions')); const NoMatch = React.lazy(() => import(/* webpackChunkName: 'no-match' */ '@/pages/NoMatch')); @@ -50,6 +51,7 @@ const App = (): JSX.Element => { const isLendingEnabled = useFeatureFlag(FeatureFlags.LENDING); const isAMMEnabled = useFeatureFlag(FeatureFlags.AMM); const isWalletEnabled = useFeatureFlag(FeatureFlags.WALLET); + const isEarnStrategiesEnabled = useFeatureFlag(FeatureFlags.EARN_STRATEGIES); // Loads the connection to the faucet - only for testnet purposes const loadFaucet = React.useCallback(async (): Promise => { @@ -212,6 +214,11 @@ const App = (): JSX.Element => { )} + {isEarnStrategiesEnabled && ( + + + + )} diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index 2959b1bd8d..e3a0d7164b 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -74,6 +74,7 @@ "issue": "Issue", "redeem": "Redeem", "nav_bridge": "Bridge", + "nav_earn_strategies": "Earn Strategies", "nav_transfer": "Transfer", "nav_lending": "Lending", "nav_swap": "Swap", diff --git a/src/pages/EarnStrategies/EarnStrategies.tsx b/src/pages/EarnStrategies/EarnStrategies.tsx new file mode 100644 index 0000000000..84ab2c0d2b --- /dev/null +++ b/src/pages/EarnStrategies/EarnStrategies.tsx @@ -0,0 +1,14 @@ +import { withErrorBoundary } from 'react-error-boundary'; + +import ErrorFallback from '@/legacy-components/ErrorFallback'; + +const EarnStrategies = (): JSX.Element => { + return

Earn Strategies

; +}; + +export default withErrorBoundary(EarnStrategies, { + FallbackComponent: ErrorFallback, + onReset: () => { + window.location.reload(); + } +}); diff --git a/src/pages/EarnStrategies/index.tsx b/src/pages/EarnStrategies/index.tsx new file mode 100644 index 0000000000..7a73a2c641 --- /dev/null +++ b/src/pages/EarnStrategies/index.tsx @@ -0,0 +1,3 @@ +import EarnStrategies from './EarnStrategies'; + +export default EarnStrategies; diff --git a/src/parts/Sidebar/SidebarContent/Navigation/index.tsx b/src/parts/Sidebar/SidebarContent/Navigation/index.tsx index 83581e5880..54dabf7446 100644 --- a/src/parts/Sidebar/SidebarContent/Navigation/index.tsx +++ b/src/parts/Sidebar/SidebarContent/Navigation/index.tsx @@ -70,6 +70,7 @@ const Navigation = ({ const isLendingEnabled = useFeatureFlag(FeatureFlags.LENDING); const isAMMEnabled = useFeatureFlag(FeatureFlags.AMM); const isWalletEnabled = useFeatureFlag(FeatureFlags.WALLET); + const isEarnStrategiesEnabled = useFeatureFlag(FeatureFlags.EARN_STRATEGIES); const NAVIGATION_ITEMS = React.useMemo( () => [ @@ -79,6 +80,12 @@ const Navigation = ({ icon: UserIcon, disabled: !isWalletEnabled }, + { + name: 'nav_earn_strategies', + link: PAGES.EARN_STRATEGIES, + icon: BanknotesIcon, + disabled: !isEarnStrategiesEnabled + }, { name: 'nav_bridge', link: PAGES.BRIDGE, @@ -193,7 +200,14 @@ const Navigation = ({ } } ], - [isWalletEnabled, isLendingEnabled, isAMMEnabled, selectedAccount?.address, vaultClientLoaded] + [ + isWalletEnabled, + isEarnStrategiesEnabled, + isLendingEnabled, + isAMMEnabled, + selectedAccount?.address, + vaultClientLoaded + ] ); return ( diff --git a/src/utils/constants/links.ts b/src/utils/constants/links.ts index 7c64f49388..fb189728c4 100644 --- a/src/utils/constants/links.ts +++ b/src/utils/constants/links.ts @@ -12,6 +12,7 @@ const URL_PARAMETERS = Object.freeze({ const PAGES = Object.freeze({ HOME: '/', BRIDGE: '/bridge', + EARN_STRATEGIES: '/earn-strategies', TRANSFER: '/transfer', TRANSACTIONS: '/transactions', TX: '/tx', diff --git a/src/utils/hooks/use-feature-flag.ts b/src/utils/hooks/use-feature-flag.ts index 9b5676a053..8f9254eab3 100644 --- a/src/utils/hooks/use-feature-flag.ts +++ b/src/utils/hooks/use-feature-flag.ts @@ -2,14 +2,16 @@ enum FeatureFlags { LENDING = 'lending', AMM = 'amm', WALLET = 'wallet', - BANXA = 'banxa' + BANXA = 'banxa', + EARN_STRATEGIES = 'earn-strategies' } const featureFlags: Record = { [FeatureFlags.LENDING]: process.env.REACT_APP_FEATURE_FLAG_LENDING, [FeatureFlags.AMM]: process.env.REACT_APP_FEATURE_FLAG_AMM, [FeatureFlags.WALLET]: process.env.REACT_APP_FEATURE_FLAG_WALLET, - [FeatureFlags.BANXA]: process.env.REACT_APP_FEATURE_FLAG_BANXA + [FeatureFlags.BANXA]: process.env.REACT_APP_FEATURE_FLAG_BANXA, + [FeatureFlags.EARN_STRATEGIES]: process.env.REACT_APP_FEATURE_FLAG_EARN_STRATEGIES }; const useFeatureFlag = (feature: FeatureFlags): boolean => featureFlags[feature] === 'enabled'; diff --git a/yarn.lock b/yarn.lock index c887f3b4fd..e7e055b8a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2075,10 +2075,10 @@ resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.5.0.tgz#483b44ba2c8b8d4391e1d2c863898d7dd0cc0296" integrity sha512-aaRnYxBb3MU2FNJf3Ut9RMTUqqU3as0aI1lQhgo2n9Fa67wRu14iOGqx93xB+uMNVfNwZ5B3y/Ndm7qZGuFeMQ== -"@heroicons/react@^2.0.0": - version "2.0.12" - resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.0.12.tgz#7e5a16c82512f89a30266dd36f8b8465b3e3e216" - integrity sha512-FZxKh3i9aKIDxyALTgIpSF2t6V6/eZfF5mRu41QlwkX3Oxzecdm1u6dpft6PQGxIBwO7TKYWaMAYYL8mp/EaOg== +"@heroicons/react@^2.0.18": + version "2.0.18" + resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.0.18.tgz#f80301907c243df03c7e9fd76c0286e95361f7c1" + integrity sha512-7TyMjRrZZMBPa+/5Y8lN0iyvUU/01PeMGX2+RE7cQWpEUIcb4QotzUObFkJDejj/HUH4qjP/eQ0gzzKs2f+6Yw== "@humanwhocodes/config-array@^0.5.0": version "0.5.0" 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 006/241] 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 }; From e1587986d4a720cf33c7b2833bc7fd1c53cc3586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Slan=C3=BD?= <47864599+peterslany@users.noreply.github.com> Date: Tue, 23 May 2023 12:07:37 +0200 Subject: [PATCH 007/241] chore: update monetary to latest 0.7.3 (#1214) * chore: update monetary to latest 0.7.3 * chore: update lib --- package.json | 4 ++-- yarn.lock | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 7e6cdc4d90..e00e21f043 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "@headlessui/react": "^1.1.1", "@heroicons/react": "^2.0.18", "@interlay/bridge": "^0.3.9", - "@interlay/interbtc-api": "2.2.2", - "@interlay/monetary-js": "0.7.2", + "@interlay/interbtc-api": "2.2.3", + "@interlay/monetary-js": "0.7.3", "@polkadot/api": "9.14.2", "@polkadot/extension-dapp": "0.44.1", "@polkadot/react-identicon": "^2.11.1", diff --git a/yarn.lock b/yarn.lock index e7e055b8a8..086d9aed21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2120,14 +2120,14 @@ dependencies: axios "^0.21.1" -"@interlay/interbtc-api@2.2.2": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-2.2.2.tgz#4803a80244abc9ef404dddefd265b9ece7ca859f" - integrity sha512-NuRjjIqeUPkt+aOTTmjMhx3TKAsL4id8kubiQsrAcyhsZnsnv/1bCXECzAWaZHVSi+XcxzfuoNLOxqrrx2+ISw== +"@interlay/interbtc-api@2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-2.2.3.tgz#570937750701bf359eb3301d4cb72d2cfa6bbcbc" + integrity sha512-W2ldNJkhG/87PWlUpLplSiBpEDlxgDQY6scZwAxZKILuVfTD1IhIlLCnB9BC8wrOG2ihFHhWK0sKTr92Am/VKw== dependencies: "@interlay/esplora-btc-api" "0.4.0" "@interlay/interbtc-types" "1.12.0" - "@interlay/monetary-js" "0.7.2" + "@interlay/monetary-js" "0.7.3" "@polkadot/api" "9.14.2" big.js "6.1.1" bitcoin-core "^3.0.0" @@ -2147,10 +2147,10 @@ resolved "https://registry.yarnpkg.com/@interlay/interbtc-types/-/interbtc-types-1.12.0.tgz#07dc8e15690292387124dbc2bbb7bf5bc8b68001" integrity sha512-ELJa2ftIbe8Ds2ejS7kO5HumN9EB5l2OBi3Qsy5iHJsHKq2HtXfFoKnW38HarM6hADrWG+e/yNGHSKJIJzEZuA== -"@interlay/monetary-js@0.7.2": - version "0.7.2" - resolved "https://registry.yarnpkg.com/@interlay/monetary-js/-/monetary-js-0.7.2.tgz#a54a315b60be12f5b1a9c31f0d71d5e8ee7ba174" - integrity sha512-SqNKJKBEstXuLnzqWi+ON+9ImrNVlKqZpXfiTtT3bcvxqh4jnVsGbYVe2I0FqDWtilCWq6/1RjKkpaSG2dvJhA== +"@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" From eee628c6d0810559c04751b7a5c51c56bd10d6da Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Tue, 23 May 2023 14:24:15 +0100 Subject: [PATCH 008/241] chore: bump lib and bridge (#1219) --- package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index e00e21f043..a07b2124a3 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "@craco/craco": "^6.1.1", "@headlessui/react": "^1.1.1", "@heroicons/react": "^2.0.18", - "@interlay/bridge": "^0.3.9", - "@interlay/interbtc-api": "2.2.3", + "@interlay/bridge": "^0.3.10", + "@interlay/interbtc-api": "2.2.4", "@interlay/monetary-js": "0.7.3", "@polkadot/api": "9.14.2", "@polkadot/extension-dapp": "0.44.1", diff --git a/yarn.lock b/yarn.lock index 086d9aed21..269fc4fdba 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.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@interlay/bridge/-/bridge-0.3.9.tgz#fc39c64708eab2f55cb0bbb970f2f0e72583cb37" - integrity sha512-QCeTux1f3LwLJ/dcfHmOTOuF8ocfpo9WDNV7Z1GWTHX/I8lspidj4xh8c/g2+jNZnHMiINXCSvHGPPr05lTnQg== +"@interlay/bridge@^0.3.10": + version "0.3.10" + resolved "https://registry.yarnpkg.com/@interlay/bridge/-/bridge-0.3.10.tgz#d686b83af91f99a1f62b72cfbb8d127c60557200" + integrity sha512-8yDfhvDNIeaW07Kbfvjp37YUNpsKsSXooT5JVfC1AdQs9ej51fbuhINUK31JNncI0xCWhTBu72Wq0ZFIQNm8RA== dependencies: "@acala-network/api" "4.1.8-13" "@acala-network/sdk" "4.1.8-13" @@ -2120,10 +2120,10 @@ dependencies: axios "^0.21.1" -"@interlay/interbtc-api@2.2.3": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-2.2.3.tgz#570937750701bf359eb3301d4cb72d2cfa6bbcbc" - integrity sha512-W2ldNJkhG/87PWlUpLplSiBpEDlxgDQY6scZwAxZKILuVfTD1IhIlLCnB9BC8wrOG2ihFHhWK0sKTr92Am/VKw== +"@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== dependencies: "@interlay/esplora-btc-api" "0.4.0" "@interlay/interbtc-types" "1.12.0" From 63b059aa5159581e2c2deb219f935b9de5fe14b9 Mon Sep 17 00:00:00 2001 From: Thomas Jeatt Date: Wed, 24 May 2023 09:28:43 +0100 Subject: [PATCH 009/241] chore: release v2.32.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a07b2124a3..d650a2e77e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interbtc-ui", - "version": "2.32.0", + "version": "2.32.1", "private": true, "dependencies": { "@craco/craco": "^6.1.1", From 65d0a009915d1e2bd2b785bccf72ce4407c255d1 Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Wed, 24 May 2023 11:25:44 +0100 Subject: [PATCH 010/241] fix: add missing icons and remove erroring RPC (#1222) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> --- .../components/ChainIcon/ChainIcon.tsx | 19 ++- .../components/ChainIcon/icons/Acala.tsx | 147 ++++++++++++++++++ .../components/ChainIcon/icons/Astar.tsx | 58 +++++++ .../components/ChainIcon/icons/Parallel.tsx | 47 ++++++ .../components/ChainIcon/icons/index.ts | 3 + src/utils/hooks/api/xcm/xcm-endpoints.ts | 7 +- 6 files changed, 274 insertions(+), 7 deletions(-) create mode 100644 src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Acala.tsx create mode 100644 src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Astar.tsx create mode 100644 src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Parallel.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/ChainIcon.tsx b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/ChainIcon.tsx index 339e83d336..dd99e9e509 100644 --- a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/ChainIcon.tsx +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/ChainIcon.tsx @@ -3,11 +3,27 @@ import { forwardRef, ForwardRefExoticComponent, RefAttributes } from 'react'; import { IconProps } from '@/component-library/Icon'; import { StyledFallbackIcon } from './ChainIcon.style'; -import { BIFROST, HEIKO, HYDRA, INTERLAY, KARURA, KINTSUGI, KUSAMA, POLKADOT, STATEMINE, STATEMINT } from './icons'; +import { + ACALA, + ASTAR, + BIFROST, + HEIKO, + HYDRA, + INTERLAY, + KARURA, + KINTSUGI, + KUSAMA, + PARALLEL, + POLKADOT, + STATEMINE, + STATEMINT +} from './icons'; type ChainComponent = ForwardRefExoticComponent>; const chainsIcon: Record = { + ACALA, + ASTAR, BIFROST, HEIKO, HYDRA, @@ -15,6 +31,7 @@ const chainsIcon: Record = { KARURA, KINTSUGI, KUSAMA, + PARALLEL, POLKADOT, STATEMINE, STATEMINT diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Acala.tsx b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Acala.tsx new file mode 100644 index 0000000000..137ffa2a35 --- /dev/null +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Acala.tsx @@ -0,0 +1,147 @@ +import { forwardRef } from 'react'; + +import { Icon, IconProps } from '@/component-library/Icon'; + +const ACALA = forwardRef((props, ref) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)); + +ACALA.displayName = 'ACALA'; + +export { ACALA }; diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Astar.tsx b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Astar.tsx new file mode 100644 index 0000000000..61601ee25d --- /dev/null +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Astar.tsx @@ -0,0 +1,58 @@ +import { forwardRef } from 'react'; + +import { Icon, IconProps } from '@/component-library/Icon'; + +const ASTAR = forwardRef((props, ref) => ( + + ASTAR + + + + + + + + + + + + + + + + + + + +)); + +ASTAR.displayName = 'ASTAR'; + +export { ASTAR }; diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Parallel.tsx b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Parallel.tsx new file mode 100644 index 0000000000..b6fc644b49 --- /dev/null +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Parallel.tsx @@ -0,0 +1,47 @@ +import { forwardRef } from 'react'; + +import { Icon, IconProps } from '@/component-library/Icon'; + +const PARALLEL = forwardRef((props, ref) => ( + + PARALLEL + + + + + + + + + + + + + + + + + +)); + +PARALLEL.displayName = 'PARALLEL'; + +export { PARALLEL }; diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/index.ts b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/index.ts index e5d13a7014..d3c471eb7a 100644 --- a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/index.ts +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/index.ts @@ -1,3 +1,5 @@ +export { ACALA } from './Acala'; +export { ASTAR } from './Astar'; export { BIFROST } from './Bifrost'; export { HEIKO } from './Heiko'; export { HYDRA } from './Hydra'; @@ -5,6 +7,7 @@ export { INTERLAY } from './Interlay'; export { KARURA } from './Karura'; export { KINTSUGI } from './Kintsugi'; export { KUSAMA } from './Kusama'; +export { PARALLEL } from './Parallel'; export { POLKADOT } from './Polkadot'; export { STATEMINE } from './Statemine'; export { STATEMINT } from './Statemint'; diff --git a/src/utils/hooks/api/xcm/xcm-endpoints.ts b/src/utils/hooks/api/xcm/xcm-endpoints.ts index 6b8407c0df..fe751d615b 100644 --- a/src/utils/hooks/api/xcm/xcm-endpoints.ts +++ b/src/utils/hooks/api/xcm/xcm-endpoints.ts @@ -3,12 +3,7 @@ import { ChainName } from '@interlay/bridge'; type XCMEndpointsRecord = Record; const XCMEndpoints: XCMEndpointsRecord = { - acala: [ - 'wss://acala-rpc-0.aca-api.network', - 'wss://acala-rpc-1.aca-api.network', - 'wss://acala-rpc-3.aca-api.network/ws', - 'wss://acala-rpc.dwellir.com' - ], + acala: ['wss://acala-rpc-1.aca-api.network', 'wss://acala-rpc-3.aca-api.network/ws', 'wss://acala-rpc.dwellir.com'], astar: ['wss://rpc.astar.network', 'wss://astar-rpc.dwellir.com'], bifrost: ['wss://bifrost-rpc.dwellir.com'], heiko: ['wss://heiko-rpc.parallel.fi'], From 250f7225f2e7912c90b5eeb59fd36f6b3d7aa636 Mon Sep 17 00:00:00 2001 From: Thomas Jeatt Date: Wed, 24 May 2023 11:34:07 +0100 Subject: [PATCH 011/241] chore: release v2.32.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d650a2e77e..389305f0a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interbtc-ui", - "version": "2.32.1", + "version": "2.32.2", "private": true, "dependencies": { "@craco/craco": "^6.1.1", From c000243e5094b36bc2a2b9c758706be9929de1a9 Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Wed, 24 May 2023 13:24:09 +0100 Subject: [PATCH 012/241] fix: compare input configs with method not operator (#1225) --- src/utils/hooks/api/xcm/use-xcm-bridge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/hooks/api/xcm/use-xcm-bridge.ts b/src/utils/hooks/api/xcm/use-xcm-bridge.ts index 32a6845d77..491a8931d3 100644 --- a/src/utils/hooks/api/xcm/use-xcm-bridge.ts +++ b/src/utils/hooks/api/xcm/use-xcm-bridge.ts @@ -111,7 +111,7 @@ const useXCMBridge = (): UseXCMBridge => { const minInputToBig = Big(inputConfig.minInput.toString()); // Never show less than zero - const transferableBalance = inputConfig.maxInput < inputConfig.minInput ? 0 : maxInputToBig; + const transferableBalance = inputConfig.maxInput.isLessThan(inputConfig.minInput) ? 0 : maxInputToBig; const currency = XCMBridge.findAdapter(from).getToken(token, from); const nativeToken = originAdapter.getNativeToken(); From 17251a3b5c76ae716da4c7f1e0de61bb15c484e8 Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Wed, 24 May 2023 14:46:32 +0100 Subject: [PATCH 013/241] refactor: reset selected account on account change (#1226) --- .../CrossChainTransferForm/CrossChainTransferForm.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx b/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx index 85b5cd0eb1..1e7a864185 100644 --- a/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx +++ b/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx @@ -212,6 +212,14 @@ const CrossChainTransferForm = (): JSX.Element => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [accountId, destinationChains]); + // TODO: When we refactor account select this should be handled there so + // that it's consitent across the application + useEffect(() => { + if (!accountId) return; + form.setFieldValue(CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD, accountId?.toString()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [accountId]); + if (!originatingChains || !destinationChains || !transferableTokens.length) { return ( From 0d81dc58af298232cb0b66924d08ee134174bfb8 Mon Sep 17 00:00:00 2001 From: Thomas Jeatt Date: Wed, 24 May 2023 15:59:14 +0100 Subject: [PATCH 014/241] chore: release v2.32.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 389305f0a0..312ec05883 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interbtc-ui", - "version": "2.32.2", + "version": "2.32.3", "private": true, "dependencies": { "@craco/craco": "^6.1.1", From 63487d09bd6bf3b55ae58b7cdc3da3d2657d4011 Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Thu, 25 May 2023 10:49:47 +0100 Subject: [PATCH 015/241] feature: add geoblock feature flag (#1230) --- src/components/Geoblock/Geoblock.tsx | 2 +- src/utils/hooks/use-feature-flag.ts | 6 ++++-- src/utils/hooks/use-geoblocking.ts | 7 ++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/Geoblock/Geoblock.tsx b/src/components/Geoblock/Geoblock.tsx index 43493562d3..0a308d5619 100644 --- a/src/components/Geoblock/Geoblock.tsx +++ b/src/components/Geoblock/Geoblock.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import { ReactNode } from 'react'; import { useGeoblocking } from '@/utils/hooks/use-geoblocking'; diff --git a/src/utils/hooks/use-feature-flag.ts b/src/utils/hooks/use-feature-flag.ts index 8f9254eab3..917bd3b3c8 100644 --- a/src/utils/hooks/use-feature-flag.ts +++ b/src/utils/hooks/use-feature-flag.ts @@ -3,7 +3,8 @@ enum FeatureFlags { AMM = 'amm', WALLET = 'wallet', BANXA = 'banxa', - EARN_STRATEGIES = 'earn-strategies' + EARN_STRATEGIES = 'earn-strategies', + GEOBLOCK = 'geoblock' } const featureFlags: Record = { @@ -11,7 +12,8 @@ const featureFlags: Record = { [FeatureFlags.AMM]: process.env.REACT_APP_FEATURE_FLAG_AMM, [FeatureFlags.WALLET]: process.env.REACT_APP_FEATURE_FLAG_WALLET, [FeatureFlags.BANXA]: process.env.REACT_APP_FEATURE_FLAG_BANXA, - [FeatureFlags.EARN_STRATEGIES]: process.env.REACT_APP_FEATURE_FLAG_EARN_STRATEGIES + [FeatureFlags.EARN_STRATEGIES]: process.env.REACT_APP_FEATURE_FLAG_EARN_STRATEGIES, + [FeatureFlags.GEOBLOCK]: process.env.REACT_APP_FEATURE_FLAG_GEOBLOCK }; const useFeatureFlag = (feature: FeatureFlags): boolean => featureFlags[feature] === 'enabled'; diff --git a/src/utils/hooks/use-geoblocking.ts b/src/utils/hooks/use-geoblocking.ts index 6ca37d3a02..09805c8398 100644 --- a/src/utils/hooks/use-geoblocking.ts +++ b/src/utils/hooks/use-geoblocking.ts @@ -1,9 +1,14 @@ import { useEffect } from 'react'; import { GEOBLOCK_API_ENDPOINT, GEOBLOCK_REDIRECTION_LINK } from '@/config/links'; +import { FeatureFlags, useFeatureFlag } from '@/utils/hooks/use-feature-flag'; const useGeoblocking = (): void => { + const isGeoblockEnabled = useFeatureFlag(FeatureFlags.GEOBLOCK); + useEffect(() => { + if (!isGeoblockEnabled) return; + const checkCountry = async () => { try { const response = await fetch(GEOBLOCK_API_ENDPOINT); @@ -16,7 +21,7 @@ const useGeoblocking = (): void => { } }; checkCountry(); - }, []); + }, [isGeoblockEnabled]); }; export { useGeoblocking }; From d6db39b3ab172082e251cac0b6310a8cdb38bb7c Mon Sep 17 00:00:00 2001 From: Thomas Jeatt Date: Thu, 25 May 2023 11:09:49 +0100 Subject: [PATCH 016/241] chore: release v2.32.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 312ec05883..735d72e184 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interbtc-ui", - "version": "2.32.3", + "version": "2.32.4", "private": true, "dependencies": { "@craco/craco": "^6.1.1", From 5f0dcb8525c12e415381ac7b3737f11490af049e Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Fri, 26 May 2023 10:49:12 +0100 Subject: [PATCH 017/241] chore: bump bridge (#1233) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 735d72e184..271ee73fe1 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@craco/craco": "^6.1.1", "@headlessui/react": "^1.1.1", "@heroicons/react": "^2.0.18", - "@interlay/bridge": "^0.3.10", + "@interlay/bridge": "^0.3.11", "@interlay/interbtc-api": "2.2.4", "@interlay/monetary-js": "0.7.3", "@polkadot/api": "9.14.2", diff --git a/yarn.lock b/yarn.lock index 269fc4fdba..77273acb3a 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.10": - version "0.3.10" - resolved "https://registry.yarnpkg.com/@interlay/bridge/-/bridge-0.3.10.tgz#d686b83af91f99a1f62b72cfbb8d127c60557200" - integrity sha512-8yDfhvDNIeaW07Kbfvjp37YUNpsKsSXooT5JVfC1AdQs9ej51fbuhINUK31JNncI0xCWhTBu72Wq0ZFIQNm8RA== +"@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== dependencies: "@acala-network/api" "4.1.8-13" "@acala-network/sdk" "4.1.8-13" From 891de67e0e61d386299d11ecc476e89843a56673 Mon Sep 17 00:00:00 2001 From: Thomas Jeatt Date: Fri, 26 May 2023 10:51:33 +0100 Subject: [PATCH 018/241] chore: release v2.32.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 271ee73fe1..3b6ae00edf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interbtc-ui", - "version": "2.32.4", + "version": "2.32.5", "private": true, "dependencies": { "@craco/craco": "^6.1.1", From 918e944ed434716071a74e80e73bc6a43e9b9a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Slan=C3=BD?= <47864599+peterslany@users.noreply.github.com> Date: Tue, 30 May 2023 16:23:02 +0200 Subject: [PATCH 019/241] 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 --- src/assets/locales/en/translation.json | 7 +- .../ReceivableAssets/index.tsx} | 14 +-- src/components/index.tsx | 1 + src/lib/form/schemas/earn-strategy.ts | 21 ++++ src/lib/form/schemas/index.ts | 1 + .../components/WithdrawForm/WithdrawForm.tsx | 5 +- .../EarnStrategies/EarnStrategies.style.tsx | 13 +++ src/pages/EarnStrategies/EarnStrategies.tsx | 9 +- .../EarnStrategyDepositForm.tsx | 66 +++++++++++ .../EarnStrategyDepositForm/index.ts | 1 + .../EarnStrategyForm.style.tsx | 34 ++++++ .../EarnStrategyForm/EarnStrategyForm.tsx | 61 ++++++++++ .../EarnStrategyForm/EarnStrategyFormFees.tsx | 35 ++++++ .../EarnStrategyWithdrawalForm.tsx | 104 ++++++++++++++++++ .../EarnStrategyWithdrawalForm/index.ts | 1 + .../components/EarnStrategyForm/index.ts | 1 + src/pages/EarnStrategies/components/index.ts | 1 + src/pages/EarnStrategies/types/form.ts | 18 +++ yarn.lock | 9 ++ 19 files changed, 390 insertions(+), 12 deletions(-) rename src/{pages/AMM/Pools/components/WithdrawForm/WithdrawAssets.tsx => components/ReceivableAssets/index.tsx} (82%) create mode 100644 src/lib/form/schemas/earn-strategy.ts create mode 100644 src/pages/EarnStrategies/EarnStrategies.style.tsx create mode 100644 src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/EarnStrategyDepositForm.tsx create mode 100644 src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/index.ts create mode 100644 src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.style.tsx create mode 100644 src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.tsx create mode 100644 src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyFormFees.tsx create mode 100644 src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/EarnStrategyWithdrawalForm.tsx create mode 100644 src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/index.ts create mode 100644 src/pages/EarnStrategies/components/EarnStrategyForm/index.ts create mode 100644 src/pages/EarnStrategies/components/index.ts create mode 100644 src/pages/EarnStrategies/types/form.ts diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index e3a0d7164b..defab36d41 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -20,6 +20,7 @@ "reimbursed": "Reimbursed", "online": "Online", "offline": "Offline", + "available": "Available", "unavailable": "Unavailable", "ok": "OK", "pending": "Pending", @@ -154,6 +155,7 @@ "unlocks": "Unlocks", "staked": "Staked", "sign_t&cs": "Sign T&Cs", + "receivable_assets": "Receivable Assets", "redeem_page": { "maximum_in_single_request": "Max redeemable in single request", "redeem": "Redeem", @@ -586,7 +588,6 @@ "pool_name": "Pool Name", "add_liquidity": "Add Liquidity", "remove_liquidity": "Remove Liquidity", - "receivable_assets": "Receivable Assets", "initial_rate_warning": "Note: You are setting the initial exchange rate of this pool. Make sure it reflects the exchange rate on other markets, please." }, "swap": "Swap", @@ -631,5 +632,9 @@ "total_governance_locked": "Total {{token}} Locked", "available_to_stake": "Available to stake", "voting_power_governance": "Voting Power {{token}}" + }, + "earn_strategy": { + "withdraw_rewards_in_wrapped": "Withdraw rewards in {{wrappedCurrencySymbol}}:", + "update_position": "Update position" } } diff --git a/src/pages/AMM/Pools/components/WithdrawForm/WithdrawAssets.tsx b/src/components/ReceivableAssets/index.tsx similarity index 82% rename from src/pages/AMM/Pools/components/WithdrawForm/WithdrawAssets.tsx rename to src/components/ReceivableAssets/index.tsx index 0558749d28..9c4fecef5f 100644 --- a/src/pages/AMM/Pools/components/WithdrawForm/WithdrawAssets.tsx +++ b/src/components/ReceivableAssets/index.tsx @@ -7,18 +7,18 @@ import { CoinIcon, Dd, Dl, DlGroup, Dt, Flex, P } from '@/component-library'; import { getTokenPrice } from '@/utils/helpers/prices'; import { Prices } from '@/utils/hooks/api/use-get-prices'; -type WithdrawAssetsProps = { - pooledAmounts: MonetaryAmount[]; +type ReceivableAssetsProps = { + assetAmounts: MonetaryAmount[]; prices?: Prices; }; -const WithdrawAssets = ({ pooledAmounts, prices }: WithdrawAssetsProps): JSX.Element => { +const ReceivableAssets = ({ assetAmounts: pooledAmounts, prices }: ReceivableAssetsProps): JSX.Element => { const { t } = useTranslation(); return (

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

{pooledAmounts.map((amount) => { @@ -50,7 +50,7 @@ const WithdrawAssets = ({ pooledAmounts, prices }: WithdrawAssetsProps): JSX.Ele ); }; -WithdrawAssets.displayName = 'WithdrawAssets'; +ReceivableAssets.displayName = 'ReceivableAssets'; -export { WithdrawAssets }; -export type { WithdrawAssetsProps }; +export { ReceivableAssets }; +export type { ReceivableAssetsProps }; diff --git a/src/components/index.tsx b/src/components/index.tsx index 5149bf3220..83fc0ca6aa 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -12,3 +12,4 @@ export type { LoanPositionsTableProps } from './LoanPositionsTable'; export { LoanPositionsTable } from './LoanPositionsTable'; export type { PoolsTableProps } from './PoolsTable'; export { PoolsTable } from './PoolsTable'; +export { ReceivableAssets } from './ReceivableAssets'; diff --git a/src/lib/form/schemas/earn-strategy.ts b/src/lib/form/schemas/earn-strategy.ts new file mode 100644 index 0000000000..75dd1f1b15 --- /dev/null +++ b/src/lib/form/schemas/earn-strategy.ts @@ -0,0 +1,21 @@ +import { EarnStrategyFormType } from '@/pages/EarnStrategies/types/form'; + +import yup, { MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; + +type EarnStrategyValidationParams = MaxAmountValidationParams & MinAmountValidationParams; + +const earnStrategySchema = ( + earnStrategyFormType: EarnStrategyFormType, + params: EarnStrategyValidationParams +): yup.ObjectSchema => { + return yup.object().shape({ + [earnStrategyFormType]: yup + .string() + .requiredAmount(earnStrategyFormType) + .maxAmount(params) + .minAmount(params, earnStrategyFormType) + }); +}; + +export { earnStrategySchema }; +export type { EarnStrategyValidationParams }; diff --git a/src/lib/form/schemas/index.ts b/src/lib/form/schemas/index.ts index a792ef0fbe..87a172bab2 100644 --- a/src/lib/form/schemas/index.ts +++ b/src/lib/form/schemas/index.ts @@ -5,6 +5,7 @@ export type { WithdrawLiquidityPoolValidationParams } from './amm'; export { depositLiquidityPoolSchema, WITHDRAW_LIQUIDITY_POOL_FIELD, withdrawLiquidityPoolSchema } from './amm'; +export { earnStrategySchema } from './earn-strategy'; export type { LoanFormData, LoanValidationParams } from './loans'; export { loanSchema } from './loans'; export type { SwapFormData, SwapValidationParams } from './swap'; diff --git a/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx b/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx index 4f2ea60b55..2d74a356af 100644 --- a/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx +++ b/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx @@ -10,7 +10,7 @@ import { newSafeMonetaryAmount } from '@/common/utils/utils'; import { Dd, DlGroup, Dt, Flex, TokenInput } from '@/component-library'; -import { AuthCTA } from '@/components'; +import { AuthCTA, ReceivableAssets } from '@/components'; import { GOVERNANCE_TOKEN, TRANSACTION_FEE_AMOUNT } from '@/config/relay-chains'; import { isFormDisabled, useForm, WITHDRAW_LIQUIDITY_POOL_FIELD } from '@/lib/form'; import { WithdrawLiquidityPoolFormData, withdrawLiquidityPoolSchema } from '@/lib/form/schemas'; @@ -23,7 +23,6 @@ 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 WithdrawFormProps = { @@ -126,7 +125,7 @@ const WithdrawForm = ({ pool, slippageModalRef, onWithdraw }: WithdrawFormProps) {...form.getFieldProps(WITHDRAW_LIQUIDITY_POOL_FIELD)} /> - +
diff --git a/src/pages/EarnStrategies/EarnStrategies.style.tsx b/src/pages/EarnStrategies/EarnStrategies.style.tsx new file mode 100644 index 0000000000..8bc1b94263 --- /dev/null +++ b/src/pages/EarnStrategies/EarnStrategies.style.tsx @@ -0,0 +1,13 @@ +import styled from 'styled-components'; + +import { theme } from '@/component-library'; +const StyledEarnStrategiesLayout = styled.div` + display: grid; + gap: ${theme.spacing.spacing6}; + @media (min-width: 80em) { + grid-template-columns: 1fr 1fr; + } + padding: ${theme.spacing.spacing6}; +`; + +export { StyledEarnStrategiesLayout }; diff --git a/src/pages/EarnStrategies/EarnStrategies.tsx b/src/pages/EarnStrategies/EarnStrategies.tsx index 84ab2c0d2b..bd1eac8fdc 100644 --- a/src/pages/EarnStrategies/EarnStrategies.tsx +++ b/src/pages/EarnStrategies/EarnStrategies.tsx @@ -2,8 +2,15 @@ import { withErrorBoundary } from 'react-error-boundary'; import ErrorFallback from '@/legacy-components/ErrorFallback'; +import { EarnStrategyForm } from './components/EarnStrategyForm'; +import { StyledEarnStrategiesLayout } from './EarnStrategies.style'; + const EarnStrategies = (): JSX.Element => { - return

Earn Strategies

; + return ( + + + + ); }; export default withErrorBoundary(EarnStrategies, { diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/EarnStrategyDepositForm.tsx b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/EarnStrategyDepositForm.tsx new file mode 100644 index 0000000000..b0939fd093 --- /dev/null +++ b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/EarnStrategyDepositForm.tsx @@ -0,0 +1,66 @@ +import { newMonetaryAmount } from '@interlay/interbtc-api'; +import { mergeProps } from '@react-aria/utils'; +import { useTranslation } from 'react-i18next'; + +import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/common/utils/utils'; +import { TokenInput } from '@/component-library'; +import { AuthCTA } from '@/components'; +import { TRANSACTION_FEE_AMOUNT, WRAPPED_TOKEN, WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains'; +import { earnStrategySchema, isFormDisabled, useForm } from '@/lib/form'; +import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; +import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { useTransaction } from '@/utils/hooks/transaction'; + +import { EarnStrategyDepositFormData } from '../../../types/form'; +import { EarnStrategyFormBaseProps } from '../EarnStrategyForm'; +import { StyledEarnStrategyFormContent } from '../EarnStrategyForm.style'; +import { EarnStrategyFormFees } from '../EarnStrategyFormFees'; + +const EarnStrategyDepositForm = ({ riskVariant, hasActiveStrategy }: EarnStrategyFormBaseProps): JSX.Element => { + const { getAvailableBalance } = useGetBalances(); + const prices = useGetPrices(); + const { t } = useTranslation(); + // TODO: add transaction + const transaction = useTransaction(); + + const handleSubmit = (data: EarnStrategyDepositFormData) => { + // TODO: Execute transaction with params + // transaction.execute(); + console.log(`transaction should be executed with parameters: ${data}, ${riskVariant}`); + }; + + const minAmount = newMonetaryAmount(1, WRAPPED_TOKEN); + const maxDepositAmount = getAvailableBalance(WRAPPED_TOKEN_SYMBOL) || newMonetaryAmount(0, WRAPPED_TOKEN); + + const form = useForm({ + initialValues: { deposit: '' }, + validationSchema: earnStrategySchema('deposit', { maxAmount: maxDepositAmount, minAmount }), + onSubmit: handleSubmit + }); + + const inputMonetaryAmount = newSafeMonetaryAmount(form.values['deposit'] || 0, WRAPPED_TOKEN, true); + const inputUSDValue = convertMonetaryAmountToValueInUSD(inputMonetaryAmount, prices?.[WRAPPED_TOKEN_SYMBOL].usd); + const isSubmitButtonDisabled = isFormDisabled(form); + + return ( +
+ + + + + {hasActiveStrategy ? t('earn_strategy.update_position') : t('deposit')} + + +
+ ); +}; + +export { EarnStrategyDepositForm }; diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/index.ts b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/index.ts new file mode 100644 index 0000000000..aaedb7d4bb --- /dev/null +++ b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/index.ts @@ -0,0 +1 @@ +export { EarnStrategyDepositForm } from './EarnStrategyDepositForm'; diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.style.tsx b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.style.tsx new file mode 100644 index 0000000000..4179c1e7a4 --- /dev/null +++ b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.style.tsx @@ -0,0 +1,34 @@ +import styled from 'styled-components'; + +import { Dl, Flex, theme } from '@/component-library'; + +const StyledEarnStrategyForm = styled(Flex)` + margin-top: ${theme.spacing.spacing8}; + background: ${theme.colors.bgPrimary}; + padding: ${theme.spacing.spacing6}; + border-radius: ${theme.rounded.md}; +`; + +const StyledDl = styled(Dl)` + background-color: ${theme.card.bg.secondary}; + padding: ${theme.spacing.spacing4}; + font-size: ${theme.text.xs}; + border-radius: ${theme.rounded.rg}; +`; + +const StyledEarnStrategyFormContent = styled(Flex)` + margin-top: ${theme.spacing.spacing8}; + flex-direction: column; + gap: ${theme.spacing.spacing8}; +`; + +const StyledSwitchLabel = styled('label')` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + font-weight: ${theme.fontWeight.bold}; +`; + +export { StyledDl, StyledEarnStrategyForm, StyledEarnStrategyFormContent, StyledSwitchLabel }; diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.tsx b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.tsx new file mode 100644 index 0000000000..7f4ba377d5 --- /dev/null +++ b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.tsx @@ -0,0 +1,61 @@ +import { newMonetaryAmount } from '@interlay/interbtc-api'; + +import { Tabs, TabsItem } from '@/component-library'; +import { WRAPPED_TOKEN } from '@/config/relay-chains'; + +import { EarnStrategyFormType, EarnStrategyRiskVariant } from '../../types/form'; +import { EarnStrategyDepositForm } from './EarnStrategyDepositForm'; +import { StyledEarnStrategyForm } from './EarnStrategyForm.style'; +import { EarnStrategyWithdrawalForm } from './EarnStrategyWithdrawalForm'; + +interface EarnStrategyFormProps { + riskVariant: EarnStrategyRiskVariant; +} + +interface EarnStrategyFormBaseProps extends EarnStrategyFormProps { + hasActiveStrategy: boolean | undefined; +} + +type TabData = { type: EarnStrategyFormType; title: string }; + +const tabs: Array = [ + { + type: 'deposit', + title: 'Deposit' + }, + { + type: 'withdraw', + title: 'Withdraw' + } +]; + +const EarnStrategyForm = ({ riskVariant }: EarnStrategyFormProps): JSX.Element => { + // TODO: replace with actually withdrawable amount once we know how to get that information, + // for now it's statically set for display purposes + const maxWithdrawableAmount = newMonetaryAmount(1.337, WRAPPED_TOKEN, true); + const hasActiveStrategy = maxWithdrawableAmount && !maxWithdrawableAmount.isZero(); + + return ( + + + {tabs.map(({ type, title }) => ( + + {type === 'deposit' ? ( + + ) : ( + + )} + + ))} + + + ); +}; + +export { EarnStrategyForm }; +export type { EarnStrategyFormBaseProps, EarnStrategyFormProps }; diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyFormFees.tsx b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyFormFees.tsx new file mode 100644 index 0000000000..007867f302 --- /dev/null +++ b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyFormFees.tsx @@ -0,0 +1,35 @@ +import { GovernanceCurrency } from '@interlay/interbtc-api'; +import { MonetaryAmount } from '@interlay/monetary-js'; +import { useTranslation } from 'react-i18next'; + +import { displayMonetaryAmountInUSDFormat } from '@/common/utils/utils'; +import { Dd, DlGroup, Dt } from '@/component-library'; +import { getTokenPrice } from '@/utils/helpers/prices'; +import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; + +import { StyledDl } from './EarnStrategyForm.style'; + +interface EarnStrategyFormFeesProps { + amount: MonetaryAmount; +} + +const EarnStrategyFormFees = ({ amount }: EarnStrategyFormFeesProps): JSX.Element => { + const prices = useGetPrices(); + const { t } = useTranslation(); + + return ( + + +
+ {t('fees')} +
+
+ {amount.toHuman()} {amount.currency.ticker} ( + {displayMonetaryAmountInUSDFormat(amount, getTokenPrice(prices, amount.currency.ticker)?.usd)}) +
+
+
+ ); +}; + +export { EarnStrategyFormFees }; diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/EarnStrategyWithdrawalForm.tsx b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/EarnStrategyWithdrawalForm.tsx new file mode 100644 index 0000000000..606fec950e --- /dev/null +++ b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/EarnStrategyWithdrawalForm.tsx @@ -0,0 +1,104 @@ +import { CurrencyExt, newMonetaryAmount, WrappedCurrency } from '@interlay/interbtc-api'; +import { MonetaryAmount } from '@interlay/monetary-js'; +import { mergeProps } from '@react-aria/utils'; +import { useTranslation } from 'react-i18next'; + +import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/common/utils/utils'; +import { Switch, TokenInput } from '@/component-library'; +import { AuthCTA, ReceivableAssets } from '@/components'; +import { + RELAY_CHAIN_NATIVE_TOKEN, + TRANSACTION_FEE_AMOUNT, + WRAPPED_TOKEN, + WRAPPED_TOKEN_SYMBOL +} from '@/config/relay-chains'; +import { earnStrategySchema, isFormDisabled, useForm } from '@/lib/form'; +import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { useTransaction } from '@/utils/hooks/transaction'; + +import { EarnStrategyWithdrawalFormData } from '../../../types/form'; +import { EarnStrategyFormBaseProps } from '../EarnStrategyForm'; +import { StyledEarnStrategyFormContent, StyledSwitchLabel } from '../EarnStrategyForm.style'; +import { EarnStrategyFormFees } from '../EarnStrategyFormFees'; + +interface EarnStrategyWithdrawalFormProps extends EarnStrategyFormBaseProps { + maxWithdrawableAmount: MonetaryAmount | undefined; +} + +const calculateReceivableAssets = ( + amountToWithdraw: MonetaryAmount, + withdrawInWrapped: boolean +): Array> => { + if (withdrawInWrapped) { + return [amountToWithdraw]; + } + // TODO: do some magic calculation to get the receivable assets based on input amount here, + // or better move this computation to earn-strategy hook + const mockedReceivableAssets = [ + amountToWithdraw.div(1.2), + newMonetaryAmount(amountToWithdraw.toBig().mul(213.2), RELAY_CHAIN_NATIVE_TOKEN, true) + ]; + + return mockedReceivableAssets; +}; + +const EarnStrategyWithdrawalForm = ({ + riskVariant, + hasActiveStrategy, + maxWithdrawableAmount +}: EarnStrategyWithdrawalFormProps): JSX.Element => { + const { t } = useTranslation(); + const prices = useGetPrices(); + // TODO: add transaction + const transaction = useTransaction(); + + const handleSubmit = (data: EarnStrategyWithdrawalFormData) => { + // TODO: Execute transaction with params + // transaction.execute() + console.log(data, riskVariant); + }; + + const minAmount = newMonetaryAmount(1, WRAPPED_TOKEN); + + const form = useForm({ + initialValues: { withdraw: '', withdrawAsWrapped: true }, + validationSchema: earnStrategySchema('withdraw', { + maxAmount: maxWithdrawableAmount || newMonetaryAmount(0, WRAPPED_TOKEN), + minAmount + }), + onSubmit: handleSubmit + }); + + const inputMonetaryAmount = newSafeMonetaryAmount(form.values['withdraw'] || 0, WRAPPED_TOKEN, true); + const inputUSDValue = convertMonetaryAmountToValueInUSD(inputMonetaryAmount, prices?.[WRAPPED_TOKEN_SYMBOL].usd); + const receivableAssets = calculateReceivableAssets(inputMonetaryAmount, !!form.values['withdrawAsWrapped']); + const isSubmitButtonDisabled = isFormDisabled(form); + + return ( +
+ + + + {t('earn_strategy.withdraw_rewards_in_wrapped', { wrappedCurrencySymbol: WRAPPED_TOKEN_SYMBOL })}{' '} + + + + + + {hasActiveStrategy ? t('earn_strategy.update_position') : t('withdraw')} + + +
+ ); +}; + +export { EarnStrategyWithdrawalForm }; diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/index.ts b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/index.ts new file mode 100644 index 0000000000..aa5c13a062 --- /dev/null +++ b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/index.ts @@ -0,0 +1 @@ +export { EarnStrategyWithdrawalForm } from './EarnStrategyWithdrawalForm'; diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/index.ts b/src/pages/EarnStrategies/components/EarnStrategyForm/index.ts new file mode 100644 index 0000000000..dd8d107bb2 --- /dev/null +++ b/src/pages/EarnStrategies/components/EarnStrategyForm/index.ts @@ -0,0 +1 @@ +export { EarnStrategyForm } from './EarnStrategyForm'; diff --git a/src/pages/EarnStrategies/components/index.ts b/src/pages/EarnStrategies/components/index.ts new file mode 100644 index 0000000000..dd8d107bb2 --- /dev/null +++ b/src/pages/EarnStrategies/components/index.ts @@ -0,0 +1 @@ +export { EarnStrategyForm } from './EarnStrategyForm'; diff --git a/src/pages/EarnStrategies/types/form.ts b/src/pages/EarnStrategies/types/form.ts new file mode 100644 index 0000000000..c912d7e549 --- /dev/null +++ b/src/pages/EarnStrategies/types/form.ts @@ -0,0 +1,18 @@ +type EarnStrategyFormType = 'deposit' | 'withdraw'; +type EarnStrategyRiskVariant = 'low' | 'high'; + +interface EarnStrategyDepositFormData { + deposit?: string; +} + +interface EarnStrategyWithdrawalFormData { + withdraw?: string; + withdrawAsWrapped?: boolean; +} + +export type { + EarnStrategyDepositFormData, + EarnStrategyFormType, + EarnStrategyRiskVariant, + EarnStrategyWithdrawalFormData +}; diff --git a/yarn.lock b/yarn.lock index 77273acb3a..b05034e254 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2156,6 +2156,15 @@ 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" From aca4e7dc88ddd1dfb29e77dba1b2b49892c19973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sim=C3=A3o?= Date: Tue, 30 May 2023 15:30:52 +0100 Subject: [PATCH 020/241] feat: add Popover, Underlay and ProgressBar. Changes to Dialog, Modal and Overlay. (#1236) --- package.json | 6 +- .../Dialog/Dialog.stories.tsx | 208 ++++++++++++++++++ src/component-library/Dialog/Dialog.style.tsx | 53 +++++ src/component-library/Dialog/Dialog.tsx | 52 +++++ src/component-library/Dialog/DialogBody.tsx | 14 ++ .../Dialog/DialogContext.tsx | 18 ++ .../Dialog/DialogDivider.tsx | 14 ++ src/component-library/Dialog/DialogFooter.tsx | 16 ++ src/component-library/Dialog/DialogHeader.tsx | 34 +++ src/component-library/Dialog/index.tsx | 10 + .../Divider/Divider.style.tsx | 5 +- src/component-library/Divider/Divider.tsx | 8 +- src/component-library/Modal/Dialog.tsx | 46 ---- src/component-library/Modal/Modal.style.tsx | 80 +------ src/component-library/Modal/Modal.tsx | 60 +++-- src/component-library/Modal/ModalBody.tsx | 17 +- src/component-library/Modal/ModalContext.tsx | 2 - src/component-library/Modal/ModalDivider.tsx | 9 +- src/component-library/Modal/ModalFooter.tsx | 9 +- src/component-library/Modal/ModalHeader.tsx | 42 +--- src/component-library/Modal/ModalWrapper.tsx | 11 +- .../Overlay/Overlay.style.tsx | 25 ++- src/component-library/Overlay/Overlay.tsx | 5 +- src/component-library/Overlay/Underlay.tsx | 19 ++ src/component-library/Overlay/index.tsx | 2 + .../Popover/Popover.stories.tsx | 40 ++++ .../Popover/Popover.style.tsx | 30 +++ src/component-library/Popover/Popover.tsx | 54 +++++ src/component-library/Popover/PopoverBody.tsx | 8 + .../Popover/PopoverContent.tsx | 40 ++++ .../Popover/PopoverContentWrapper.tsx | 74 +++++++ .../Popover/PopoverContext.tsx | 22 ++ .../Popover/PopoverFooter.tsx | 10 + .../Popover/PopoverHeader.tsx | 12 + .../Popover/PopoverTrigger.tsx | 38 ++++ src/component-library/Popover/index.tsx | 12 + .../ProgressBar/ProgressBar.stories.tsx | 18 ++ .../ProgressBar/ProgressBar.style.tsx | 27 +++ .../ProgressBar/ProgressBar.tsx | 51 +++++ src/component-library/ProgressBar/index.tsx | 2 + src/component-library/Select/Select.style.tsx | 3 +- src/component-library/Text/style.tsx | 17 +- src/component-library/Text/types.ts | 1 + src/component-library/Text/utils.ts | 6 +- .../TextLink/TextLink.style.tsx | 23 +- src/component-library/TextLink/TextLink.tsx | 25 ++- .../TokenInput/TokenInput.style.tsx | 4 +- src/component-library/Tooltip/Tooltip.tsx | 3 +- src/component-library/index.tsx | 11 + src/component-library/theme/theme.base.css | 1 + src/component-library/theme/theme.ts | 103 +++++++-- src/component-library/utils/prop-types.ts | 5 +- src/component-library/utils/theme.ts | 6 + .../AccountSelect/AccountSelect.style.tsx | 3 +- .../FundWallet/FundWallet.style.tsx | 2 +- .../components/PoolModal/PoolModal.style.tsx | 2 +- .../components/LoanModal/LoanModal.style.tsx | 2 +- yarn.lock | 140 +++++++++++- 58 files changed, 1302 insertions(+), 258 deletions(-) create mode 100644 src/component-library/Dialog/Dialog.stories.tsx create mode 100644 src/component-library/Dialog/Dialog.style.tsx create mode 100644 src/component-library/Dialog/Dialog.tsx create mode 100644 src/component-library/Dialog/DialogBody.tsx create mode 100644 src/component-library/Dialog/DialogContext.tsx create mode 100644 src/component-library/Dialog/DialogDivider.tsx create mode 100644 src/component-library/Dialog/DialogFooter.tsx create mode 100644 src/component-library/Dialog/DialogHeader.tsx create mode 100644 src/component-library/Dialog/index.tsx delete mode 100644 src/component-library/Modal/Dialog.tsx create mode 100644 src/component-library/Overlay/Underlay.tsx create mode 100644 src/component-library/Popover/Popover.stories.tsx create mode 100644 src/component-library/Popover/Popover.style.tsx create mode 100644 src/component-library/Popover/Popover.tsx create mode 100644 src/component-library/Popover/PopoverBody.tsx create mode 100644 src/component-library/Popover/PopoverContent.tsx create mode 100644 src/component-library/Popover/PopoverContentWrapper.tsx create mode 100644 src/component-library/Popover/PopoverContext.tsx create mode 100644 src/component-library/Popover/PopoverFooter.tsx create mode 100644 src/component-library/Popover/PopoverHeader.tsx create mode 100644 src/component-library/Popover/PopoverTrigger.tsx create mode 100644 src/component-library/Popover/index.tsx create mode 100644 src/component-library/ProgressBar/ProgressBar.stories.tsx create mode 100644 src/component-library/ProgressBar/ProgressBar.style.tsx create mode 100644 src/component-library/ProgressBar/ProgressBar.tsx create mode 100644 src/component-library/ProgressBar/index.tsx diff --git a/package.json b/package.json index 3b6ae00edf..af4e4260e6 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "@react-aria/link": "^3.4.0", "@react-aria/listbox": "^3.8.1", "@react-aria/meter": "^3.2.1", - "@react-aria/overlays": "^3.12.0", - "@react-aria/progress": "^3.3.0", + "@react-aria/overlays": "^3.14.0", + "@react-aria/progress": "^3.4.1", "@react-aria/select": "^3.9.0", "@react-aria/separator": "^3.2.5", "@react-aria/switch": "^3.2.4", @@ -37,7 +37,7 @@ "@react-aria/visually-hidden": "^3.6.1", "@react-stately/collections": "^3.4.1", "@react-stately/list": "^3.6.1", - "@react-stately/overlays": "^3.4.3", + "@react-stately/overlays": "^3.5.1", "@react-stately/select": "^3.4.0", "@react-stately/table": "^3.3.0", "@react-stately/tabs": "^3.4.0", diff --git a/src/component-library/Dialog/Dialog.stories.tsx b/src/component-library/Dialog/Dialog.stories.tsx new file mode 100644 index 0000000000..86b2a655a5 --- /dev/null +++ b/src/component-library/Dialog/Dialog.stories.tsx @@ -0,0 +1,208 @@ +import { Meta, Story } from '@storybook/react'; + +import { CTA } from '../CTA'; +import { Dialog, DialogBody, DialogDivider, DialogFooter, DialogHeader, DialogProps } from '.'; + +const Template: Story = ({ + children, + hasFooter, + hasTitle, + ...args +}) => { + return ( + <> + + {hasTitle && ( + <> + + Title + + + + )} + {children} + {hasFooter && ( + + Procced + + )} + + + ); +}; + +const Default = Template.bind({}); +Default.args = { + children: ( + <> + Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. + Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl + consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, + egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, + vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac + facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo + cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo + odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. + + ) +}; + +const WithTitle = Template.bind({}); +WithTitle.args = { + hasTitle: true, + children: ( + <> + Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. + Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl + consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, + egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, + vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac + facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo + cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo + odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. + + ) +}; + +const WithFooter = Template.bind({}); +WithFooter.args = { + hasFooter: true, + children: ( + <> + Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. + Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl + consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, + egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, + vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac + facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo + cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo + odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. + + ) +}; + +const LargeContent = Template.bind({}); +LargeContent.args = { + hasFooter: true, + hasTitle: true, + children: ( + <> + Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. + Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl + consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, + egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, + vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac + facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo + cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo + odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. + Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet + fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, + vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur + purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac + consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras + mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi + leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl + consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, + egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, + vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac + facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo + cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo + odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. + Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet + fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, + vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur + purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac + consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras + mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi + leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl + consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, + egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, + vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac + facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo + cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo + odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. + Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet + fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, + vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur + purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac + consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras + mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi + leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl + consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, + egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, + vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac + facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo + cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo + odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. + Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet + fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, + vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur + purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac + consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras + mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi + leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl + consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, + egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, + vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac + facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo + cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo + odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. + Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet + fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, + vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur + purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac + consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras + mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi + leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl + consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, + egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, + vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac + facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo + cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo + odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. + Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet + fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, + vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur + purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac + consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras + mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi + leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl + consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, + egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, + vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac + facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo + cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo + odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. + Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet + fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, + vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur + purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac + consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras + mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi + leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl + consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, + egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, + vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac + facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo + cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo + odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. + Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet + fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, + vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras mattis consectetur + purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac + consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras + mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi + leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel scelerisque nisl + consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, + egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, + vel scelerisque nisl consectetur et. + + ) +}; + +export { Default, LargeContent, WithFooter, WithTitle }; + +export default { + title: 'Overlays/Dialog', + component: Dialog +} as Meta; diff --git a/src/component-library/Dialog/Dialog.style.tsx b/src/component-library/Dialog/Dialog.style.tsx new file mode 100644 index 0000000000..5f482bc6c5 --- /dev/null +++ b/src/component-library/Dialog/Dialog.style.tsx @@ -0,0 +1,53 @@ +import styled from 'styled-components'; + +import { CTA } from '../CTA'; +import { Divider } from '../Divider'; +import { Flex } from '../Flex'; +import { H3 } from '../Text'; +import { theme } from '../theme'; +import { Sizes } from '../utils/prop-types'; + +type StyledDialogProps = { + $size: Sizes; +}; + +const StyledDialog = styled.section` + background: ${theme.colors.bgPrimary}; + border: ${theme.border.default}; + border-radius: ${theme.rounded.md}; + color: ${theme.colors.textPrimary}; + width: ${({ $size }) => theme.dialog[$size].width}; + display: flex; + flex-direction: column; + position: relative; + outline: none; +`; + +const StyledCloseCTA = styled(CTA)` + position: absolute; + top: ${theme.spacing.spacing2}; + right: ${theme.spacing.spacing2}; + z-index: ${theme.dialog.closeBtn.zIndex}; +`; + +const StyledDialogHeader = styled(H3)` + padding: ${({ $size }) => theme.dialog[$size].header.padding}; + overflow: hidden; + flex-shrink: 0; +`; + +const StyledDialogDivider = styled(Divider)` + margin: ${({ $size }) => `0 ${theme.dialog[$size].divider.marginX} ${theme.dialog[$size].divider.marginBottom}`}; + flex-shrink: 0; +`; + +const StyledDialogBody = styled(Flex)` + padding: ${({ $size }) => `${theme.dialog[$size].body.paddingY} ${theme.dialog[$size].body.paddingX}`}; + flex: 1 1 auto; +`; + +const StyledDialogFooter = styled(Flex)` + padding: ${({ $size }) => theme.dialog[$size].footer.padding}; +`; + +export { StyledCloseCTA, StyledDialog, StyledDialogBody, StyledDialogDivider, StyledDialogFooter, StyledDialogHeader }; diff --git a/src/component-library/Dialog/Dialog.tsx b/src/component-library/Dialog/Dialog.tsx new file mode 100644 index 0000000000..cf706540e5 --- /dev/null +++ b/src/component-library/Dialog/Dialog.tsx @@ -0,0 +1,52 @@ +import { AriaDialogProps, useDialog } from '@react-aria/dialog'; +import { mergeProps } from '@react-aria/utils'; +import { PressEvent } from '@react-types/shared'; +import { forwardRef, ReactNode } from 'react'; + +import { XMark } from '@/assets/icons'; + +import { useDOMRef } from '../utils/dom'; +import { CTASizes, Sizes } from '../utils/prop-types'; +import { StyledCloseCTA, StyledDialog } from './Dialog.style'; +import { DialogContext } from './DialogContext'; + +const closeCTASizeMap: Record = { small: 'x-small', medium: 'small', large: 'small' }; + +type Props = { + children?: ReactNode; + onClose?: (e: PressEvent) => void; + size?: Sizes; +}; + +type InheritAttrs = Omit; + +type DialogProps = Props & InheritAttrs; + +const Dialog = forwardRef( + ({ children, onClose, size = 'medium', ...props }, ref): JSX.Element => { + const dialogRef = useDOMRef(ref); + + // Get props for the dialog and its title + const { dialogProps, titleProps } = useDialog(props, dialogRef); + + const closeCTASize = closeCTASizeMap[size]; + + return ( + + + {onClose && ( + + + + )} + {children} + + + ); + } +); + +Dialog.displayName = 'Dialog'; + +export { Dialog }; +export type { DialogProps }; diff --git a/src/component-library/Dialog/DialogBody.tsx b/src/component-library/Dialog/DialogBody.tsx new file mode 100644 index 0000000000..0c7a36d260 --- /dev/null +++ b/src/component-library/Dialog/DialogBody.tsx @@ -0,0 +1,14 @@ +import { FlexProps } from '../Flex'; +import { StyledDialogBody } from './Dialog.style'; +import { useDialogContext } from './DialogContext'; + +type DialogBodyProps = FlexProps; + +const DialogBody = ({ direction = 'column', ...props }: DialogBodyProps): JSX.Element => { + const { size } = useDialogContext(); + + return ; +}; + +export { DialogBody }; +export type { DialogBodyProps }; diff --git a/src/component-library/Dialog/DialogContext.tsx b/src/component-library/Dialog/DialogContext.tsx new file mode 100644 index 0000000000..ca351cb5b4 --- /dev/null +++ b/src/component-library/Dialog/DialogContext.tsx @@ -0,0 +1,18 @@ +import { DOMAttributes } from '@react-types/shared'; +import React from 'react'; + +import { Sizes } from '../utils/prop-types'; + +interface DialogConfig { + titleProps?: DOMAttributes; + size: Sizes; +} + +const defaultContext: DialogConfig = { size: 'medium' }; + +const DialogContext = React.createContext(defaultContext); + +const useDialogContext = (): DialogConfig => React.useContext(DialogContext); + +export { DialogContext, useDialogContext }; +export type { DialogConfig }; diff --git a/src/component-library/Dialog/DialogDivider.tsx b/src/component-library/Dialog/DialogDivider.tsx new file mode 100644 index 0000000000..e724cd4293 --- /dev/null +++ b/src/component-library/Dialog/DialogDivider.tsx @@ -0,0 +1,14 @@ +import { DividerProps } from '../Divider'; +import { StyledDialogDivider } from './Dialog.style'; +import { useDialogContext } from './DialogContext'; + +type DialogDividerProps = Omit; + +const DialogDivider = (props: DialogDividerProps): JSX.Element => { + const { size } = useDialogContext(); + + return ; +}; + +export { DialogDivider }; +export type { DialogDividerProps }; diff --git a/src/component-library/Dialog/DialogFooter.tsx b/src/component-library/Dialog/DialogFooter.tsx new file mode 100644 index 0000000000..a9212d5a6f --- /dev/null +++ b/src/component-library/Dialog/DialogFooter.tsx @@ -0,0 +1,16 @@ +import { FlexProps } from '../Flex'; +import { StyledDialogFooter } from './Dialog.style'; +import { useDialogContext } from './DialogContext'; + +type InheritAttrs = FlexProps; + +type DialogFooterProps = InheritAttrs; + +const DialogFooter = (props: DialogFooterProps): JSX.Element => { + const { size } = useDialogContext(); + + return ; +}; + +export { DialogFooter }; +export type { DialogFooterProps }; diff --git a/src/component-library/Dialog/DialogHeader.tsx b/src/component-library/Dialog/DialogHeader.tsx new file mode 100644 index 0000000000..4130cb70b3 --- /dev/null +++ b/src/component-library/Dialog/DialogHeader.tsx @@ -0,0 +1,34 @@ +import { mergeProps } from '@react-aria/utils'; +import { ElementType } from 'react'; + +import { TextProps } from '../Text'; +import { FontSize, Sizes } from '../utils/prop-types'; +import { StyledDialogHeader } from './Dialog.style'; +import { useDialogContext } from './DialogContext'; + +const sizeMap: Record = { + small: 'base', + medium: 'xl', + large: 'xl' +}; + +type Props = { + elementType?: ElementType; +}; + +type InheritAttrs = Omit; + +type DialogHeaderProps = Props & InheritAttrs; + +const DialogHeader = ({ elementType, children, ...props }: DialogHeaderProps): JSX.Element => { + const { titleProps, size } = useDialogContext(); + + return ( + + {children} + + ); +}; + +export { DialogHeader }; +export type { DialogHeaderProps }; diff --git a/src/component-library/Dialog/index.tsx b/src/component-library/Dialog/index.tsx new file mode 100644 index 0000000000..b5980d46cd --- /dev/null +++ b/src/component-library/Dialog/index.tsx @@ -0,0 +1,10 @@ +export type { DialogProps } from './Dialog'; +export { Dialog } from './Dialog'; +export type { DialogBodyProps } from './DialogBody'; +export { DialogBody } from './DialogBody'; +export type { DialogDividerProps } from './DialogDivider'; +export { DialogDivider } from './DialogDivider'; +export type { DialogFooterProps } from './DialogFooter'; +export { DialogFooter } from './DialogFooter'; +export type { DialogHeaderProps } from './DialogHeader'; +export { DialogHeader } from './DialogHeader'; diff --git a/src/component-library/Divider/Divider.style.tsx b/src/component-library/Divider/Divider.style.tsx index 28d8b361bc..2cf4de19ef 100644 --- a/src/component-library/Divider/Divider.style.tsx +++ b/src/component-library/Divider/Divider.style.tsx @@ -1,5 +1,6 @@ import styled from 'styled-components'; +import { marginCSS, StyledMarginProps } from '../css/margin'; import { theme } from '../theme'; import { DividerVariants, Orientation, Sizes } from '../utils/prop-types'; import { resolveColor } from '../utils/theme'; @@ -8,7 +9,7 @@ type StyledDividerProps = { $color: DividerVariants; $orientation: Orientation; $size: Sizes; -}; +} & StyledMarginProps; const StyledDivider = styled.hr` background-color: ${({ $color }) => ($color === 'default' ? 'var(--colors-border)' : resolveColor($color))}; @@ -17,6 +18,8 @@ const StyledDivider = styled.hr` border: 0; margin: 0; align-self: stretch; + flex-shrink: 0; + ${(props) => marginCSS(props)}; `; export { StyledDivider }; diff --git a/src/component-library/Divider/Divider.tsx b/src/component-library/Divider/Divider.tsx index 6d1e3cebfa..6b71b7a1e2 100644 --- a/src/component-library/Divider/Divider.tsx +++ b/src/component-library/Divider/Divider.tsx @@ -2,7 +2,8 @@ import { useSeparator } from '@react-aria/separator'; import { mergeProps } from '@react-aria/utils'; import { forwardRef, HTMLAttributes } from 'react'; -import { DividerVariants, ElementTypeProp, Orientation, Sizes } from '../utils/prop-types'; +import { DividerVariants, ElementTypeProp, MarginProps, Orientation, Sizes } from '../utils/prop-types'; +import { useStyleProps } from '../utils/use-style-props'; import { StyledDivider } from './Divider.style'; type Props = { @@ -13,7 +14,7 @@ type Props = { type NativeAttrs = Omit, keyof Props>; -type DividerProps = Props & NativeAttrs & ElementTypeProp; +type DividerProps = Props & NativeAttrs & ElementTypeProp & MarginProps; const Divider = forwardRef( ( @@ -26,6 +27,7 @@ const Divider = forwardRef( ...props, elementType }); + const { styleProps, componentProps } = useStyleProps(props); return ( ( $color={color} $orientation={orientation} $size={size} - {...mergeProps(separatorProps, props)} + {...mergeProps(separatorProps, styleProps, componentProps)} /> ); } diff --git a/src/component-library/Modal/Dialog.tsx b/src/component-library/Modal/Dialog.tsx deleted file mode 100644 index ecd782729a..0000000000 --- a/src/component-library/Modal/Dialog.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { AriaDialogProps, useDialog } from '@react-aria/dialog'; -import { forwardRef, ReactNode } from 'react'; - -import { XMark } from '@/assets/icons'; - -import { useDOMRef } from '../utils/dom'; -import { StyledCloseCTA, StyledDialog } from './Modal.style'; -import { ModalContext } from './ModalContext'; - -type Props = { - children: ReactNode; - align?: 'top' | 'center'; - hasMaxHeight?: boolean; - onClose: () => void; -}; - -type InheritAttrs = Omit; - -type DialogProps = Props & InheritAttrs; - -const Dialog = forwardRef( - ({ children, align = 'center', hasMaxHeight, onClose, ...props }, ref): JSX.Element | null => { - const dialogRef = useDOMRef(ref); - - // Get props for the dialog and its title - const { dialogProps, titleProps } = useDialog(props, dialogRef); - - const isCentered = align === 'center'; - - return ( - - - - - - {children} - - - ); - } -); - -Dialog.displayName = 'Dialog'; - -export { Dialog }; -export type { DialogProps }; diff --git a/src/component-library/Modal/Modal.style.tsx b/src/component-library/Modal/Modal.style.tsx index 1dc9da67af..9a222b8a7f 100644 --- a/src/component-library/Modal/Modal.style.tsx +++ b/src/component-library/Modal/Modal.style.tsx @@ -1,10 +1,7 @@ import styled from 'styled-components'; import { overlayCSS } from '../css/overlay'; -import { CTA } from '../CTA'; -import { Divider } from '../Divider'; -import { Flex } from '../Flex'; -import { H3 } from '../Text'; +import { Dialog, DialogBody } from '../Dialog'; import { theme } from '../theme'; import { Overflow } from '../utils/prop-types'; @@ -23,26 +20,6 @@ type StyledModalBodyProps = { $noPadding?: boolean; }; -type StyledUnderlayProps = { - $isOpen: boolean; - $isCentered?: boolean; -}; - -const StyledUnderlay = styled.div` - position: fixed; - z-index: ${theme.modal.underlay.zIndex}; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - - background: ${theme.modal.underlay.bg}; - - ${({ $isOpen }) => overlayCSS($isOpen)} - transition: ${({ $isOpen }) => - $isOpen ? theme.modal.underlay.transition.entering : theme.modal.underlay.transition.exiting}; -`; - const StyledWrapper = styled.div` position: fixed; top: 0; @@ -59,8 +36,6 @@ const StyledWrapper = styled.div` `; const StyledModal = styled.div` - width: 100%; - max-width: ${theme.modal.maxWidth}; max-height: ${({ $isCentered }) => $isCentered && theme.modal.maxHeight}; margin: ${({ $isCentered }) => ($isCentered ? 0 : theme.spacing.spacing16)} ${theme.spacing.spacing6}; @@ -77,60 +52,15 @@ const StyledModal = styled.div` outline: none; `; -const StyledDialog = styled.section` - background: ${theme.colors.bgPrimary}; - border: ${theme.border.default}; - border-radius: ${theme.rounded.md}; - color: ${theme.colors.textPrimary}; - - width: 100%; +const StyledDialog = styled(Dialog)` max-height: ${({ $hasMaxHeight }) => $hasMaxHeight && '560px'}; overflow: ${({ $isCentered }) => $isCentered && 'hidden'}; - - display: flex; - flex-direction: column; - position: relative; - - outline: none; `; -const StyledCloseCTA = styled(CTA)` - position: absolute; - top: ${theme.spacing.spacing2}; - right: ${theme.spacing.spacing2}; - z-index: ${theme.modal.closeBtn.zIndex}; -`; - -const StyledModalHeader = styled(H3)` - padding: ${theme.modal.header.paddingY} ${theme.modal.header.paddingRight} ${theme.modal.header.paddingY} - ${theme.modal.header.paddingX}; - flex-shrink: 0; -`; - -const StyledModalDivider = styled(Divider)` - margin: 0 ${theme.modal.divider.marginX} ${theme.modal.divider.marginBottom}; - flex-shrink: 0; -`; - -const StyledModalBody = styled(Flex)` - flex: 1 1 auto; +const StyledDialogBody = styled(DialogBody)` overflow-y: ${({ $overflow }) => $overflow}; position: relative; - padding: ${({ $noPadding }) => !$noPadding && `${theme.modal.body.paddingY} ${theme.modal.body.paddingX}`}; + padding: ${({ $noPadding }) => $noPadding && 0}; `; -const StyledModalFooter = styled(Flex)` - padding: ${theme.modal.footer.paddingTop} ${theme.modal.footer.paddingX} ${theme.modal.footer.paddingBottom}; -`; - -export { - StyledCloseCTA, - StyledDialog, - StyledModal, - StyledModalBody, - StyledModalDivider, - StyledModalFooter, - StyledModalHeader, - StyledUnderlay, - StyledWrapper -}; +export { StyledDialog, StyledDialogBody, StyledModal, StyledWrapper }; diff --git a/src/component-library/Modal/Modal.tsx b/src/component-library/Modal/Modal.tsx index 9bff5876a4..54e1723365 100644 --- a/src/component-library/Modal/Modal.tsx +++ b/src/component-library/Modal/Modal.tsx @@ -1,17 +1,27 @@ -import { forwardRef } from 'react'; +import { forwardRef, useRef } from 'react'; +import { DialogProps } from '../Dialog'; import { Overlay } from '../Overlay'; import { useDOMRef } from '../utils/dom'; -import { Dialog, DialogProps } from './Dialog'; +import { StyledDialog } from './Modal.style'; +import { ModalContext } from './ModalContext'; import { ModalWrapper, ModalWrapperProps } from './ModalWrapper'; +const isInteractingWithToasts = (element: Element) => { + const toastsContainer = document.querySelector('.Toastify'); + + if (!toastsContainer) return false; + + return toastsContainer.contains(element); +}; + type Props = { container?: Element; hasMaxHeight?: boolean; align?: 'top' | 'center'; }; -type InheritAttrs = Omit; +type InheritAttrs = Omit; type ModalProps = Props & InheritAttrs; @@ -32,24 +42,36 @@ const Modal = forwardRef( ): JSX.Element | null => { const domRef = useDOMRef(ref); const { isOpen, onClose } = props; + const wrapperRef = useRef(null); + + const isCentered = align === 'center'; + + // Does not allow the modal to close when clicking on toasts + const handleShouldCloseOnInteractOutside = (element: Element) => + shouldCloseOnInteractOutside + ? shouldCloseOnInteractOutside?.(element) && !isInteractingWithToasts(element) + : !isInteractingWithToasts(element); return ( - - - - {children} - - - + + + + + {children} + + + + ); } ); diff --git a/src/component-library/Modal/ModalBody.tsx b/src/component-library/Modal/ModalBody.tsx index b888725293..7f949b254c 100644 --- a/src/component-library/Modal/ModalBody.tsx +++ b/src/component-library/Modal/ModalBody.tsx @@ -1,6 +1,6 @@ -import { FlexProps } from '../Flex'; +import { DialogBodyProps } from '../Dialog'; import { Overflow } from '../utils/prop-types'; -import { StyledModalBody } from './Modal.style'; +import { StyledDialogBody } from './Modal.style'; import { useModalContext } from './ModalContext'; type Props = { @@ -8,21 +8,14 @@ type Props = { noPadding?: boolean; }; -type InheritAttrs = Omit; +type InheritAttrs = Omit; type ModalBodyProps = Props & InheritAttrs; -const ModalBody = ({ overflow, noPadding, direction = 'column', ...props }: ModalBodyProps): JSX.Element => { +const ModalBody = ({ overflow, noPadding, ...props }: ModalBodyProps): JSX.Element => { const { bodyProps } = useModalContext(); - return ( - - ); + return ; }; export { ModalBody }; diff --git a/src/component-library/Modal/ModalContext.tsx b/src/component-library/Modal/ModalContext.tsx index 4c890537fe..42353cbe0e 100644 --- a/src/component-library/Modal/ModalContext.tsx +++ b/src/component-library/Modal/ModalContext.tsx @@ -1,10 +1,8 @@ -import { DOMAttributes } from '@react-types/shared'; import React from 'react'; import { ModalBodyProps } from './ModalBody'; interface ModalConfig { - titleProps?: DOMAttributes; bodyProps?: ModalBodyProps; } diff --git a/src/component-library/Modal/ModalDivider.tsx b/src/component-library/Modal/ModalDivider.tsx index 7159d73208..1e9630a48e 100644 --- a/src/component-library/Modal/ModalDivider.tsx +++ b/src/component-library/Modal/ModalDivider.tsx @@ -1,11 +1,8 @@ -import { DividerProps } from '../Divider'; -import { StyledModalDivider } from './Modal.style'; +import { DialogDivider, DialogDividerProps } from '../Dialog'; -type ModalDividerProps = Omit; +type ModalDividerProps = DialogDividerProps; -const ModalDivider = (props: ModalDividerProps): JSX.Element => ( - -); +const ModalDivider = (props: ModalDividerProps): JSX.Element => ; export { ModalDivider }; export type { ModalDividerProps }; diff --git a/src/component-library/Modal/ModalFooter.tsx b/src/component-library/Modal/ModalFooter.tsx index bd66ff2c3d..655a8d5d20 100644 --- a/src/component-library/Modal/ModalFooter.tsx +++ b/src/component-library/Modal/ModalFooter.tsx @@ -1,12 +1,9 @@ -import { FlexProps } from '../Flex'; -import { StyledModalFooter } from './Modal.style'; +import { DialogFooter, DialogFooterProps } from '../Dialog'; -type InheritAttrs = FlexProps; - -type ModalFooterProps = InheritAttrs; +type ModalFooterProps = DialogFooterProps; const ModalFooter = ({ direction = 'column', gap = 'spacing4', ...props }: ModalFooterProps): JSX.Element => ( - + ); export { ModalFooter }; diff --git a/src/component-library/Modal/ModalHeader.tsx b/src/component-library/Modal/ModalHeader.tsx index 38545726fc..decf2059e3 100644 --- a/src/component-library/Modal/ModalHeader.tsx +++ b/src/component-library/Modal/ModalHeader.tsx @@ -1,40 +1,12 @@ -import { mergeProps } from '@react-aria/utils'; -import { ElementType } from 'react'; +import { DialogHeader, DialogHeaderProps } from '../Dialog'; -import { TextProps } from '../Text'; -import { StyledModalHeader } from './Modal.style'; -import { useModalContext } from './ModalContext'; +type ModalHeaderProps = DialogHeaderProps; -type Props = { - elementType?: ElementType; -}; - -type InheritAttrs = Omit; - -type ModalHeaderProps = Props & InheritAttrs; - -const ModalHeader = ({ - align = 'center', - size = 'xl', - weight = 'semibold', - elementType, - children, - ...props -}: ModalHeaderProps): JSX.Element => { - const { titleProps } = useModalContext(); - - return ( - - {children} - - ); -}; +const ModalHeader = ({ align = 'center', children, ...props }: ModalHeaderProps): JSX.Element => ( + + {children} + +); export { ModalHeader }; export type { ModalHeaderProps }; diff --git a/src/component-library/Modal/ModalWrapper.tsx b/src/component-library/Modal/ModalWrapper.tsx index 3bb7dab4c9..8ce98068e6 100644 --- a/src/component-library/Modal/ModalWrapper.tsx +++ b/src/component-library/Modal/ModalWrapper.tsx @@ -3,13 +3,15 @@ import { mergeProps } from '@react-aria/utils'; import { OverlayTriggerState } from '@react-stately/overlays'; import { forwardRef, ReactNode, RefObject } from 'react'; -import { StyledModal, StyledUnderlay, StyledWrapper } from './Modal.style'; +import { Underlay } from '../Overlay'; +import { StyledModal, StyledWrapper } from './Modal.style'; type Props = { children: ReactNode; align?: 'top' | 'center'; isOpen?: boolean; onClose: () => void; + wrapperRef: RefObject; }; type InheritAttrs = Omit; @@ -27,6 +29,7 @@ const ModalWrapper = forwardRef( isOpen, shouldCloseOnInteractOutside, shouldCloseOnBlur, + wrapperRef, ...props }, ref @@ -49,14 +52,14 @@ const ModalWrapper = forwardRef( const isCentered = align === 'center'; return ( - <> - +
+ {children} - +
); } ); diff --git a/src/component-library/Overlay/Overlay.style.tsx b/src/component-library/Overlay/Overlay.style.tsx index 770f816067..d348e5594f 100644 --- a/src/component-library/Overlay/Overlay.style.tsx +++ b/src/component-library/Overlay/Overlay.style.tsx @@ -1,8 +1,31 @@ import styled from 'styled-components'; +import { overlayCSS } from '../css/overlay'; +import { theme } from '../theme'; + +type StyledUnderlayProps = { + $isOpen: boolean; + $isTransparent: boolean; +}; + const StyledOverlayWrapper = styled.div` isolation: isolate; background: transparent; `; -export { StyledOverlayWrapper }; +const StyledUnderlay = styled.div` + position: fixed; + z-index: ${theme.modal.underlay.zIndex}; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + + background: ${({ $isTransparent }) => ($isTransparent ? 'transparent' : theme.modal.underlay.bg)}; + + ${({ $isOpen }) => overlayCSS($isOpen)} + transition: ${({ $isOpen }) => + $isOpen ? theme.modal.underlay.transition.entering : theme.modal.underlay.transition.exiting}; +`; + +export { StyledOverlayWrapper, StyledUnderlay }; diff --git a/src/component-library/Overlay/Overlay.tsx b/src/component-library/Overlay/Overlay.tsx index 45cafff805..3e92902704 100644 --- a/src/component-library/Overlay/Overlay.tsx +++ b/src/component-library/Overlay/Overlay.tsx @@ -1,11 +1,12 @@ import { Overlay as AriaOverlay } from '@react-aria/overlays'; -import { ReactNode, useCallback, useState } from 'react'; +import { ReactNode, RefObject, useCallback, useState } from 'react'; import { OpenTransition } from './OpenTransition'; import { StyledOverlayWrapper } from './Overlay.style'; type OverlayProps = { children: ReactNode; + nodeRef: RefObject; isOpen?: boolean; container?: Element; onEnter?: () => void; @@ -18,6 +19,7 @@ type OverlayProps = { const Overlay = ({ children, + nodeRef, isOpen, container, onEnter, @@ -64,6 +66,7 @@ const Overlay = ({ onEnter={onEnter} onEntering={onEntering} onEntered={handleEntered} + nodeRef={nodeRef} > {children} diff --git a/src/component-library/Overlay/Underlay.tsx b/src/component-library/Overlay/Underlay.tsx new file mode 100644 index 0000000000..9dfcbebc8f --- /dev/null +++ b/src/component-library/Overlay/Underlay.tsx @@ -0,0 +1,19 @@ +import { HTMLAttributes } from 'react'; + +import { StyledUnderlay } from './Overlay.style'; + +type Props = { + isTransparent?: boolean; + isOpen?: boolean; +}; + +type NativeAttrs = Omit, keyof Props>; + +type UnderlayProps = Props & NativeAttrs; + +const Underlay = ({ isTransparent = false, isOpen = false, ...props }: UnderlayProps): JSX.Element => ( + +); + +export { Underlay }; +export type { UnderlayProps }; diff --git a/src/component-library/Overlay/index.tsx b/src/component-library/Overlay/index.tsx index 9c047f467d..ac912a2e2b 100644 --- a/src/component-library/Overlay/index.tsx +++ b/src/component-library/Overlay/index.tsx @@ -1,2 +1,4 @@ export type { OverlayProps } from './Overlay'; export { Overlay } from './Overlay'; +export type { UnderlayProps } from './Underlay'; +export { Underlay } from './Underlay'; diff --git a/src/component-library/Popover/Popover.stories.tsx b/src/component-library/Popover/Popover.stories.tsx new file mode 100644 index 0000000000..68ef806297 --- /dev/null +++ b/src/component-library/Popover/Popover.stories.tsx @@ -0,0 +1,40 @@ +import { Meta, Story } from '@storybook/react'; + +import { CTA } from '../CTA'; +import { P } from '../Text'; +import { Placement } from '../utils/prop-types'; +import { Popover, PopoverBody, PopoverContent, PopoverFooter, PopoverHeader, PopoverProps, PopoverTrigger } from '.'; + +const Template: Story = ({ placement, ...args }) => { + return ( + + + Open Popover + + + Popover Header + +

+ Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget + quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus magna, vel + scelerisque nisl consectetur et. Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus + ac facilisis in, egestas eget quam. +

+
+ + Confirm + +
+
+ ); +}; + +const Default = Template.bind({}); +Default.args = { placement: 'right' }; + +export { Default }; + +export default { + title: 'Overlays/Popover', + component: Popover +} as Meta; diff --git a/src/component-library/Popover/Popover.style.tsx b/src/component-library/Popover/Popover.style.tsx new file mode 100644 index 0000000000..40db6c8b91 --- /dev/null +++ b/src/component-library/Popover/Popover.style.tsx @@ -0,0 +1,30 @@ +import styled from 'styled-components'; + +import { getOverlayPlacementCSS, overlayCSS } from '../css/overlay'; +import { Placement } from '../utils/prop-types'; + +type StyledPopoverProps = { + $placement?: Placement | 'center'; + $isOpen: boolean; +}; + +const StyledPopover = styled.div` + display: inline-flex; + flex-direction: column; + box-sizing: border-box; + + min-width: 32px; + min-height: 32px; + + position: absolute; + + outline: none; /* Hide focus outline */ + box-sizing: border-box; + + ${({ $isOpen }) => overlayCSS(!!$isOpen)} + ${({ $placement }) => $placement && getOverlayPlacementCSS($placement as any)} + + transition: transform 100ms ease-in-out, opacity 100ms ease-in-out, visibility 0s linear 100ms; +`; + +export { StyledPopover }; diff --git a/src/component-library/Popover/Popover.tsx b/src/component-library/Popover/Popover.tsx new file mode 100644 index 0000000000..0d974f04af --- /dev/null +++ b/src/component-library/Popover/Popover.tsx @@ -0,0 +1,54 @@ +import { useOverlayTrigger } from '@react-aria/overlays'; +import { OverlayTriggerProps, useOverlayTriggerState } from '@react-stately/overlays'; +import { ReactNode, useRef } from 'react'; + +import { Placement } from '../utils/prop-types'; +import { PopoverContext } from './PopoverContext'; + +type Props = { + children?: ReactNode; + placement?: Placement; + offset?: number; + crossOffset?: number; + /* usePopover attempts to flip popovers on the main axis */ + /* overrides usePopover flip */ + shouldFlip?: boolean; + /* Control the minimum padding required between the popover and the surrounding container. */ + /* Affects the popover flip */ + containerPadding?: number; +}; + +type InheritAttrs = Omit; + +type PopoverProps = Props & InheritAttrs; + +const Popover = ({ + children, + placement, + offset, + crossOffset, + shouldFlip, + containerPadding, + ...props +}: PopoverProps): JSX.Element | null => { + const triggerRef = useRef(null); + const state = useOverlayTriggerState(props); + const { triggerProps, overlayProps } = useOverlayTrigger({ type: 'dialog' }, state, triggerRef); + + return ( + + {children} + + ); +}; + +export { Popover }; +export type { PopoverProps }; diff --git a/src/component-library/Popover/PopoverBody.tsx b/src/component-library/Popover/PopoverBody.tsx new file mode 100644 index 0000000000..d39dd0fcd0 --- /dev/null +++ b/src/component-library/Popover/PopoverBody.tsx @@ -0,0 +1,8 @@ +import { DialogBody, DialogBodyProps } from '../Dialog'; + +type PopoverBodyProps = DialogBodyProps; + +const PopoverBody = (props: PopoverBodyProps): JSX.Element => ; + +export { PopoverBody }; +export type { PopoverBodyProps }; diff --git a/src/component-library/Popover/PopoverContent.tsx b/src/component-library/Popover/PopoverContent.tsx new file mode 100644 index 0000000000..8becd5f989 --- /dev/null +++ b/src/component-library/Popover/PopoverContent.tsx @@ -0,0 +1,40 @@ +import { forwardRef, ReactNode, useRef } from 'react'; + +import { Overlay } from '../Overlay'; +import { useDOMRef } from '../utils/dom'; +import { PopoverContentWrapper } from './PopoverContentWrapper'; +import { usePopoverContext } from './PopoverContext'; + +type Props = { children?: ReactNode }; + +type PopoverContentProps = Props; + +const PopoverContent = forwardRef( + (props, ref): JSX.Element => { + const { children, ...otherProps } = props; + const domRef = useDOMRef(ref); + const wrapperRef = useRef(null); + const { state, triggerRef, dialogProps, popoverProps } = usePopoverContext(); + + return ( + + } + wrapperRef={wrapperRef} + > + {children} + + + ); + } +); + +PopoverContent.displayName = 'PopoverContent'; + +export { PopoverContent }; +export type { PopoverContentProps }; diff --git a/src/component-library/Popover/PopoverContentWrapper.tsx b/src/component-library/Popover/PopoverContentWrapper.tsx new file mode 100644 index 0000000000..7287366a9f --- /dev/null +++ b/src/component-library/Popover/PopoverContentWrapper.tsx @@ -0,0 +1,74 @@ +import { AriaPopoverProps, DismissButton, usePopover } from '@react-aria/overlays'; +import { OverlayTriggerState } from '@react-stately/overlays'; +import { DOMProps } from '@react-types/shared'; +import { forwardRef, HTMLAttributes, RefObject } from 'react'; + +import { Dialog } from '../Dialog'; +import { Underlay } from '../Overlay'; +import { StyledPopover } from './Popover.style'; + +type Props = { + state: OverlayTriggerState; + wrapperRef: RefObject; + isOpen?: boolean; + dialogProps?: DOMProps; + popoverProps?: Partial; +}; + +type InheritAttrs = Omit; + +type NativeAttrs = Omit, keyof Props & InheritAttrs>; + +type PopoverContentWrapperProps = Props & InheritAttrs & NativeAttrs; + +const PopoverContentWrapper = forwardRef( + (props, ref): JSX.Element | null => { + const { + children, + wrapperRef, + state, + isOpen, + className, + style, + isNonModal, + dialogProps, + popoverProps: popoverPropsProp + } = props; + + const { popoverProps, underlayProps, placement } = usePopover( + { + ...props, + popoverRef: ref as RefObject, + ...popoverPropsProp + }, + state + ); + + return ( +
+ {!isNonModal && } + + {!isNonModal && } + + {children} + + + +
+ ); + } +); + +PopoverContentWrapper.displayName = 'PopoverContentWrapper'; + +export { PopoverContentWrapper }; +export type { PopoverContentWrapperProps }; diff --git a/src/component-library/Popover/PopoverContext.tsx b/src/component-library/Popover/PopoverContext.tsx new file mode 100644 index 0000000000..37c34dc9fc --- /dev/null +++ b/src/component-library/Popover/PopoverContext.tsx @@ -0,0 +1,22 @@ +import { AriaButtonProps } from '@react-aria/button'; +import { AriaPopoverProps } from '@react-aria/overlays'; +import { OverlayTriggerState } from '@react-stately/overlays'; +import { DOMProps } from '@react-types/shared'; +import React, { RefObject } from 'react'; + +interface PopoverConfig { + state: OverlayTriggerState; + triggerRef?: RefObject; + triggerProps?: AriaButtonProps<'button'>; + dialogProps?: DOMProps; + popoverProps?: Partial; +} + +const defaultContext = { state: { isOpen: false } as OverlayTriggerState }; + +const PopoverContext = React.createContext(defaultContext); + +const usePopoverContext = (): PopoverConfig => React.useContext(PopoverContext); + +export { PopoverContext, usePopoverContext }; +export type { PopoverConfig }; diff --git a/src/component-library/Popover/PopoverFooter.tsx b/src/component-library/Popover/PopoverFooter.tsx new file mode 100644 index 0000000000..83a07a5524 --- /dev/null +++ b/src/component-library/Popover/PopoverFooter.tsx @@ -0,0 +1,10 @@ +import { DialogFooter, DialogFooterProps } from '../Dialog'; + +type PopoverFooterProps = DialogFooterProps; + +const PopoverFooter = ({ justifyContent = 'flex-end', ...props }: PopoverFooterProps): JSX.Element => ( + +); + +export { PopoverFooter }; +export type { PopoverFooterProps }; diff --git a/src/component-library/Popover/PopoverHeader.tsx b/src/component-library/Popover/PopoverHeader.tsx new file mode 100644 index 0000000000..04ea84f09e --- /dev/null +++ b/src/component-library/Popover/PopoverHeader.tsx @@ -0,0 +1,12 @@ +import { DialogHeader, DialogHeaderProps } from '../Dialog'; + +type PopoverHeaderProps = DialogHeaderProps; + +const PopoverHeader = ({ size = 'base', weight = 'semibold', children, ...props }: PopoverHeaderProps): JSX.Element => ( + + {children} + +); + +export { PopoverHeader }; +export type { PopoverHeaderProps }; diff --git a/src/component-library/Popover/PopoverTrigger.tsx b/src/component-library/Popover/PopoverTrigger.tsx new file mode 100644 index 0000000000..812b6b0516 --- /dev/null +++ b/src/component-library/Popover/PopoverTrigger.tsx @@ -0,0 +1,38 @@ +import { useButton } from '@react-aria/button'; +import { mergeProps } from '@react-aria/utils'; +import React, { Children, cloneElement, ElementType, ReactNode, RefObject } from 'react'; + +import { useDOMRef } from '../utils/dom'; +import { usePopoverContext } from './PopoverContext'; + +type Props = { + children?: ReactNode; +}; + +type PopoverTriggerProps = Props; + +const PopoverTrigger = ({ children }: PopoverTriggerProps): JSX.Element => { + const { triggerRef, triggerProps: { onPress, ...triggerAriaProps } = {} } = usePopoverContext(); + const ref = useDOMRef(triggerRef as RefObject); + + // MEMO: Ensure tooltip has only one child node + const child = Children.only(children) as React.ReactElement & { + ref?: React.Ref; + }; + + const elementType = ref.current?.tagName.toLowerCase() as ElementType; + + const { buttonProps } = useButton({ onPress, elementType, isDisabled: elementType === 'button' } || {}, ref); + + const triggerProps = + elementType === 'button' + ? mergeProps(child.props, triggerAriaProps, { onPress }) + : mergeProps(child.props, triggerAriaProps, buttonProps); + + const trigger = cloneElement(child, mergeProps(triggerProps, { ref })); + + return trigger; +}; + +export { PopoverTrigger }; +export type { PopoverTriggerProps }; diff --git a/src/component-library/Popover/index.tsx b/src/component-library/Popover/index.tsx new file mode 100644 index 0000000000..a69a739789 --- /dev/null +++ b/src/component-library/Popover/index.tsx @@ -0,0 +1,12 @@ +export type { PopoverProps } from './Popover'; +export { Popover } from './Popover'; +export type { PopoverBodyProps } from './PopoverBody'; +export { PopoverBody } from './PopoverBody'; +export type { PopoverContentProps } from './PopoverContent'; +export { PopoverContent } from './PopoverContent'; +export type { PopoverFooterProps } from './PopoverFooter'; +export { PopoverFooter } from './PopoverFooter'; +export type { PopoverHeaderProps } from './PopoverHeader'; +export { PopoverHeader } from './PopoverHeader'; +export type { PopoverTriggerProps } from './PopoverTrigger'; +export { PopoverTrigger } from './PopoverTrigger'; diff --git a/src/component-library/ProgressBar/ProgressBar.stories.tsx b/src/component-library/ProgressBar/ProgressBar.stories.tsx new file mode 100644 index 0000000000..d0047ef340 --- /dev/null +++ b/src/component-library/ProgressBar/ProgressBar.stories.tsx @@ -0,0 +1,18 @@ +import { Meta, Story } from '@storybook/react'; + +import { ProgressBar, ProgressBarProps } from '.'; + +const Template: Story = (args) => ; + +const Default = Template.bind({}); +Default.args = { + value: 20, + label: 'Loading...' +}; + +export { Default }; + +export default { + title: 'Elements/ProgressBar', + component: ProgressBar +} as Meta; diff --git a/src/component-library/ProgressBar/ProgressBar.style.tsx b/src/component-library/ProgressBar/ProgressBar.style.tsx new file mode 100644 index 0000000000..feeff94120 --- /dev/null +++ b/src/component-library/ProgressBar/ProgressBar.style.tsx @@ -0,0 +1,27 @@ +import styled from 'styled-components'; + +import { theme } from '../theme'; +import { ProgressBarColors } from '../utils/prop-types'; + +type StyledFillProps = { + $color: ProgressBarColors; +}; + +const StyledTrack = styled.div` + overflow: hidden; + z-index: 1; + width: 100%; + min-width: ${theme.spacing.spacing6}; + background-color: ${theme.progressBar.bg}; + height: 1px; +`; + +const StyledFill = styled.div` + background-color: ${({ $color }) => ($color === 'red' ? theme.alert.status.error : theme.colors.textSecondary)}; + height: 1px; + border: none; + transition: width ${theme.transition.duration.duration100}ms; + will-change: width; +`; + +export { StyledFill, StyledTrack }; diff --git a/src/component-library/ProgressBar/ProgressBar.tsx b/src/component-library/ProgressBar/ProgressBar.tsx new file mode 100644 index 0000000000..a7eaf508ff --- /dev/null +++ b/src/component-library/ProgressBar/ProgressBar.tsx @@ -0,0 +1,51 @@ +import { AriaProgressBarProps, useProgressBar } from '@react-aria/progress'; +import { CSSProperties } from 'react'; + +import { Flex, FlexProps } from '../Flex'; +import { Span } from '../Text'; +import { ProgressBarColors } from '../utils/prop-types'; +import { StyledFill, StyledTrack } from './ProgressBar.style'; + +type Props = { color?: ProgressBarColors; showValueLabel?: boolean }; + +type AriaAttrs = Omit; + +type InheritAttrs = Omit; + +type ProgressBarProps = Props & InheritAttrs & AriaAttrs; + +const ProgressBar = (props: ProgressBarProps): JSX.Element => { + const { progressBarProps, labelProps } = useProgressBar(props); + + const { + value = 0, + minValue = 0, + maxValue = 100, + color = 'default', + showValueLabel, + label, + className, + style, + hidden + } = props; + + const percentage = (value - minValue) / (maxValue - minValue); + const barStyle: CSSProperties = { width: `${Math.round(percentage * 100)}%` }; + + return ( + + ); +}; + +export { ProgressBar }; +export type { ProgressBarProps }; diff --git a/src/component-library/ProgressBar/index.tsx b/src/component-library/ProgressBar/index.tsx new file mode 100644 index 0000000000..098ea9d4bb --- /dev/null +++ b/src/component-library/ProgressBar/index.tsx @@ -0,0 +1,2 @@ +export type { ProgressBarProps } from './ProgressBar'; +export { ProgressBar } from './ProgressBar'; diff --git a/src/component-library/Select/Select.style.tsx b/src/component-library/Select/Select.style.tsx index d79d62f111..03a766d386 100644 --- a/src/component-library/Select/Select.style.tsx +++ b/src/component-library/Select/Select.style.tsx @@ -66,7 +66,8 @@ const StyledTriggerValue = styled(Span)` const StyledList = styled(List)` overflow: auto; - padding: 0 ${theme.modal.body.paddingX} ${theme.modal.body.paddingY} ${theme.modal.body.paddingX}; + padding: 0 ${theme.dialog.medium.body.paddingX} ${theme.dialog.medium.body.paddingY} + ${theme.dialog.medium.body.paddingX}; `; const StyledChevronDown = styled(ChevronDown)` diff --git a/src/component-library/Text/style.tsx b/src/component-library/Text/style.tsx index abf26946fa..2299c8592c 100644 --- a/src/component-library/Text/style.tsx +++ b/src/component-library/Text/style.tsx @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { theme } from '../theme'; import { Colors, FontSize, FontWeight, NormalAlignments } from '../utils/prop-types'; @@ -9,6 +9,7 @@ type StyledTextProps = { $size?: FontSize; $align?: NormalAlignments; $weight?: FontWeight; + $rows?: number; }; const Text = styled.p` @@ -17,6 +18,20 @@ const Text = styled.p` line-height: ${({ $size }) => resolveHeight($size)}; font-weight: ${({ $weight }) => $weight && theme.fontWeight[$weight]}; text-align: ${({ $align }) => $align}; + + ${({ $rows }) => { + return ( + $rows && + css` + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + line-clamp: ${$rows}; + -webkit-line-clamp: ${$rows}; + -webkit-box-orient: vertical; + ` + ); + }} `; export { Text }; diff --git a/src/component-library/Text/types.ts b/src/component-library/Text/types.ts index 812bcc7def..56f047f723 100644 --- a/src/component-library/Text/types.ts +++ b/src/component-library/Text/types.ts @@ -7,6 +7,7 @@ type Props = { size?: FontSize; align?: NormalAlignments; weight?: FontWeight; + rows?: number; }; type NativeAttrs = Omit, keyof Props>; diff --git a/src/component-library/Text/utils.ts b/src/component-library/Text/utils.ts index ea9034044d..fa16b5d428 100644 --- a/src/component-library/Text/utils.ts +++ b/src/component-library/Text/utils.ts @@ -6,13 +6,15 @@ const mapTextProps = ({ size, align, weight, + rows, ...props -}: T): Omit & StyledTextProps => ({ +}: T): Omit & StyledTextProps => ({ ...props, $color: color, $size: size, $weight: weight, - $align: align + $align: align, + $rows: rows }); export { mapTextProps }; diff --git a/src/component-library/TextLink/TextLink.style.tsx b/src/component-library/TextLink/TextLink.style.tsx index 4fdb3ccce7..e3bdf78580 100644 --- a/src/component-library/TextLink/TextLink.style.tsx +++ b/src/component-library/TextLink/TextLink.style.tsx @@ -1,15 +1,25 @@ import styled from 'styled-components'; -import { Colors } from '../utils/prop-types'; -import { resolveColor } from '../utils/theme'; +import { ArrowTopRightOnSquare } from '@/assets/icons'; + +import { theme } from '../theme'; +import { Colors, FontSize, FontWeight } from '../utils/prop-types'; +import { resolveColor, resolveHeight } from '../utils/theme'; type BaseTextLinkProps = { $color?: Colors; $underlined?: boolean; + $size?: FontSize; + $weight?: FontWeight; }; const BaseTextLink = styled.a` + display: inline-flex; + align-items: center; color: ${({ $color }) => resolveColor($color)}; + font-size: ${({ $size }) => $size && theme.text[$size]}; + line-height: ${({ $size }) => resolveHeight($size)}; + font-weight: ${({ $weight }) => $weight && theme.fontWeight[$weight]}; text-decoration: ${(props) => props.$underlined && 'underline'}; &:hover, @@ -18,4 +28,11 @@ const BaseTextLink = styled.a` } `; -export { BaseTextLink }; +const StyledIcon = styled(ArrowTopRightOnSquare)` + margin-left: ${theme.spacing.spacing2}; + width: 1em; + height: 1em; + color: inherit; +`; + +export { BaseTextLink, StyledIcon }; diff --git a/src/component-library/TextLink/TextLink.tsx b/src/component-library/TextLink/TextLink.tsx index 25fec25ee1..7bb6cb05ab 100644 --- a/src/component-library/TextLink/TextLink.tsx +++ b/src/component-library/TextLink/TextLink.tsx @@ -1,13 +1,16 @@ import { forwardRef } from 'react'; import { Link, LinkProps } from 'react-router-dom'; -import { Colors } from '../utils/prop-types'; -import { BaseTextLink } from './TextLink.style'; +import { Colors, FontSize, FontWeight } from '../utils/prop-types'; +import { BaseTextLink, StyledIcon } from './TextLink.style'; type Props = { color?: Colors; external?: boolean; underlined?: boolean; + size?: FontSize; + weight?: FontWeight; + icon?: boolean; }; type NativeAttrs = Omit; @@ -16,12 +19,26 @@ type TextLinkProps = Props & NativeAttrs; // TODO: merge this with CTALink const TextLink = forwardRef( - ({ color = 'primary', external, to, underlined, ...props }, ref): JSX.Element => { + ({ color = 'primary', external, to, underlined, size, weight, icon, children, ...props }, ref): JSX.Element => { const linkProps: TextLinkProps = external ? { to: { pathname: to as string }, target: '_blank', rel: 'noreferrer' } : { to }; - return ; + return ( + + {children} + {icon && } + + ); } ); diff --git a/src/component-library/TokenInput/TokenInput.style.tsx b/src/component-library/TokenInput/TokenInput.style.tsx index 3331c5b6d2..5ba46b0c61 100644 --- a/src/component-library/TokenInput/TokenInput.style.tsx +++ b/src/component-library/TokenInput/TokenInput.style.tsx @@ -89,11 +89,11 @@ const StyledListItemLabel = styled(Span)` const StyledList = styled(List)` overflow: auto; - padding: 0 ${theme.modal.body.paddingX} ${theme.modal.body.paddingY} ${theme.modal.body.paddingX}; + padding: 0 ${theme.spacing.spacing4} ${theme.spacing.spacing2} ${theme.spacing.spacing4}; `; const StyledListHeader = styled(Flex)` - padding: ${theme.modal.body.paddingY} ${theme.modal.body.paddingX}; + padding: ${theme.spacing.spacing2} ${theme.spacing.spacing4}; `; const StyledListTokenWrapper = styled(Flex)` diff --git a/src/component-library/Tooltip/Tooltip.tsx b/src/component-library/Tooltip/Tooltip.tsx index 6077f95860..7e83274bb8 100644 --- a/src/component-library/Tooltip/Tooltip.tsx +++ b/src/component-library/Tooltip/Tooltip.tsx @@ -6,14 +6,13 @@ import { AriaTooltipProps, TooltipTriggerProps as StatelyTooltipTriggerProps } f import React, { Children, cloneElement, HTMLAttributes, ReactElement, ReactNode, useRef } from 'react'; import { Span } from '../Text'; -import { theme } from '../theme'; import { Placement } from '../utils/prop-types'; import { StyledTooltip, StyledTooltipLabel, StyledTooltipTip } from './Tooltip.style'; // MEMO: https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/tooltip/src/TooltipTrigger.tsx#L22 const DEFAULT_OFFSET = -1; const DEFAULT_CROSS_OFFSET = 0; -const DEFAULT_DELAY = Number(theme.transition.default); +const DEFAULT_DELAY = 500; type Props = { label?: ReactNode; diff --git a/src/component-library/index.tsx b/src/component-library/index.tsx index d385b075d1..2a09fe612f 100644 --- a/src/component-library/index.tsx +++ b/src/component-library/index.tsx @@ -32,6 +32,17 @@ export type { ModalBodyProps, ModalDividerProps, ModalFooterProps, ModalHeaderPr export { Modal, ModalBody, ModalDivider, ModalFooter, ModalHeader } from './Modal'; export type { NumberInputProps } from './NumberInput'; export { NumberInput } from './NumberInput'; +export type { + PopoverBodyProps, + PopoverContentProps, + PopoverFooterProps, + PopoverHeaderProps, + PopoverProps, + PopoverTriggerProps +} from './Popover'; +export { Popover, PopoverBody, PopoverContent, PopoverFooter, PopoverHeader, PopoverTrigger } from './Popover'; +export type { ProgressBarProps } from './ProgressBar'; +export { ProgressBar } from './ProgressBar'; export type { SelectProps } from './Select'; export { Item, Select } from './Select'; export type { StackProps } from './Stack'; diff --git a/src/component-library/theme/theme.base.css b/src/component-library/theme/theme.base.css index 532086f98e..801f09e550 100644 --- a/src/component-library/theme/theme.base.css +++ b/src/component-library/theme/theme.base.css @@ -98,6 +98,7 @@ --spacing-12: 3rem; --spacing-14: 3.5rem; --spacing-16: 4rem; + --spacing-18: 4.5rem; --spacing-28: 7rem; --rounded-sm: 2px; diff --git a/src/component-library/theme/theme.ts b/src/component-library/theme/theme.ts index c7a21c8791..45f079bb66 100644 --- a/src/component-library/theme/theme.ts +++ b/src/component-library/theme/theme.ts @@ -16,7 +16,10 @@ const theme = { textSecondary: 'var(--colors-text-secondary)', textTertiary: 'var(--colors-text-tertiary)', bgPrimary: 'var(--colors-bg-primary)', - warn: `var(--colors-shared-red)` + warn: `var(--colors-shared-red)`, + error: 'var(--colors-error)', + warning: 'var(--colors-warning)', + success: 'var(--colors-success-darker)' }, font: { primary: 'var(--fonts-primary)' @@ -60,6 +63,7 @@ const theme = { spacing12: 'var(--spacing-12)', spacing14: 'var(--spacing-14)', spacing16: 'var(--spacing-16)', + spacing18: 'var(--spacing-18)', spacing28: 'var(--spacing-28)' }, rounded: { @@ -320,6 +324,9 @@ const theme = { } } }, + progressBar: { + bg: 'var(--colors-border)' + }, spinner: { determinate: { color: 'var(--colors-cta-primary)', @@ -381,8 +388,79 @@ const theme = { width: '5.625rem' } }, + dialog: { + small: { + width: '400px', + header: { + paddingTop: 'var(--spacing-4)', + paddingBottom: 'var(--spacing-2)', + paddingX: 'var(--spacing-4)', + padding: 'var(--spacing-4) var(--spacing-8) var(--spacing-2) var(--spacing-4)' + }, + divider: { + marginX: 'var(--spacing-4)', + marginBottom: 'var(--spacing-1)' + }, + body: { + paddingY: 'var(--spacing-2)', + paddingX: 'var(--spacing-4)' + }, + footer: { + paddingTop: 'var(--spacing-1)', + paddingBottom: 'var(--spacing-4)', + paddingX: 'var(--spacing-4)', + padding: 'var(--spacing-1) var(--spacing-4) var(--spacing-4)' + } + }, + medium: { + width: '32rem', + header: { + paddingY: 'var(--spacing-4)', + paddingX: 'var(--spacing-6)', + padding: 'var(--spacing-4) var(--spacing-8) var(--spacing-4) var(--spacing-6)' + }, + divider: { + marginX: 'var(--spacing-6)', + marginBottom: 'var(--spacing-2)' + }, + body: { + paddingY: 'var(--spacing-3)', + paddingX: 'var(--spacing-6)' + }, + footer: { + paddingTop: 'var(--spacing-4)', + paddingBottom: 'var(--spacing-6)', + paddingX: 'var(--spacing-6)', + padding: 'var(--spacing-4) var(--spacing-6) var(--spacing-6)' + } + }, + large: { + width: '32rem', + header: { + paddingY: 'var(--spacing-4)', + paddingX: 'var(--spacing-6)', + padding: 'var(--spacing-4) var(--spacing-8) var(--spacing-4) var(--spacing-6)' + }, + divider: { + marginX: 'var(--spacing-6)', + marginBottom: 'var(--spacing-2)' + }, + body: { + paddingY: 'var(--spacing-3)', + paddingX: 'var(--spacing-6)' + }, + footer: { + paddingTop: 'var(--spacing-4)', + paddingBottom: 'var(--spacing-6)', + paddingX: 'var(--spacing-6)', + padding: 'var(--spacing-4) var(--spacing-6) var(--spacing-6)' + } + }, + closeBtn: { + zIndex: 100 + } + }, modal: { - maxWidth: '32rem', maxHeight: 'calc(100vh - var(--spacing-12))', // TODO: z-index needs to be higher zIndex: 2, @@ -394,27 +472,6 @@ const theme = { exiting: 'opacity .1s cubic-bezier(0.5,0,1,1), visibility 0s linear .1s' } }, - header: { - paddingY: 'var(--spacing-4)', - paddingX: 'var(--spacing-6)', - paddingRight: 'var(--spacing-8)' - }, - divider: { - marginX: 'var(--spacing-6)', - marginBottom: 'var(--spacing-2)' - }, - body: { - paddingY: 'var(--spacing-3)', - paddingX: 'var(--spacing-6)' - }, - footer: { - paddingTop: 'var(--spacing-4)', - paddingBottom: 'var(--spacing-6)', - paddingX: 'var(--spacing-6)' - }, - closeBtn: { - zIndex: 100 - }, transition: { entering: 'transform .15s cubic-bezier(0,0,0.4,1) .1s, opacity .15s cubic-bezier(0,0,0.4,1)', exiting: 'opacity .1s cubic-bezier(0.5,0,1,1), visibility 0s linear, transform 0s linear .1s' diff --git a/src/component-library/utils/prop-types.ts b/src/component-library/utils/prop-types.ts index ee0db47dc0..2b5d4b629d 100644 --- a/src/component-library/utils/prop-types.ts +++ b/src/component-library/utils/prop-types.ts @@ -12,7 +12,8 @@ export const status = tuple('error', 'warning', 'success'); export const sizes = tuple('small', 'medium', 'large'); -export const colors = tuple('primary', 'secondary', 'tertiary'); +// TODO: add info +export const colors = tuple('primary', 'secondary', 'tertiary', 'success', 'warning', 'error'); export const justifyContent = tuple( 'flex-start', @@ -105,3 +106,5 @@ export type IconSize = keyof typeof theme.icon.sizes; export type Overflow = 'auto' | 'hidden' | 'scroll' | 'visible' | 'inherit'; export type BreakPoints = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +export type ProgressBarColors = 'default' | 'red'; diff --git a/src/component-library/utils/theme.ts b/src/component-library/utils/theme.ts index d9ff873a53..7f597b8379 100644 --- a/src/component-library/utils/theme.ts +++ b/src/component-library/utils/theme.ts @@ -9,6 +9,12 @@ const resolveColor = (color: Colors | undefined): string => { return theme.colors.textSecondary; case 'tertiary': return theme.colors.textTertiary; + case 'success': + return theme.colors.success; + case 'warning': + return theme.colors.warning; + case 'error': + return theme.colors.error; default: return theme.colors.textPrimary; } diff --git a/src/components/AccountSelect/AccountSelect.style.tsx b/src/components/AccountSelect/AccountSelect.style.tsx index 850c26e0fc..ea962fc8e0 100644 --- a/src/components/AccountSelect/AccountSelect.style.tsx +++ b/src/components/AccountSelect/AccountSelect.style.tsx @@ -49,7 +49,8 @@ const StyledAccountLabelName = styled(StyledAccountLabelAddress) Date: Wed, 31 May 2023 09:37:50 +0100 Subject: [PATCH 021/241] fix: Dialog, Modal and Popover (#1245) --- src/component-library/Dialog/Dialog.style.tsx | 1 + src/component-library/Modal/Modal.style.tsx | 1 + src/component-library/Popover/Popover.style.tsx | 6 ++++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/component-library/Dialog/Dialog.style.tsx b/src/component-library/Dialog/Dialog.style.tsx index 5f482bc6c5..59c0278559 100644 --- a/src/component-library/Dialog/Dialog.style.tsx +++ b/src/component-library/Dialog/Dialog.style.tsx @@ -16,6 +16,7 @@ const StyledDialog = styled.section` border: ${theme.border.default}; border-radius: ${theme.rounded.md}; color: ${theme.colors.textPrimary}; + max-width: 100%; width: ${({ $size }) => theme.dialog[$size].width}; display: flex; flex-direction: column; diff --git a/src/component-library/Modal/Modal.style.tsx b/src/component-library/Modal/Modal.style.tsx index 9a222b8a7f..250dad20f8 100644 --- a/src/component-library/Modal/Modal.style.tsx +++ b/src/component-library/Modal/Modal.style.tsx @@ -36,6 +36,7 @@ const StyledWrapper = styled.div` `; const StyledModal = styled.div` + max-width: calc(100% - ${theme.spacing.spacing12}); max-height: ${({ $isCentered }) => $isCentered && theme.modal.maxHeight}; margin: ${({ $isCentered }) => ($isCentered ? 0 : theme.spacing.spacing16)} ${theme.spacing.spacing6}; diff --git a/src/component-library/Popover/Popover.style.tsx b/src/component-library/Popover/Popover.style.tsx index 40db6c8b91..26fe2d5f73 100644 --- a/src/component-library/Popover/Popover.style.tsx +++ b/src/component-library/Popover/Popover.style.tsx @@ -1,6 +1,7 @@ import styled from 'styled-components'; import { getOverlayPlacementCSS, overlayCSS } from '../css/overlay'; +import { theme } from '../theme'; import { Placement } from '../utils/prop-types'; type StyledPopoverProps = { @@ -13,8 +14,9 @@ const StyledPopover = styled.div` flex-direction: column; box-sizing: border-box; - min-width: 32px; - min-height: 32px; + min-width: ${theme.spacing.spacing8}; + min-height: ${theme.spacing.spacing8}; + max-width: calc(100% - ${theme.spacing.spacing8}); position: absolute; From c845c54f6d550ccaf038a384cbe3b7c9e628330e Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Wed, 31 May 2023 11:28:26 +0100 Subject: [PATCH 022/241] chore: rename strategies feature (#1247) --- .env.dev | 2 +- src/App.tsx | 10 +++--- src/assets/locales/en/translation.json | 4 +-- src/lib/form/schemas/earn-strategy.ts | 21 ----------- src/lib/form/schemas/index.ts | 2 +- src/lib/form/schemas/strategy.ts | 21 +++++++++++ src/pages/EarnStrategies/EarnStrategies.tsx | 21 ----------- .../EarnStrategyDepositForm/index.ts | 1 - .../EarnStrategyWithdrawalForm/index.ts | 1 - .../components/EarnStrategyForm/index.ts | 1 - src/pages/EarnStrategies/components/index.ts | 1 - src/pages/EarnStrategies/index.tsx | 3 -- src/pages/EarnStrategies/types/form.ts | 18 ---------- .../Strategies.style.tsx} | 4 +-- src/pages/Strategies/Strategies.tsx | 21 +++++++++++ .../StrategyDepositForm.tsx} | 28 +++++++-------- .../StrategyForm/StrategyDepositForm/index.ts | 1 + .../StrategyForm/StrategyForm.style.tsx} | 6 ++-- .../components/StrategyForm/StrategyForm.tsx} | 30 ++++++++-------- .../StrategyForm/StrategyFormFees.tsx} | 8 ++--- .../StrategyWithdrawalForm.tsx} | 36 +++++++++---------- .../StrategyWithdrawalForm/index.ts | 1 + .../components/StrategyForm/index.ts | 1 + src/pages/Strategies/components/index.ts | 1 + src/pages/Strategies/index.tsx | 3 ++ src/pages/Strategies/types/form.ts | 13 +++++++ .../SidebarContent/Navigation/index.tsx | 17 +++------ src/utils/constants/links.ts | 2 +- src/utils/hooks/use-feature-flag.ts | 4 +-- 29 files changed, 135 insertions(+), 147 deletions(-) delete mode 100644 src/lib/form/schemas/earn-strategy.ts create mode 100644 src/lib/form/schemas/strategy.ts delete mode 100644 src/pages/EarnStrategies/EarnStrategies.tsx delete mode 100644 src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/index.ts delete mode 100644 src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/index.ts delete mode 100644 src/pages/EarnStrategies/components/EarnStrategyForm/index.ts delete mode 100644 src/pages/EarnStrategies/components/index.ts delete mode 100644 src/pages/EarnStrategies/index.tsx delete mode 100644 src/pages/EarnStrategies/types/form.ts rename src/pages/{EarnStrategies/EarnStrategies.style.tsx => Strategies/Strategies.style.tsx} (74%) create mode 100644 src/pages/Strategies/Strategies.tsx rename src/pages/{EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/EarnStrategyDepositForm.tsx => Strategies/components/StrategyForm/StrategyDepositForm/StrategyDepositForm.tsx} (69%) create mode 100644 src/pages/Strategies/components/StrategyForm/StrategyDepositForm/index.ts rename src/pages/{EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.style.tsx => Strategies/components/StrategyForm/StrategyForm.style.tsx} (79%) rename src/pages/{EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.tsx => Strategies/components/StrategyForm/StrategyForm.tsx} (56%) rename src/pages/{EarnStrategies/components/EarnStrategyForm/EarnStrategyFormFees.tsx => Strategies/components/StrategyForm/StrategyFormFees.tsx} (81%) rename src/pages/{EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/EarnStrategyWithdrawalForm.tsx => Strategies/components/StrategyForm/StrategyWithdrawalForm/StrategyWithdrawalForm.tsx} (73%) create mode 100644 src/pages/Strategies/components/StrategyForm/StrategyWithdrawalForm/index.ts create mode 100644 src/pages/Strategies/components/StrategyForm/index.ts create mode 100644 src/pages/Strategies/components/index.ts create mode 100644 src/pages/Strategies/index.tsx create mode 100644 src/pages/Strategies/types/form.ts diff --git a/.env.dev b/.env.dev index 95c64b470e..5c4633047e 100644 --- a/.env.dev +++ b/.env.dev @@ -4,7 +4,7 @@ REACT_APP_FEATURE_FLAG_LENDING=enabled REACT_APP_FEATURE_FLAG_AMM=enabled REACT_APP_FEATURE_FLAG_WALLET=enabled REACT_APP_FEATURE_FLAG_BANXA=enabled -REACT_APP_FEATURE_FLAG_EARN_STRATEGIES=enabled +REACT_APP_FEATURE_FLAG_STRATEGIES=enabled /* DEVELOPMENT */ diff --git a/src/App.tsx b/src/App.tsx index 463f6f1528..a94a8a4eb2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,7 +26,7 @@ import TestnetBanner from './legacy-components/TestnetBanner'; import { FeatureFlags, useFeatureFlag } from './utils/hooks/use-feature-flag'; const Bridge = React.lazy(() => import(/* webpackChunkName: 'bridge' */ '@/pages/Bridge')); -const EarnStrategies = React.lazy(() => import(/* webpackChunkName: 'earn-strategies' */ '@/pages/EarnStrategies')); +const Strategies = React.lazy(() => import(/* webpackChunkName: 'strategies' */ '@/pages/Strategies')); const Transfer = React.lazy(() => import(/* webpackChunkName: 'transfer' */ '@/pages/Transfer')); const Transactions = React.lazy(() => import(/* webpackChunkName: 'transactions' */ '@/pages/Transactions')); const TX = React.lazy(() => import(/* webpackChunkName: 'tx' */ '@/pages/TX')); @@ -51,7 +51,7 @@ const App = (): JSX.Element => { const isLendingEnabled = useFeatureFlag(FeatureFlags.LENDING); const isAMMEnabled = useFeatureFlag(FeatureFlags.AMM); const isWalletEnabled = useFeatureFlag(FeatureFlags.WALLET); - const isEarnStrategiesEnabled = useFeatureFlag(FeatureFlags.EARN_STRATEGIES); + const isStrategiesEnabled = useFeatureFlag(FeatureFlags.STRATEGIES); // Loads the connection to the faucet - only for testnet purposes const loadFaucet = React.useCallback(async (): Promise => { @@ -214,9 +214,9 @@ const App = (): JSX.Element => { )} - {isEarnStrategiesEnabled && ( - - + {isStrategiesEnabled && ( + + )} diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index defab36d41..9e03c16050 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -75,7 +75,7 @@ "issue": "Issue", "redeem": "Redeem", "nav_bridge": "Bridge", - "nav_earn_strategies": "Earn Strategies", + "nav_strategies": "Strategies", "nav_transfer": "Transfer", "nav_lending": "Lending", "nav_swap": "Swap", @@ -633,7 +633,7 @@ "available_to_stake": "Available to stake", "voting_power_governance": "Voting Power {{token}}" }, - "earn_strategy": { + "strategy": { "withdraw_rewards_in_wrapped": "Withdraw rewards in {{wrappedCurrencySymbol}}:", "update_position": "Update position" } diff --git a/src/lib/form/schemas/earn-strategy.ts b/src/lib/form/schemas/earn-strategy.ts deleted file mode 100644 index 75dd1f1b15..0000000000 --- a/src/lib/form/schemas/earn-strategy.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { EarnStrategyFormType } from '@/pages/EarnStrategies/types/form'; - -import yup, { MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; - -type EarnStrategyValidationParams = MaxAmountValidationParams & MinAmountValidationParams; - -const earnStrategySchema = ( - earnStrategyFormType: EarnStrategyFormType, - params: EarnStrategyValidationParams -): yup.ObjectSchema => { - return yup.object().shape({ - [earnStrategyFormType]: yup - .string() - .requiredAmount(earnStrategyFormType) - .maxAmount(params) - .minAmount(params, earnStrategyFormType) - }); -}; - -export { earnStrategySchema }; -export type { EarnStrategyValidationParams }; diff --git a/src/lib/form/schemas/index.ts b/src/lib/form/schemas/index.ts index 87a172bab2..fac035a489 100644 --- a/src/lib/form/schemas/index.ts +++ b/src/lib/form/schemas/index.ts @@ -5,9 +5,9 @@ export type { WithdrawLiquidityPoolValidationParams } from './amm'; export { depositLiquidityPoolSchema, WITHDRAW_LIQUIDITY_POOL_FIELD, withdrawLiquidityPoolSchema } from './amm'; -export { earnStrategySchema } from './earn-strategy'; export type { LoanFormData, LoanValidationParams } from './loans'; export { loanSchema } from './loans'; +export { StrategySchema } from './strategy'; export type { SwapFormData, SwapValidationParams } from './swap'; export { SWAP_INPUT_AMOUNT_FIELD, diff --git a/src/lib/form/schemas/strategy.ts b/src/lib/form/schemas/strategy.ts new file mode 100644 index 0000000000..66a38c8ce0 --- /dev/null +++ b/src/lib/form/schemas/strategy.ts @@ -0,0 +1,21 @@ +import { StrategyFormType } from '@/pages/Strategies/types/form'; + +import yup, { MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; + +type StrategyValidationParams = MaxAmountValidationParams & MinAmountValidationParams; + +const StrategySchema = ( + StrategyFormType: StrategyFormType, + params: StrategyValidationParams +): yup.ObjectSchema => { + return yup.object().shape({ + [StrategyFormType]: yup + .string() + .requiredAmount(StrategyFormType) + .maxAmount(params) + .minAmount(params, StrategyFormType) + }); +}; + +export { StrategySchema }; +export type { StrategyValidationParams }; diff --git a/src/pages/EarnStrategies/EarnStrategies.tsx b/src/pages/EarnStrategies/EarnStrategies.tsx deleted file mode 100644 index bd1eac8fdc..0000000000 --- a/src/pages/EarnStrategies/EarnStrategies.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { withErrorBoundary } from 'react-error-boundary'; - -import ErrorFallback from '@/legacy-components/ErrorFallback'; - -import { EarnStrategyForm } from './components/EarnStrategyForm'; -import { StyledEarnStrategiesLayout } from './EarnStrategies.style'; - -const EarnStrategies = (): JSX.Element => { - return ( - - - - ); -}; - -export default withErrorBoundary(EarnStrategies, { - FallbackComponent: ErrorFallback, - onReset: () => { - window.location.reload(); - } -}); diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/index.ts b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/index.ts deleted file mode 100644 index aaedb7d4bb..0000000000 --- a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { EarnStrategyDepositForm } from './EarnStrategyDepositForm'; diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/index.ts b/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/index.ts deleted file mode 100644 index aa5c13a062..0000000000 --- a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { EarnStrategyWithdrawalForm } from './EarnStrategyWithdrawalForm'; diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/index.ts b/src/pages/EarnStrategies/components/EarnStrategyForm/index.ts deleted file mode 100644 index dd8d107bb2..0000000000 --- a/src/pages/EarnStrategies/components/EarnStrategyForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { EarnStrategyForm } from './EarnStrategyForm'; diff --git a/src/pages/EarnStrategies/components/index.ts b/src/pages/EarnStrategies/components/index.ts deleted file mode 100644 index dd8d107bb2..0000000000 --- a/src/pages/EarnStrategies/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { EarnStrategyForm } from './EarnStrategyForm'; diff --git a/src/pages/EarnStrategies/index.tsx b/src/pages/EarnStrategies/index.tsx deleted file mode 100644 index 7a73a2c641..0000000000 --- a/src/pages/EarnStrategies/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import EarnStrategies from './EarnStrategies'; - -export default EarnStrategies; diff --git a/src/pages/EarnStrategies/types/form.ts b/src/pages/EarnStrategies/types/form.ts deleted file mode 100644 index c912d7e549..0000000000 --- a/src/pages/EarnStrategies/types/form.ts +++ /dev/null @@ -1,18 +0,0 @@ -type EarnStrategyFormType = 'deposit' | 'withdraw'; -type EarnStrategyRiskVariant = 'low' | 'high'; - -interface EarnStrategyDepositFormData { - deposit?: string; -} - -interface EarnStrategyWithdrawalFormData { - withdraw?: string; - withdrawAsWrapped?: boolean; -} - -export type { - EarnStrategyDepositFormData, - EarnStrategyFormType, - EarnStrategyRiskVariant, - EarnStrategyWithdrawalFormData -}; diff --git a/src/pages/EarnStrategies/EarnStrategies.style.tsx b/src/pages/Strategies/Strategies.style.tsx similarity index 74% rename from src/pages/EarnStrategies/EarnStrategies.style.tsx rename to src/pages/Strategies/Strategies.style.tsx index 8bc1b94263..a9bf6f17ea 100644 --- a/src/pages/EarnStrategies/EarnStrategies.style.tsx +++ b/src/pages/Strategies/Strategies.style.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; import { theme } from '@/component-library'; -const StyledEarnStrategiesLayout = styled.div` +const StyledStrategiesLayout = styled.div` display: grid; gap: ${theme.spacing.spacing6}; @media (min-width: 80em) { @@ -10,4 +10,4 @@ const StyledEarnStrategiesLayout = styled.div` padding: ${theme.spacing.spacing6}; `; -export { StyledEarnStrategiesLayout }; +export { StyledStrategiesLayout }; diff --git a/src/pages/Strategies/Strategies.tsx b/src/pages/Strategies/Strategies.tsx new file mode 100644 index 0000000000..88c8b90357 --- /dev/null +++ b/src/pages/Strategies/Strategies.tsx @@ -0,0 +1,21 @@ +import { withErrorBoundary } from 'react-error-boundary'; + +import ErrorFallback from '@/legacy-components/ErrorFallback'; + +import { StrategyForm } from './components/StrategyForm'; +import { StyledStrategiesLayout } from './Strategies.style'; + +const Strategies = (): JSX.Element => { + return ( + + + + ); +}; + +export default withErrorBoundary(Strategies, { + FallbackComponent: ErrorFallback, + onReset: () => { + window.location.reload(); + } +}); diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/EarnStrategyDepositForm.tsx b/src/pages/Strategies/components/StrategyForm/StrategyDepositForm/StrategyDepositForm.tsx similarity index 69% rename from src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/EarnStrategyDepositForm.tsx rename to src/pages/Strategies/components/StrategyForm/StrategyDepositForm/StrategyDepositForm.tsx index b0939fd093..ab318185af 100644 --- a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyDepositForm/EarnStrategyDepositForm.tsx +++ b/src/pages/Strategies/components/StrategyForm/StrategyDepositForm/StrategyDepositForm.tsx @@ -6,24 +6,24 @@ import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/comm import { TokenInput } from '@/component-library'; import { AuthCTA } from '@/components'; import { TRANSACTION_FEE_AMOUNT, WRAPPED_TOKEN, WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains'; -import { earnStrategySchema, isFormDisabled, useForm } from '@/lib/form'; +import { isFormDisabled, StrategySchema, useForm } from '@/lib/form'; import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; import { useTransaction } from '@/utils/hooks/transaction'; -import { EarnStrategyDepositFormData } from '../../../types/form'; -import { EarnStrategyFormBaseProps } from '../EarnStrategyForm'; -import { StyledEarnStrategyFormContent } from '../EarnStrategyForm.style'; -import { EarnStrategyFormFees } from '../EarnStrategyFormFees'; +import { StrategyDepositFormData } from '../../../types/form'; +import { StrategyFormBaseProps } from '../StrategyForm'; +import { StyledStrategyFormContent } from '../StrategyForm.style'; +import { StrategyFormFees } from '../StrategyFormFees'; -const EarnStrategyDepositForm = ({ riskVariant, hasActiveStrategy }: EarnStrategyFormBaseProps): JSX.Element => { +const StrategyDepositForm = ({ riskVariant, hasActiveStrategy }: StrategyFormBaseProps): JSX.Element => { const { getAvailableBalance } = useGetBalances(); const prices = useGetPrices(); const { t } = useTranslation(); // TODO: add transaction const transaction = useTransaction(); - const handleSubmit = (data: EarnStrategyDepositFormData) => { + const handleSubmit = (data: StrategyDepositFormData) => { // TODO: Execute transaction with params // transaction.execute(); console.log(`transaction should be executed with parameters: ${data}, ${riskVariant}`); @@ -32,9 +32,9 @@ const EarnStrategyDepositForm = ({ riskVariant, hasActiveStrategy }: EarnStrateg const minAmount = newMonetaryAmount(1, WRAPPED_TOKEN); const maxDepositAmount = getAvailableBalance(WRAPPED_TOKEN_SYMBOL) || newMonetaryAmount(0, WRAPPED_TOKEN); - const form = useForm({ + const form = useForm({ initialValues: { deposit: '' }, - validationSchema: earnStrategySchema('deposit', { maxAmount: maxDepositAmount, minAmount }), + validationSchema: StrategySchema('deposit', { maxAmount: maxDepositAmount, minAmount }), onSubmit: handleSubmit }); @@ -44,7 +44,7 @@ const EarnStrategyDepositForm = ({ riskVariant, hasActiveStrategy }: EarnStrateg return (
- + - + - {hasActiveStrategy ? t('earn_strategy.update_position') : t('deposit')} + {hasActiveStrategy ? t('strategy.update_position') : t('deposit')} - +
); }; -export { EarnStrategyDepositForm }; +export { StrategyDepositForm }; diff --git a/src/pages/Strategies/components/StrategyForm/StrategyDepositForm/index.ts b/src/pages/Strategies/components/StrategyForm/StrategyDepositForm/index.ts new file mode 100644 index 0000000000..3cf5b84bea --- /dev/null +++ b/src/pages/Strategies/components/StrategyForm/StrategyDepositForm/index.ts @@ -0,0 +1 @@ +export { StrategyDepositForm } from './StrategyDepositForm'; diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.style.tsx b/src/pages/Strategies/components/StrategyForm/StrategyForm.style.tsx similarity index 79% rename from src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.style.tsx rename to src/pages/Strategies/components/StrategyForm/StrategyForm.style.tsx index 4179c1e7a4..59c4bd98a5 100644 --- a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.style.tsx +++ b/src/pages/Strategies/components/StrategyForm/StrategyForm.style.tsx @@ -2,7 +2,7 @@ import styled from 'styled-components'; import { Dl, Flex, theme } from '@/component-library'; -const StyledEarnStrategyForm = styled(Flex)` +const StyledStrategyForm = styled(Flex)` margin-top: ${theme.spacing.spacing8}; background: ${theme.colors.bgPrimary}; padding: ${theme.spacing.spacing6}; @@ -16,7 +16,7 @@ const StyledDl = styled(Dl)` border-radius: ${theme.rounded.rg}; `; -const StyledEarnStrategyFormContent = styled(Flex)` +const StyledStrategyFormContent = styled(Flex)` margin-top: ${theme.spacing.spacing8}; flex-direction: column; gap: ${theme.spacing.spacing8}; @@ -31,4 +31,4 @@ const StyledSwitchLabel = styled('label')` font-weight: ${theme.fontWeight.bold}; `; -export { StyledDl, StyledEarnStrategyForm, StyledEarnStrategyFormContent, StyledSwitchLabel }; +export { StyledDl, StyledStrategyForm, StyledStrategyFormContent, StyledSwitchLabel }; diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.tsx b/src/pages/Strategies/components/StrategyForm/StrategyForm.tsx similarity index 56% rename from src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.tsx rename to src/pages/Strategies/components/StrategyForm/StrategyForm.tsx index 7f4ba377d5..4dee2206fa 100644 --- a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyForm.tsx +++ b/src/pages/Strategies/components/StrategyForm/StrategyForm.tsx @@ -3,20 +3,20 @@ import { newMonetaryAmount } from '@interlay/interbtc-api'; import { Tabs, TabsItem } from '@/component-library'; import { WRAPPED_TOKEN } from '@/config/relay-chains'; -import { EarnStrategyFormType, EarnStrategyRiskVariant } from '../../types/form'; -import { EarnStrategyDepositForm } from './EarnStrategyDepositForm'; -import { StyledEarnStrategyForm } from './EarnStrategyForm.style'; -import { EarnStrategyWithdrawalForm } from './EarnStrategyWithdrawalForm'; +import { StrategyFormType, StrategyRiskVariant } from '../../types/form'; +import { StrategyDepositForm } from './StrategyDepositForm'; +import { StyledStrategyForm } from './StrategyForm.style'; +import { StrategyWithdrawalForm } from './StrategyWithdrawalForm'; -interface EarnStrategyFormProps { - riskVariant: EarnStrategyRiskVariant; +interface StrategyFormProps { + riskVariant: StrategyRiskVariant; } -interface EarnStrategyFormBaseProps extends EarnStrategyFormProps { +interface StrategyFormBaseProps extends StrategyFormProps { hasActiveStrategy: boolean | undefined; } -type TabData = { type: EarnStrategyFormType; title: string }; +type TabData = { type: StrategyFormType; title: string }; const tabs: Array = [ { @@ -29,21 +29,21 @@ const tabs: Array = [ } ]; -const EarnStrategyForm = ({ riskVariant }: EarnStrategyFormProps): JSX.Element => { +const StrategyForm = ({ riskVariant }: StrategyFormProps): JSX.Element => { // TODO: replace with actually withdrawable amount once we know how to get that information, // for now it's statically set for display purposes const maxWithdrawableAmount = newMonetaryAmount(1.337, WRAPPED_TOKEN, true); const hasActiveStrategy = maxWithdrawableAmount && !maxWithdrawableAmount.isZero(); return ( - + {tabs.map(({ type, title }) => ( {type === 'deposit' ? ( - + ) : ( - ))} - + ); }; -export { EarnStrategyForm }; -export type { EarnStrategyFormBaseProps, EarnStrategyFormProps }; +export { StrategyForm }; +export type { StrategyFormBaseProps, StrategyFormProps }; diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyFormFees.tsx b/src/pages/Strategies/components/StrategyForm/StrategyFormFees.tsx similarity index 81% rename from src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyFormFees.tsx rename to src/pages/Strategies/components/StrategyForm/StrategyFormFees.tsx index 007867f302..13fd333592 100644 --- a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyFormFees.tsx +++ b/src/pages/Strategies/components/StrategyForm/StrategyFormFees.tsx @@ -7,13 +7,13 @@ import { Dd, DlGroup, Dt } from '@/component-library'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; -import { StyledDl } from './EarnStrategyForm.style'; +import { StyledDl } from './StrategyForm.style'; -interface EarnStrategyFormFeesProps { +interface StrategyFormFeesProps { amount: MonetaryAmount; } -const EarnStrategyFormFees = ({ amount }: EarnStrategyFormFeesProps): JSX.Element => { +const StrategyFormFees = ({ amount }: StrategyFormFeesProps): JSX.Element => { const prices = useGetPrices(); const { t } = useTranslation(); @@ -32,4 +32,4 @@ const EarnStrategyFormFees = ({ amount }: EarnStrategyFormFeesProps): JSX.Elemen ); }; -export { EarnStrategyFormFees }; +export { StrategyFormFees }; diff --git a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/EarnStrategyWithdrawalForm.tsx b/src/pages/Strategies/components/StrategyForm/StrategyWithdrawalForm/StrategyWithdrawalForm.tsx similarity index 73% rename from src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/EarnStrategyWithdrawalForm.tsx rename to src/pages/Strategies/components/StrategyForm/StrategyWithdrawalForm/StrategyWithdrawalForm.tsx index 606fec950e..bcf30df624 100644 --- a/src/pages/EarnStrategies/components/EarnStrategyForm/EarnStrategyWithdrawalForm/EarnStrategyWithdrawalForm.tsx +++ b/src/pages/Strategies/components/StrategyForm/StrategyWithdrawalForm/StrategyWithdrawalForm.tsx @@ -12,16 +12,16 @@ import { WRAPPED_TOKEN, WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains'; -import { earnStrategySchema, isFormDisabled, useForm } from '@/lib/form'; +import { isFormDisabled, StrategySchema, useForm } from '@/lib/form'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; import { useTransaction } from '@/utils/hooks/transaction'; -import { EarnStrategyWithdrawalFormData } from '../../../types/form'; -import { EarnStrategyFormBaseProps } from '../EarnStrategyForm'; -import { StyledEarnStrategyFormContent, StyledSwitchLabel } from '../EarnStrategyForm.style'; -import { EarnStrategyFormFees } from '../EarnStrategyFormFees'; +import { StrategyWithdrawalFormData } from '../../../types/form'; +import { StrategyFormBaseProps } from '../StrategyForm'; +import { StyledStrategyFormContent, StyledSwitchLabel } from '../StrategyForm.style'; +import { StrategyFormFees } from '../StrategyFormFees'; -interface EarnStrategyWithdrawalFormProps extends EarnStrategyFormBaseProps { +interface StrategyWithdrawalFormProps extends StrategyFormBaseProps { maxWithdrawableAmount: MonetaryAmount | undefined; } @@ -33,7 +33,7 @@ const calculateReceivableAssets = ( return [amountToWithdraw]; } // TODO: do some magic calculation to get the receivable assets based on input amount here, - // or better move this computation to earn-strategy hook + // or better move this computation to strategy hook const mockedReceivableAssets = [ amountToWithdraw.div(1.2), newMonetaryAmount(amountToWithdraw.toBig().mul(213.2), RELAY_CHAIN_NATIVE_TOKEN, true) @@ -42,17 +42,17 @@ const calculateReceivableAssets = ( return mockedReceivableAssets; }; -const EarnStrategyWithdrawalForm = ({ +const StrategyWithdrawalForm = ({ riskVariant, hasActiveStrategy, maxWithdrawableAmount -}: EarnStrategyWithdrawalFormProps): JSX.Element => { +}: StrategyWithdrawalFormProps): JSX.Element => { const { t } = useTranslation(); const prices = useGetPrices(); // TODO: add transaction const transaction = useTransaction(); - const handleSubmit = (data: EarnStrategyWithdrawalFormData) => { + const handleSubmit = (data: StrategyWithdrawalFormData) => { // TODO: Execute transaction with params // transaction.execute() console.log(data, riskVariant); @@ -60,9 +60,9 @@ const EarnStrategyWithdrawalForm = ({ const minAmount = newMonetaryAmount(1, WRAPPED_TOKEN); - const form = useForm({ + const form = useForm({ initialValues: { withdraw: '', withdrawAsWrapped: true }, - validationSchema: earnStrategySchema('withdraw', { + validationSchema: StrategySchema('withdraw', { maxAmount: maxWithdrawableAmount || newMonetaryAmount(0, WRAPPED_TOKEN), minAmount }), @@ -76,7 +76,7 @@ const EarnStrategyWithdrawalForm = ({ return (
- + - {t('earn_strategy.withdraw_rewards_in_wrapped', { wrappedCurrencySymbol: WRAPPED_TOKEN_SYMBOL })}{' '} + {t('strategy.withdraw_rewards_in_wrapped', { wrappedCurrencySymbol: WRAPPED_TOKEN_SYMBOL })}{' '} - + - {hasActiveStrategy ? t('earn_strategy.update_position') : t('withdraw')} + {hasActiveStrategy ? t('strategy.update_position') : t('withdraw')} - +
); }; -export { EarnStrategyWithdrawalForm }; +export { StrategyWithdrawalForm }; diff --git a/src/pages/Strategies/components/StrategyForm/StrategyWithdrawalForm/index.ts b/src/pages/Strategies/components/StrategyForm/StrategyWithdrawalForm/index.ts new file mode 100644 index 0000000000..26ff40f62c --- /dev/null +++ b/src/pages/Strategies/components/StrategyForm/StrategyWithdrawalForm/index.ts @@ -0,0 +1 @@ +export { StrategyWithdrawalForm } from './StrategyWithdrawalForm'; diff --git a/src/pages/Strategies/components/StrategyForm/index.ts b/src/pages/Strategies/components/StrategyForm/index.ts new file mode 100644 index 0000000000..2088c63879 --- /dev/null +++ b/src/pages/Strategies/components/StrategyForm/index.ts @@ -0,0 +1 @@ +export { StrategyForm } from './StrategyForm'; diff --git a/src/pages/Strategies/components/index.ts b/src/pages/Strategies/components/index.ts new file mode 100644 index 0000000000..2088c63879 --- /dev/null +++ b/src/pages/Strategies/components/index.ts @@ -0,0 +1 @@ +export { StrategyForm } from './StrategyForm'; diff --git a/src/pages/Strategies/index.tsx b/src/pages/Strategies/index.tsx new file mode 100644 index 0000000000..617a2532db --- /dev/null +++ b/src/pages/Strategies/index.tsx @@ -0,0 +1,3 @@ +import Strategies from './Strategies'; + +export default Strategies; diff --git a/src/pages/Strategies/types/form.ts b/src/pages/Strategies/types/form.ts new file mode 100644 index 0000000000..c6b5d6bad5 --- /dev/null +++ b/src/pages/Strategies/types/form.ts @@ -0,0 +1,13 @@ +type StrategyFormType = 'deposit' | 'withdraw'; +type StrategyRiskVariant = 'low' | 'high'; + +interface StrategyDepositFormData { + deposit?: string; +} + +interface StrategyWithdrawalFormData { + withdraw?: string; + withdrawAsWrapped?: boolean; +} + +export type { StrategyDepositFormData, StrategyFormType, StrategyRiskVariant, StrategyWithdrawalFormData }; diff --git a/src/parts/Sidebar/SidebarContent/Navigation/index.tsx b/src/parts/Sidebar/SidebarContent/Navigation/index.tsx index 54dabf7446..065c5be8e2 100644 --- a/src/parts/Sidebar/SidebarContent/Navigation/index.tsx +++ b/src/parts/Sidebar/SidebarContent/Navigation/index.tsx @@ -70,7 +70,7 @@ const Navigation = ({ const isLendingEnabled = useFeatureFlag(FeatureFlags.LENDING); const isAMMEnabled = useFeatureFlag(FeatureFlags.AMM); const isWalletEnabled = useFeatureFlag(FeatureFlags.WALLET); - const isEarnStrategiesEnabled = useFeatureFlag(FeatureFlags.EARN_STRATEGIES); + const isStrategiesEnabled = useFeatureFlag(FeatureFlags.STRATEGIES); const NAVIGATION_ITEMS = React.useMemo( () => [ @@ -81,10 +81,10 @@ const Navigation = ({ disabled: !isWalletEnabled }, { - name: 'nav_earn_strategies', - link: PAGES.EARN_STRATEGIES, + name: 'nav_strategies', + link: PAGES.STRATEGIES, icon: BanknotesIcon, - disabled: !isEarnStrategiesEnabled + disabled: !isStrategiesEnabled }, { name: 'nav_bridge', @@ -200,14 +200,7 @@ const Navigation = ({ } } ], - [ - isWalletEnabled, - isEarnStrategiesEnabled, - isLendingEnabled, - isAMMEnabled, - selectedAccount?.address, - vaultClientLoaded - ] + [isWalletEnabled, isStrategiesEnabled, isLendingEnabled, isAMMEnabled, selectedAccount?.address, vaultClientLoaded] ); return ( diff --git a/src/utils/constants/links.ts b/src/utils/constants/links.ts index fb189728c4..c6cd038371 100644 --- a/src/utils/constants/links.ts +++ b/src/utils/constants/links.ts @@ -12,7 +12,7 @@ const URL_PARAMETERS = Object.freeze({ const PAGES = Object.freeze({ HOME: '/', BRIDGE: '/bridge', - EARN_STRATEGIES: '/earn-strategies', + STRATEGIES: '/strategies', TRANSFER: '/transfer', TRANSACTIONS: '/transactions', TX: '/tx', diff --git a/src/utils/hooks/use-feature-flag.ts b/src/utils/hooks/use-feature-flag.ts index 917bd3b3c8..94a1f979f0 100644 --- a/src/utils/hooks/use-feature-flag.ts +++ b/src/utils/hooks/use-feature-flag.ts @@ -3,7 +3,7 @@ enum FeatureFlags { AMM = 'amm', WALLET = 'wallet', BANXA = 'banxa', - EARN_STRATEGIES = 'earn-strategies', + STRATEGIES = 'strategies', GEOBLOCK = 'geoblock' } @@ -12,7 +12,7 @@ const featureFlags: Record = { [FeatureFlags.AMM]: process.env.REACT_APP_FEATURE_FLAG_AMM, [FeatureFlags.WALLET]: process.env.REACT_APP_FEATURE_FLAG_WALLET, [FeatureFlags.BANXA]: process.env.REACT_APP_FEATURE_FLAG_BANXA, - [FeatureFlags.EARN_STRATEGIES]: process.env.REACT_APP_FEATURE_FLAG_EARN_STRATEGIES, + [FeatureFlags.STRATEGIES]: process.env.REACT_APP_FEATURE_FLAG_EARN_STRATEGIES, [FeatureFlags.GEOBLOCK]: process.env.REACT_APP_FEATURE_FLAG_GEOBLOCK }; From 64716ee03cb395576b79e250e5afe57306fad418 Mon Sep 17 00:00:00 2001 From: Thomas Jeatt Date: Wed, 31 May 2023 11:29:10 +0100 Subject: [PATCH 023/241] chore: release v2.32.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af4e4260e6..581d654255 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interbtc-ui", - "version": "2.32.5", + "version": "2.32.6", "private": true, "dependencies": { "@craco/craco": "^6.1.1", From a38f2af4f398c0cd2a31dd5bf70e5315379b39a5 Mon Sep 17 00:00:00 2001 From: Chanakya Kilaru Date: Wed, 31 May 2023 16:57:06 +0530 Subject: [PATCH 024/241] 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> --- src/utils/hooks/use-update-query-parameters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) }); From ce9f2846d0630c79651bfec0738bfc5eee9c9b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sim=C3=A3o?= Date: Thu, 1 Jun 2023 16:04:59 +0100 Subject: [PATCH 025/241] feat: add transaction notifications (#1177) --- package.json | 2 +- src/App.tsx | 3 +- src/assets/icons/CheckCircle.tsx | 25 ++ src/assets/icons/ListBullet.tsx | 25 ++ src/assets/icons/XCircle.tsx | 25 ++ src/assets/icons/index.ts | 3 + src/assets/locales/en/translation.json | 76 ++++ src/common/actions/general.actions.ts | 20 +- src/common/reducers/general.reducer.ts | 29 +- src/common/types/actions.types.ts | 21 +- src/common/types/util.types.ts | 22 ++ .../NotificationsList.tsx | 35 ++ .../NotificationsListItem.tsx | 42 ++ .../NotificationsPopover.styles.tsx | 18 + .../NotificationsPopover.tsx | 55 +++ src/components/NotificationsPopover/index.tsx | 2 + .../ToastContainer/ToastContainer.styles.tsx | 36 ++ .../ToastContainer/ToastContainer.tsx | 5 + src/components/ToastContainer/index.tsx | 2 + .../TransactionModal.style.tsx | 21 + .../TransactionModal/TransactionModal.tsx | 112 ++++++ src/components/TransactionModal/index.tsx | 1 + .../TransactionToast.styles.tsx | 13 + .../TransactionToast/TransactionToast.tsx | 132 +++++++ src/components/TransactionToast/index.tsx | 2 + src/components/index.tsx | 6 + src/index.tsx | 9 +- src/legacy-components/ErrorModal/index.tsx | 34 -- .../ConfirmedIssueRequest/index.tsx | 33 -- .../ManualIssueExecutionUI/index.tsx | 16 +- src/legacy-components/IssueUI/index.tsx | 5 +- .../RedeemUI/ReimburseStatusUI/index.tsx | 70 ++-- src/legacy-components/RedeemUI/index.tsx | 5 +- .../index.tsx | 3 +- .../components/DepositForm/DepositForm.tsx | 24 +- .../Pools/components/PoolModal/PoolModal.tsx | 9 +- .../PoolsInsights/PoolsInsights.tsx | 8 +- .../components/WithdrawForm/WithdrawForm.tsx | 21 +- .../AMM/Swap/components/SwapForm/SwapCTA.tsx | 8 +- .../AMM/Swap/components/SwapForm/SwapForm.tsx | 30 +- src/pages/Bridge/BurnForm/index.tsx | 51 +-- .../SubmittedIssueRequestModal/index.tsx | 42 +- src/pages/Bridge/IssueForm/index.tsx | 80 ++-- .../SubmittedRedeemRequestModal/index.tsx | 16 +- src/pages/Bridge/RedeemForm/index.tsx | 18 +- .../CollateralModal/CollateralModal.tsx | 49 +-- .../components/LoanForm/LoanForm.tsx | 58 ++- .../LoansInsights/LoansInsights.tsx | 18 +- .../Staking/ClaimRewardsButton/index.tsx | 29 +- src/pages/Staking/WithdrawButton/index.tsx | 39 +- src/pages/Staking/index.tsx | 12 - .../IssueRequestModal/index.tsx | 23 +- .../RedeemRequestModal/index.tsx | 23 +- .../CrossChainTransferForm.tsx | 62 +-- src/pages/Transfer/TransferForm/index.tsx | 53 +-- .../Vaults/Vault/RequestIssueModal/index.tsx | 68 ++-- .../Vaults/Vault/RequestRedeemModal/index.tsx | 26 +- .../Vault/RequestReplacementModal/index.tsx | 23 +- .../Vault/UpdateCollateralModal/index.tsx | 23 +- .../CollateralForm/CollateralForm.styles.tsx | 44 --- .../CollateralForm/CollateralForm.tsx | 312 --------------- .../Vault/components/CollateralForm/index.tsx | 2 - .../Vault/components/Rewards/Rewards.tsx | 11 - src/pages/Vaults/Vault/components/index.tsx | 4 +- .../DespositCollateralStep.tsx | 12 +- .../AvailableAssetsTable/ActionsCell.tsx | 22 +- src/parts/Topbar/index.tsx | 6 +- src/utils/constants/links.ts | 21 +- src/utils/context/Notifications.tsx | 141 +++++++ .../transaction/extrinsics/extrinsics.ts | 46 +++ .../hooks/transaction/extrinsics/index.ts | 1 + .../{utils/extrinsic.ts => extrinsics/lib.ts} | 44 +-- src/utils/hooks/transaction/extrinsics/xcm.ts | 27 ++ src/utils/hooks/transaction/types/index.ts | 29 +- src/utils/hooks/transaction/types/vesting.ts | 13 + src/utils/hooks/transaction/types/xcm.ts | 21 + .../use-transaction-notifications.tsx | 107 ++++++ .../hooks/transaction/use-transaction.ts | 95 +++-- .../hooks/transaction/utils/description.ts | 363 ++++++++++++++++++ src/utils/hooks/transaction/utils/submit.ts | 46 ++- src/utils/hooks/use-copy-tooltip.tsx | 1 + src/utils/hooks/use-countdown.ts | 69 ++++ src/utils/hooks/use-sign-message.ts | 1 + src/utils/hooks/use-window-focus.ts | 26 ++ yarn.lock | 12 +- 85 files changed, 2000 insertions(+), 1197 deletions(-) create mode 100644 src/assets/icons/CheckCircle.tsx create mode 100644 src/assets/icons/ListBullet.tsx create mode 100644 src/assets/icons/XCircle.tsx create mode 100644 src/components/NotificationsPopover/NotificationsList.tsx create mode 100644 src/components/NotificationsPopover/NotificationsListItem.tsx create mode 100644 src/components/NotificationsPopover/NotificationsPopover.styles.tsx create mode 100644 src/components/NotificationsPopover/NotificationsPopover.tsx create mode 100644 src/components/NotificationsPopover/index.tsx create mode 100644 src/components/ToastContainer/ToastContainer.styles.tsx create mode 100644 src/components/ToastContainer/ToastContainer.tsx create mode 100644 src/components/ToastContainer/index.tsx create mode 100644 src/components/TransactionModal/TransactionModal.style.tsx create mode 100644 src/components/TransactionModal/TransactionModal.tsx create mode 100644 src/components/TransactionModal/index.tsx create mode 100644 src/components/TransactionToast/TransactionToast.styles.tsx create mode 100644 src/components/TransactionToast/TransactionToast.tsx create mode 100644 src/components/TransactionToast/index.tsx delete mode 100644 src/legacy-components/ErrorModal/index.tsx delete mode 100644 src/pages/Vaults/Vault/components/CollateralForm/CollateralForm.styles.tsx delete mode 100644 src/pages/Vaults/Vault/components/CollateralForm/CollateralForm.tsx delete mode 100644 src/pages/Vaults/Vault/components/CollateralForm/index.tsx create mode 100644 src/utils/context/Notifications.tsx create mode 100644 src/utils/hooks/transaction/extrinsics/extrinsics.ts create mode 100644 src/utils/hooks/transaction/extrinsics/index.ts rename src/utils/hooks/transaction/{utils/extrinsic.ts => extrinsics/lib.ts} (70%) create mode 100644 src/utils/hooks/transaction/extrinsics/xcm.ts create mode 100644 src/utils/hooks/transaction/types/vesting.ts create mode 100644 src/utils/hooks/transaction/types/xcm.ts create mode 100644 src/utils/hooks/transaction/use-transaction-notifications.tsx create mode 100644 src/utils/hooks/transaction/utils/description.ts create mode 100644 src/utils/hooks/use-countdown.ts create mode 100644 src/utils/hooks/use-window-focus.ts diff --git a/package.json b/package.json index 581d654255..5e804dcbdd 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "react-table": "^7.6.3", - "react-toastify": "^6.0.5", + "react-toastify": "^9.1.2", "react-transition-group": "^4.4.5", "react-use": "^17.2.3", "redux": "^4.0.5", diff --git a/src/App.tsx b/src/App.tsx index a94a8a4eb2..1722f65d75 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,3 @@ -import 'react-toastify/dist/ReactToastify.css'; import './i18n'; import { FaucetClient, SecurityStatusCode } from '@interlay/interbtc-api'; @@ -21,6 +20,7 @@ import vaultsByAccountIdQuery from '@/services/queries/vaults-by-accountId-query import { BitcoinNetwork } from '@/types/bitcoin'; import { PAGES } from '@/utils/constants/links'; +import { TransactionModal } from './components/TransactionModal'; import * as constants from './constants'; import TestnetBanner from './legacy-components/TestnetBanner'; import { FeatureFlags, useFeatureFlag } from './utils/hooks/use-feature-flag'; @@ -234,6 +234,7 @@ const App = (): JSX.Element => { )} /> + ); }; diff --git a/src/assets/icons/CheckCircle.tsx b/src/assets/icons/CheckCircle.tsx new file mode 100644 index 0000000000..2bcca13aed --- /dev/null +++ b/src/assets/icons/CheckCircle.tsx @@ -0,0 +1,25 @@ +import { forwardRef } from 'react'; + +import { Icon, IconProps } from '@/component-library/Icon'; + +const CheckCircle = forwardRef((props, ref) => ( + + + +)); + +CheckCircle.displayName = 'CheckCircle'; + +export { CheckCircle }; diff --git a/src/assets/icons/ListBullet.tsx b/src/assets/icons/ListBullet.tsx new file mode 100644 index 0000000000..21eb5ba490 --- /dev/null +++ b/src/assets/icons/ListBullet.tsx @@ -0,0 +1,25 @@ +import { forwardRef } from 'react'; + +import { Icon, IconProps } from '@/component-library/Icon'; + +const ListBullet = forwardRef((props, ref) => ( + + + +)); + +ListBullet.displayName = 'ListBullet'; + +export { ListBullet }; diff --git a/src/assets/icons/XCircle.tsx b/src/assets/icons/XCircle.tsx new file mode 100644 index 0000000000..c1b84d58cd --- /dev/null +++ b/src/assets/icons/XCircle.tsx @@ -0,0 +1,25 @@ +import { forwardRef } from 'react'; + +import { Icon, IconProps } from '@/component-library/Icon'; + +const XCircle = forwardRef((props, ref) => ( + + + +)); + +XCircle.displayName = 'XCircle'; + +export { XCircle }; diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index bf537097cc..2508fb9299 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -2,11 +2,14 @@ export { ArrowRight } from './ArrowRight'; export { ArrowRightCircle } from './ArrowRightCircle'; export { ArrowsUpDown } from './ArrowsUpDown'; export { ArrowTopRightOnSquare } from './ArrowTopRightOnSquare'; +export { CheckCircle } from './CheckCircle'; export { ChevronDown } from './ChevronDown'; export { Cog } from './Cog'; export { DocumentDuplicate } from './DocumentDuplicate'; export { InformationCircle } from './InformationCircle'; +export { ListBullet } from './ListBullet'; export { PencilSquare } from './PencilSquare'; export { PlusCircle } from './PlusCircle'; export { Warning } from './Warning'; +export { XCircle } from './XCircle'; export { XMark } from './XMark'; diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index 9e03c16050..e6cd47edf2 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -156,6 +156,7 @@ "staked": "Staked", "sign_t&cs": "Sign T&Cs", "receivable_assets": "Receivable Assets", + "dismiss": "Dismiss", "redeem_page": { "maximum_in_single_request": "Max redeemable in single request", "redeem": "Redeem", @@ -636,5 +637,80 @@ "strategy": { "withdraw_rewards_in_wrapped": "Withdraw rewards in {{wrappedCurrencySymbol}}:", "update_position": "Update position" + }, + "transaction": { + "recent_transactions": "Recent transactions", + "no_recent_transactions": "No recent transactions", + "confirm_transaction_wallet": "Confirm this transaction in your wallet", + "confirm_transaction": "Confirm transaction", + "transaction_processing": "Transaction processing", + "transaction_failed": "Transaction failed", + "transaction_successful": "Transaction successful", + "swapping_to": "Swapping {{fromAmount}} {{fromCurrency}} to {{toAmount}} {{toCurrency}}", + "swapped_to": "Swapped {{fromAmount}} {{fromCurrency}} to {{toAmount}} {{toCurrency}}", + "adding_liquidity_to_pool": "Adding liquidity to {{poolName}} Pool", + "added_liquidity_to_pool": "Added liquidity to {{poolName}} Pool", + "removing_liquidity_from_pool": "Removing liquidity from {{poolName}} Pool", + "removed_liquidity_from_pool": "Removed liquidity from {{poolName}} Pool", + "claiming_pool_rewards": "Claiming pools rewards", + "claimed_pool_rewards": "Claimed pools rewards", + "issuing_amount": "Issuing {{amount}} {{currency}}", + "issued_amount": "Issuing {{amount}} {{currency}}", + "redeeming_amount": "Redeeming {{amount}} {{currency}}", + "redeemed_amount": "Redeemed {{amount}} {{currency}}", + "burning_amount": "Burning {{amount}} {{currency}}", + "burned_amount": "Burned {{amount}} {{currency}}", + "retrying_redeem_id": "Retrying redeem {{resquestId}}", + "retried_redeem_id": "Retried redeem {{resquestId}}", + "reimbursing_redeem_id": "Reimbursing redeem {{resquestId}}", + "reimbersed_redeem_id": "Reimbursed redeem {{resquestId}}", + "executing_issue": "Executing issue", + "executed_issue": "Executed issue", + "transfering_amount_to_address": "Transfering {{amount}} {{currency}} to {{address}}", + "transfered_amount_to_address": "Transfered {{amount}} {{currency}} to {{address}}", + "transfering_amount_from_chain_to_chain": "Transfering {{amount}} {{currency}} from {{fromChain}} to {{toChain}}", + "transfered_amount_from_chain_to_chain": "Transfered {{amount}} {{currency}} from {{fromChain}} to {{toChain}}", + "claiming_lending_rewards": "Claiming lending rewards", + "claimed_lending_rewards": "Claimed lending rewards", + "borrowing_amount": "Borrowing {{amount}} {{currency}}", + "borrowed_amount": "Borrowed {{amount}} {{currency}}", + "lending_amount": "Lending {{amount}} {{currency}}", + "lent_amount": "Lent {{amount}} {{currency}}", + "repaying_amount": "Repaying {{amount}} {{currency}}", + "repaid_amount": "Repaid {{amount}} {{currency}}", + "repaying": "Repaying {{currency}}", + "repaid": "Repaid {{currency}}", + "withdrawing_amount": "Withdrawing {{amount}} {{currency}}", + "withdrew_amount": "Withdrew {{amount}} {{currency}}", + "withdrawing": "Withdrawing {{currency}}", + "withdrew": "Withdrew {{currency}}", + "disabling_loan_as_collateral": "Disabling {{currency}} as collateral", + "disabled_loan_as_collateral": "Disabled {{currency}} as collateral", + "enabling_loan_as_collateral": "Enabling {{currency}} as collateral", + "enabled_loan_as_collateral": "Enabled {{currency}} as collateral", + "creating_currency_vault": "Creating {{currency}} vault", + "created_currency_vault": "Created {{currency}} vault", + "depositing_amount_to_vault": "Depositing {{amount}} {{currency}} to vault", + "deposited_amount_to_vault": "Deposited {{amount}} {{currency}} to vault", + "withdrawing_amount_from_vault": "Withdrawing {{amount}} {{currency}} from vault", + "withdrew_amount_from_vault": "Withdrew {{amount}} {{currency}} from vault", + "claiming_vault_rewards": "Claiming vault rewards", + "claimed_vault_rewards": "Claimed vault rewards", + "staking_amount": "Staking {{amount}} {{currency}}", + "staked_amount": "Staking {{amount}} {{currency}}", + "adding_amount_to_staked_amount": "Adding {{amount}} {{currency}} to staked amount", + "added_amount_to_staked_amount": "Added {{amount}} {{currency}} to staked amount", + "increasing_stake_lock_time": "Increasing stake lock time", + "increased_stake_lock_time": "Increased stake lock time", + "withdrawing_stake": "Withdrawing stake", + "withdrew_stake": "Withdrew stake", + "claiming_staking_rewards": "Claiming staking rewards", + "claimed_staking_rewards": "Claimed staking rewards", + "increasing_stake_locked_time_amount": "Increasing stake locked time and amount", + "increased_stake_locked_time_amount": "Increased stake locked time and amount", + "requesting_vault_replacement": "Requesting vault replacement", + "requested_vault_replacement": "Requested vault replacement", + "claiming_vesting": "Claiming vesting", + "claimed_vesting": "Claimed vesting" } } diff --git a/src/common/actions/general.actions.ts b/src/common/actions/general.actions.ts index 8bfaa85c76..880e1da1ce 100644 --- a/src/common/actions/general.actions.ts +++ b/src/common/actions/general.actions.ts @@ -4,6 +4,8 @@ import { BitcoinAmount, MonetaryAmount } from '@interlay/monetary-js'; import { GovernanceTokenMonetaryAmount } from '@/config/relay-chains'; import { + ADD_NOTIFICATION, + AddNotification, INIT_GENERAL_DATA_ACTION, InitGeneralDataAction, IS_BRIDGE_LOADED, @@ -20,10 +22,12 @@ import { ShowSignTermsModal, UPDATE_HEIGHTS, UPDATE_TOTALS, + UPDATE_TRANSACTION_MODAL_STATUS, UpdateHeights, - UpdateTotals + UpdateTotals, + UpdateTransactionModal } from '../types/actions.types'; -import { ParachainStatus } from '../types/util.types'; +import { Notification, ParachainStatus, TransactionModalData } from '../types/util.types'; export const isBridgeLoaded = (isLoaded = false): IsBridgeLoaded => ({ type: IS_BRIDGE_LOADED, @@ -86,3 +90,15 @@ export const updateTotalsAction = ( totalLockedCollateralTokenAmount, totalWrappedTokenAmount }); + +export const addNotification = (accountAddress: string, notification: Notification): AddNotification => ({ + type: ADD_NOTIFICATION, + accountAddress, + notification +}); + +export const updateTransactionModal = (isOpen: boolean, data: TransactionModalData): UpdateTransactionModal => ({ + type: UPDATE_TRANSACTION_MODAL_STATUS, + isOpen, + data +}); diff --git a/src/common/reducers/general.reducer.ts b/src/common/reducers/general.reducer.ts index cc89bc33e7..23093c810a 100644 --- a/src/common/reducers/general.reducer.ts +++ b/src/common/reducers/general.reducer.ts @@ -2,8 +2,10 @@ import { newMonetaryAmount } from '@interlay/interbtc-api'; import { BitcoinAmount } from '@interlay/monetary-js'; import { RELAY_CHAIN_NATIVE_TOKEN } from '@/config/relay-chains'; +import { TransactionStatus } from '@/utils/hooks/transaction/types'; import { + ADD_NOTIFICATION, GeneralActions, INIT_GENERAL_DATA_ACTION, IS_BRIDGE_LOADED, @@ -12,7 +14,8 @@ import { SHOW_BUY_MODAL, SHOW_SIGN_TERMS_MODAL, UPDATE_HEIGHTS, - UPDATE_TOTALS + UPDATE_TOTALS, + UPDATE_TRANSACTION_MODAL_STATUS } from '../types/actions.types'; import { GeneralState, ParachainStatus } from '../types/util.types'; @@ -33,6 +36,11 @@ const initialState = { relayChainNativeToken: { usd: 0 }, governanceToken: { usd: 0 }, wrappedToken: { usd: 0 } + }, + notifications: {}, + transactionModal: { + isOpen: false, + data: { variant: TransactionStatus.CONFIRM } } }; @@ -65,6 +73,25 @@ export const generalReducer = (state: GeneralState = initialState, action: Gener return { ...state, isBuyModalOpen: action.isBuyModalOpen }; case SHOW_SIGN_TERMS_MODAL: return { ...state, isSignTermsModalOpen: action.isSignTermsModalOpen }; + case ADD_NOTIFICATION: { + const newAccountNotifications = [...(state.notifications[action.accountAddress] || []), action.notification]; + + return { + ...state, + notifications: { + ...state.notifications, + [action.accountAddress]: newAccountNotifications + } + }; + } + case UPDATE_TRANSACTION_MODAL_STATUS: + return { + ...state, + transactionModal: { + ...state.transactionModal, + ...action + } + }; default: return state; } diff --git a/src/common/types/actions.types.ts b/src/common/types/actions.types.ts index 4ebf3cb5df..f4744b03ac 100644 --- a/src/common/types/actions.types.ts +++ b/src/common/types/actions.types.ts @@ -3,7 +3,7 @@ import { BitcoinAmount, MonetaryAmount } from '@interlay/monetary-js'; import { GovernanceTokenMonetaryAmount } from '@/config/relay-chains'; -import { ParachainStatus, StoreType } from './util.types'; +import { Notification, ParachainStatus, StoreType, TransactionModalData } from './util.types'; // GENERAL ACTIONS export const IS_BRIDGE_LOADED = 'IS_BRIDGE_LOADED'; @@ -20,6 +20,9 @@ export const SHOW_SIGN_TERMS_MODAL = 'SHOW_SIGN_TERMS_MODAL'; export const UPDATE_HEIGHTS = 'UPDATE_HEIGHTS'; export const UPDATE_TOTALS = 'UPDATE_TOTALS'; export const SHOW_BUY_MODAL = 'SHOW_BUY_MODAL'; +export const ADD_NOTIFICATION = 'ADD_NOTIFICATION'; +export const SHOW_TRANSACTION_MODAL = 'SHOW_TRANSACTION_MODAL'; +export const UPDATE_TRANSACTION_MODAL_STATUS = 'UPDATE_TRANSACTION_MODAL_STATUS'; export interface UpdateTotals { type: typeof UPDATE_TOTALS; @@ -98,6 +101,18 @@ export interface ShowBuyModal { isBuyModalOpen: boolean; } +export interface AddNotification { + type: typeof ADD_NOTIFICATION; + accountAddress: string; + notification: Notification; +} + +export interface UpdateTransactionModal { + type: typeof UPDATE_TRANSACTION_MODAL_STATUS; + isOpen: boolean; + data: TransactionModalData; +} + export type GeneralActions = | IsBridgeLoaded | InitGeneralDataAction @@ -110,7 +125,9 @@ export type GeneralActions = | UpdateHeights | UpdateTotals | ShowBuyModal - | ShowSignTermsModal; + | ShowSignTermsModal + | AddNotification + | UpdateTransactionModal; // REDEEM export const ADD_VAULT_REDEEMS = 'ADD_VAULT_REDEEMS'; diff --git a/src/common/types/util.types.ts b/src/common/types/util.types.ts index b49a70f30b..922531dad0 100644 --- a/src/common/types/util.types.ts +++ b/src/common/types/util.types.ts @@ -3,6 +3,8 @@ import { BitcoinAmount, MonetaryAmount } from '@interlay/monetary-js'; import { u256 } from '@polkadot/types/primitive'; import { CombinedState, Store } from 'redux'; +import { TransactionStatus } from '@/utils/hooks/transaction/types'; + import { rootReducer } from '../reducers/index'; import { GeneralActions, RedeemActions, VaultActions } from './actions.types'; import { RedeemState } from './redeem.types'; @@ -45,6 +47,21 @@ export enum ParachainStatus { Shutdown } +export type Notification = { + status: TransactionStatus; + description: string; + date: Date; + url?: string; +}; + +export type TransactionModalData = { + variant: TransactionStatus; + timestamp?: number; + description?: string; + url?: string; + errorMessage?: string; +}; + export type GeneralState = { bridgeLoaded: boolean; vaultClientLoaded: boolean; @@ -56,6 +73,11 @@ export type GeneralState = { btcRelayHeight: number; bitcoinHeight: number; parachainStatus: ParachainStatus; + notifications: Record; + transactionModal: { + isOpen: boolean; + data: TransactionModalData; + }; }; export type AppState = ReturnType; diff --git a/src/components/NotificationsPopover/NotificationsList.tsx b/src/components/NotificationsPopover/NotificationsList.tsx new file mode 100644 index 0000000000..97d51a2c9a --- /dev/null +++ b/src/components/NotificationsPopover/NotificationsList.tsx @@ -0,0 +1,35 @@ +import { useTranslation } from 'react-i18next'; + +import { Notification } from '@/common/types/util.types'; +import { Flex, P } from '@/component-library'; + +import { NotificationListItem } from './NotificationsListItem'; + +type NotificationsListProps = { + items: Notification[]; +}; + +const NotificationsList = ({ items }: NotificationsListProps): JSX.Element => { + const { t } = useTranslation(); + + if (!items.length) { + return ( +

+ {t('transaction.no_recent_transactions')} +

+ ); + } + + const latestTransactions = items.slice(-5); + + return ( + + {latestTransactions.map((item, index) => ( + + ))} + + ); +}; + +export { NotificationsList }; +export type { NotificationsListProps }; diff --git a/src/components/NotificationsPopover/NotificationsListItem.tsx b/src/components/NotificationsPopover/NotificationsListItem.tsx new file mode 100644 index 0000000000..7c29cdfce8 --- /dev/null +++ b/src/components/NotificationsPopover/NotificationsListItem.tsx @@ -0,0 +1,42 @@ +import { useButton } from '@react-aria/button'; +import { formatDistanceToNowStrict } from 'date-fns'; +import { useRef } from 'react'; + +import { CheckCircle, XCircle } from '@/assets/icons'; +import { Notification } from '@/common/types/util.types'; +import { Flex, P } from '@/component-library'; +import { TransactionStatus } from '@/utils/hooks/transaction/types'; + +import { StyledListItem } from './NotificationsPopover.styles'; + +type NotificationListItemProps = Notification; + +const NotificationListItem = ({ date, description, status, url }: NotificationListItemProps): JSX.Element => { + const ref = useRef(null); + + const ariaLabel = url ? 'navigate to transaction subscan page' : undefined; + + const handlePress = () => window.open(url, '_blank', 'noopener'); + + const { buttonProps } = useButton( + { 'aria-label': ariaLabel, isDisabled: !url, elementType: 'div', onPress: handlePress }, + ref + ); + + return ( + + + + {status === TransactionStatus.SUCCESS ? : } +

{description}

+
+

+ {formatDistanceToNowStrict(date)} ago +

+
+
+ ); +}; + +export { NotificationListItem }; +export type { NotificationListItemProps }; diff --git a/src/components/NotificationsPopover/NotificationsPopover.styles.tsx b/src/components/NotificationsPopover/NotificationsPopover.styles.tsx new file mode 100644 index 0000000000..828c137438 --- /dev/null +++ b/src/components/NotificationsPopover/NotificationsPopover.styles.tsx @@ -0,0 +1,18 @@ +import styled from 'styled-components'; + +import { CTA, theme } from '@/component-library'; + +const StyledListItem = styled.div` + padding: ${theme.spacing.spacing3} ${theme.spacing.spacing2}; + + &:not(:last-of-type) { + border-bottom: ${theme.border.default}; + } +`; + +const StyledCTA = styled(CTA)` + padding: ${theme.spacing.spacing3}; + border: ${theme.border.default}; +`; + +export { StyledCTA, StyledListItem }; diff --git a/src/components/NotificationsPopover/NotificationsPopover.tsx b/src/components/NotificationsPopover/NotificationsPopover.tsx new file mode 100644 index 0000000000..334298a192 --- /dev/null +++ b/src/components/NotificationsPopover/NotificationsPopover.tsx @@ -0,0 +1,55 @@ +import { useTranslation } from 'react-i18next'; + +import { ListBullet } from '@/assets/icons'; +import { Notification } from '@/common/types/util.types'; +import { + Popover, + PopoverBody, + PopoverContent, + PopoverFooter, + PopoverHeader, + PopoverTrigger, + TextLink +} from '@/component-library'; +import { EXTERNAL_PAGES, EXTERNAL_URL_PARAMETERS } from '@/utils/constants/links'; + +import { NotificationsList } from './NotificationsList'; +import { StyledCTA } from './NotificationsPopover.styles'; + +type NotificationsPopoverProps = { + address?: string; + items: Notification[]; +}; + +const NotificationsPopover = ({ address, items }: NotificationsPopoverProps): JSX.Element => { + const { t } = useTranslation(); + + const accountTransactionsUrl = + address && EXTERNAL_PAGES.SUBSCAN.ACCOUNT.replace(`:${EXTERNAL_URL_PARAMETERS.SUBSCAN.ACCOUNT.ADDRESS}`, address); + + return ( + + + + + + + + {t('transaction.recent_transactions')} + + + + {accountTransactionsUrl && ( + + + View all transactions + + + )} + + + ); +}; + +export { NotificationsPopover }; +export type { NotificationsPopoverProps }; diff --git a/src/components/NotificationsPopover/index.tsx b/src/components/NotificationsPopover/index.tsx new file mode 100644 index 0000000000..9d68f4a5e0 --- /dev/null +++ b/src/components/NotificationsPopover/index.tsx @@ -0,0 +1,2 @@ +export type { NotificationsPopoverProps } from './NotificationsPopover'; +export { NotificationsPopover } from './NotificationsPopover'; diff --git a/src/components/ToastContainer/ToastContainer.styles.tsx b/src/components/ToastContainer/ToastContainer.styles.tsx new file mode 100644 index 0000000000..0de455dab2 --- /dev/null +++ b/src/components/ToastContainer/ToastContainer.styles.tsx @@ -0,0 +1,36 @@ +import 'react-toastify/dist/ReactToastify.css'; + +import { ToastContainer } from 'react-toastify'; +import styled from 'styled-components'; + +import { theme } from '@/component-library'; + +// &&& is used to override css styles +const StyledToastContainer = styled(ToastContainer)` + &&&.Toastify__toast-container { + color: ${theme.colors.textPrimary}; + padding: 0 ${theme.spacing.spacing4}; + } + + @media ${theme.breakpoints.up('sm')} { + &&&.Toastify__toast-container { + padding: 0; + } + } + + .Toastify__toast { + margin-bottom: 1rem; + padding: 0; + border-radius: 12px; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); + font-family: inherit; + background: ${theme.colors.bgPrimary}; + border: ${theme.border.default}; + } + + .Toastify__toast-body { + padding: 0; + } +`; + +export { StyledToastContainer }; diff --git a/src/components/ToastContainer/ToastContainer.tsx b/src/components/ToastContainer/ToastContainer.tsx new file mode 100644 index 0000000000..8119b67efd --- /dev/null +++ b/src/components/ToastContainer/ToastContainer.tsx @@ -0,0 +1,5 @@ +import { ToastContainerProps } from 'react-toastify'; + +import { StyledToastContainer } from './ToastContainer.styles'; +export { StyledToastContainer as ToastContainer }; +export type { ToastContainerProps }; diff --git a/src/components/ToastContainer/index.tsx b/src/components/ToastContainer/index.tsx new file mode 100644 index 0000000000..31f30105c2 --- /dev/null +++ b/src/components/ToastContainer/index.tsx @@ -0,0 +1,2 @@ +export type { ToastContainerProps } from './ToastContainer'; +export { ToastContainer } from './ToastContainer'; diff --git a/src/components/TransactionModal/TransactionModal.style.tsx b/src/components/TransactionModal/TransactionModal.style.tsx new file mode 100644 index 0000000000..7819fa032b --- /dev/null +++ b/src/components/TransactionModal/TransactionModal.style.tsx @@ -0,0 +1,21 @@ +import styled from 'styled-components'; + +import { CheckCircle, XCircle } from '@/assets/icons'; +import { Card, theme } from '@/component-library'; + +const StyledXCircle = styled(XCircle)` + width: 4rem; + height: 4rem; +`; + +const StyledCheckCircle = styled(CheckCircle)` + width: 4rem; + height: 4rem; +`; + +const StyledCard = styled(Card)` + border-radius: ${theme.rounded.rg}; + padding: ${theme.spacing.spacing4}; +`; + +export { StyledCard, StyledCheckCircle, StyledXCircle }; diff --git a/src/components/TransactionModal/TransactionModal.tsx b/src/components/TransactionModal/TransactionModal.tsx new file mode 100644 index 0000000000..6ab35ada22 --- /dev/null +++ b/src/components/TransactionModal/TransactionModal.tsx @@ -0,0 +1,112 @@ +import { TFunction } from 'i18next'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; + +import { updateTransactionModal } from '@/common/actions/general.actions'; +import { StoreType } from '@/common/types/util.types'; +import { + CTA, + Flex, + H4, + H5, + LoadingSpinner, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + P, + TextLink +} from '@/component-library'; +import { NotificationToast, useNotifications } from '@/utils/context/Notifications'; +import { TransactionStatus } from '@/utils/hooks/transaction/types'; + +import { StyledCard, StyledCheckCircle, StyledXCircle } from './TransactionModal.style'; + +const loadingSpinner = ; + +const getData = (t: TFunction, variant: TransactionStatus) => + ({ + [TransactionStatus.CONFIRM]: { + title: t('transaction.confirm_transaction'), + subtitle: t('transaction.confirm_transaction_wallet'), + icon: loadingSpinner + }, + [TransactionStatus.SUBMITTING]: { + title: t('transaction.transaction_processing'), + icon: loadingSpinner + }, + [TransactionStatus.ERROR]: { + title: t('transaction.transaction_failed'), + icon: + }, + [TransactionStatus.SUCCESS]: { + title: t('transaction.transaction_successful'), + icon: + } + }[variant]); + +const TransactionModal = (): JSX.Element => { + const { t } = useTranslation(); + + const notifications = useNotifications(); + + const { isOpen, data } = useSelector((state: StoreType) => state.general.transactionModal); + const { variant, description, url, timestamp, errorMessage } = data; + const dispatch = useDispatch(); + + const { title, subtitle, icon } = getData(t, variant); + + const hasDismiss = variant !== TransactionStatus.CONFIRM; + + const handleClose = () => { + // Only show toast if the current transaction variant is CONFIRM or SUBMITTING. + // No need to show toast if the transaction is SUCCESS or ERROR + if (timestamp && (variant === TransactionStatus.CONFIRM || variant === TransactionStatus.SUBMITTING)) { + notifications.show(timestamp, { + type: NotificationToast.TRANSACTION, + props: { variant: variant, url, description } + }); + } + + dispatch(updateTransactionModal(false, data)); + }; + + return ( + + {title} + + {icon} + + {subtitle && ( +

+ {subtitle} +

+ )} + {description && ( +

+ {description} +

+ )} + {errorMessage && ( + +
+ Message: +
+

+ {errorMessage} +

+
+ )} + {url && ( + + View transaction on Subscan + + )} +
+
+ {hasDismiss && {t('dismiss')}} +
+ ); +}; + +export { TransactionModal }; diff --git a/src/components/TransactionModal/index.tsx b/src/components/TransactionModal/index.tsx new file mode 100644 index 0000000000..db2576f068 --- /dev/null +++ b/src/components/TransactionModal/index.tsx @@ -0,0 +1 @@ +export { TransactionModal } from './TransactionModal'; diff --git a/src/components/TransactionToast/TransactionToast.styles.tsx b/src/components/TransactionToast/TransactionToast.styles.tsx new file mode 100644 index 0000000000..11a85da6e7 --- /dev/null +++ b/src/components/TransactionToast/TransactionToast.styles.tsx @@ -0,0 +1,13 @@ +import styled from 'styled-components'; + +import { Flex, ProgressBar, theme } from '@/component-library'; + +const StyledWrapper = styled(Flex)` + padding: ${theme.spacing.spacing4}; +`; + +const StyledProgressBar = styled(ProgressBar)` + margin-top: ${theme.spacing.spacing4}; +`; + +export { StyledProgressBar, StyledWrapper }; diff --git a/src/components/TransactionToast/TransactionToast.tsx b/src/components/TransactionToast/TransactionToast.tsx new file mode 100644 index 0000000000..fc413baba1 --- /dev/null +++ b/src/components/TransactionToast/TransactionToast.tsx @@ -0,0 +1,132 @@ +import { useHover } from '@react-aria/interactions'; +import { mergeProps } from '@react-aria/utils'; +import { TFunction } from 'i18next'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; + +import { CheckCircle, XCircle } from '@/assets/icons'; +import { updateTransactionModal } from '@/common/actions/general.actions'; +import { CTA, CTALink, Divider, Flex, FlexProps, LoadingSpinner, P } from '@/component-library'; +import { TransactionStatus } from '@/utils/hooks/transaction/types'; +import { useCountdown } from '@/utils/hooks/use-countdown'; + +import { StyledProgressBar, StyledWrapper } from './TransactionToast.styles'; + +const loadingSpinner = ; + +const getData = (t: TFunction, variant: TransactionStatus) => + ({ + [TransactionStatus.CONFIRM]: { + title: t('transaction.confirm_transaction'), + icon: loadingSpinner + }, + [TransactionStatus.SUBMITTING]: { + title: t('transaction.transaction_processing'), + icon: loadingSpinner + }, + [TransactionStatus.SUCCESS]: { + title: t('transaction.transaction_successful'), + icon: + }, + [TransactionStatus.ERROR]: { + title: t('transaction.transaction_failed'), + icon: + } + }[variant]); + +type Props = { + variant?: TransactionStatus; + description?: string; + url?: string; + errorMessage?: string; + timeout?: number; + onDismiss?: () => void; +}; + +type InheritAttrs = Omit; + +type TransactionToastProps = Props & InheritAttrs; + +const TransactionToast = ({ + variant = TransactionStatus.SUCCESS, + timeout = 8000, + url, + description, + onDismiss, + errorMessage, + ...props +}: TransactionToastProps): JSX.Element => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const showCountdown = variant === TransactionStatus.SUCCESS || variant === TransactionStatus.ERROR; + + const { value: countdown, start, stop } = useCountdown({ + timeout, + disabled: !showCountdown, + onEndCountdown: onDismiss + }); + + const { hoverProps } = useHover({ + onHoverStart: stop, + onHoverEnd: start, + isDisabled: !showCountdown + }); + + const handleViewDetails = () => { + dispatch(updateTransactionModal(true, { variant: TransactionStatus.ERROR, description, errorMessage })); + onDismiss?.(); + }; + + const { title, icon } = getData(t, variant); + + return ( + + + + {icon} + + +

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+
+ {showCountdown && ( + + )} + + {(url || errorMessage) && ( + <> + {url && ( + + View Subscan + + )} + {errorMessage && !url && ( + + View Details + + )} + + + )} + + Dismiss + + +
+ ); +}; + +export { TransactionToast }; +export type { TransactionToastProps }; diff --git a/src/components/TransactionToast/index.tsx b/src/components/TransactionToast/index.tsx new file mode 100644 index 0000000000..36ce2db462 --- /dev/null +++ b/src/components/TransactionToast/index.tsx @@ -0,0 +1,2 @@ +export type { TransactionToastProps } from './TransactionToast'; +export { TransactionToast } from './TransactionToast'; diff --git a/src/components/index.tsx b/src/components/index.tsx index 83fc0ca6aa..bb20578fa9 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -10,6 +10,12 @@ export type { IsAuthenticatedProps } from './IsAuthenticated'; export { IsAuthenticated } from './IsAuthenticated'; export type { LoanPositionsTableProps } from './LoanPositionsTable'; export { LoanPositionsTable } from './LoanPositionsTable'; +export type { NotificationsPopoverProps } from './NotificationsPopover'; +export { NotificationsPopover } from './NotificationsPopover'; export type { PoolsTableProps } from './PoolsTable'; export { PoolsTable } from './PoolsTable'; export { ReceivableAssets } from './ReceivableAssets'; +export type { ToastContainerProps } from './ToastContainer'; +export { ToastContainer } from './ToastContainer'; +export type { TransactionToastProps } from './TransactionToast'; +export { TransactionToast } from './TransactionToast'; diff --git a/src/index.tsx b/src/index.tsx index 4901f7d9a1..327b7658e6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -21,6 +21,7 @@ import App from './App'; import { GeoblockingWrapper } from './components/Geoblock/Geoblock'; import reportWebVitals from './reportWebVitals'; import { store } from './store'; +import { NotificationsProvider } from './utils/context/Notifications'; configGlobalBig(); @@ -40,9 +41,11 @@ ReactDOM.render( - - - + + + + + diff --git a/src/legacy-components/ErrorModal/index.tsx b/src/legacy-components/ErrorModal/index.tsx deleted file mode 100644 index 8dc60f3f6e..0000000000 --- a/src/legacy-components/ErrorModal/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import clsx from 'clsx'; -import * as React from 'react'; - -import CloseIconButton from '@/legacy-components/buttons/CloseIconButton'; -import InterlayModal, { - InterlayModalInnerWrapper, - InterlayModalTitle, - Props as ModalProps -} from '@/legacy-components/UI/InterlayModal'; - -interface CustomProps { - title: string; - description: string; -} - -const ErrorModal = ({ open, onClose, title, description }: Props): JSX.Element => { - const focusRef = React.useRef(null); - - return ( - - - - {title} - - -

{description}

-
-
- ); -}; - -export type Props = Omit & CustomProps; - -export default ErrorModal; diff --git a/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx b/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx index e76d52fe2d..3472455342 100644 --- a/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx +++ b/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx @@ -1,22 +1,14 @@ import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import { FaCheckCircle } from 'react-icons/fa'; -import { useQueryClient } from 'react-query'; -import { toast } from 'react-toastify'; import { BTC_EXPLORER_TRANSACTION_API } from '@/config/blockstream-explorer-links'; import { WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains'; import AddressWithCopyUI from '@/legacy-components/AddressWithCopyUI'; -import ErrorModal from '@/legacy-components/ErrorModal'; import ExternalLink from '@/legacy-components/ExternalLink'; import RequestWrapper from '@/pages/Bridge/RequestWrapper'; -import { ISSUES_FETCHER } from '@/services/fetchers/issues-fetcher'; -import { TABLE_PAGE_LIMIT } from '@/utils/constants/general'; -import { QUERY_PARAMETERS } from '@/utils/constants/links'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import { getColorShade } from '@/utils/helpers/colors'; -import { Transaction, useTransaction } from '@/utils/hooks/transaction'; -import useQueryParams from '@/utils/hooks/use-query-params'; import ManualIssueExecutionUI from '../ManualIssueExecutionUI'; @@ -28,21 +20,6 @@ interface Props { const ConfirmedIssueRequest = ({ request }: Props): JSX.Element => { const { t } = useTranslation(); - const queryParams = useQueryParams(); - const selectedPage = Number(queryParams.get(QUERY_PARAMETERS.PAGE)) || 1; - const selectedPageIndex = selectedPage - 1; - - const queryClient = useQueryClient(); - - // TODO: check if this transaction is necessary - const transaction = useTransaction(Transaction.ISSUE_EXECUTE, { - onSuccess: (_, variables) => { - const [requestId] = variables.args; - queryClient.invalidateQueries([ISSUES_FETCHER, selectedPageIndex * TABLE_PAGE_LIMIT, TABLE_PAGE_LIMIT]); - toast.success(t('issue_page.successfully_executed', { id: requestId })); - } - }); - return ( <> @@ -75,16 +52,6 @@ const ConfirmedIssueRequest = ({ request }: Props): JSX.Element => {

- {transaction.isError && transaction.error && ( - { - transaction.reset(); - }} - title='Error' - description={typeof transaction.error === 'string' ? transaction.error : transaction.error.message} - /> - )} ); }; diff --git a/src/legacy-components/IssueUI/IssueRequestStatusUI/ManualIssueExecutionUI/index.tsx b/src/legacy-components/IssueUI/IssueRequestStatusUI/ManualIssueExecutionUI/index.tsx index c93aa9aae2..a111faff63 100644 --- a/src/legacy-components/IssueUI/IssueRequestStatusUI/ManualIssueExecutionUI/index.tsx +++ b/src/legacy-components/IssueUI/IssueRequestStatusUI/ManualIssueExecutionUI/index.tsx @@ -8,12 +8,10 @@ import { import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import { useQuery, useQueryClient } from 'react-query'; -import { toast } from 'react-toastify'; import { displayMonetaryAmount } from '@/common/utils/utils'; import { WRAPPED_TOKEN, WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains'; import InterlayDenimOrKintsugiMidnightOutlinedButton from '@/legacy-components/buttons/InterlayDenimOrKintsugiMidnightOutlinedButton'; -import ErrorModal from '@/legacy-components/ErrorModal'; import { useSubstrateSecureState } from '@/lib/substrate'; import { ISSUES_FETCHER } from '@/services/fetchers/issues-fetcher'; import { TABLE_PAGE_LIMIT } from '@/utils/constants/general'; @@ -57,10 +55,8 @@ const ManualIssueExecutionUI = ({ request }: Props): JSX.Element => { const queryClient = useQueryClient(); const transaction = useTransaction(Transaction.ISSUE_EXECUTE, { - onSuccess: (_, variables) => { - const [requestId] = variables.args; + onSuccess: () => { queryClient.invalidateQueries([ISSUES_FETCHER, selectedPageIndex * TABLE_PAGE_LIMIT, TABLE_PAGE_LIMIT]); - toast.success(t('issue_page.successfully_executed', { id: requestId })); } }); @@ -139,16 +135,6 @@ const ManualIssueExecutionUI = ({ request }: Props): JSX.Element => { wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL })} - {transaction.isError && transaction.error && ( - { - transaction.reset(); - }} - title='Error' - description={typeof transaction.error === 'string' ? transaction.error : transaction.error.message} - /> - )} ); }; diff --git a/src/legacy-components/IssueUI/index.tsx b/src/legacy-components/IssueUI/index.tsx index 2158916386..4edd9b6878 100644 --- a/src/legacy-components/IssueUI/index.tsx +++ b/src/legacy-components/IssueUI/index.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { ReactComponent as BitcoinLogoIcon } from '@/assets/img/bitcoin-logo.svg'; import { displayMonetaryAmountInUSDFormat, formatNumber } from '@/common/utils/utils'; +import { Flex } from '@/component-library'; import { WRAPPED_TOKEN_SYMBOL, WrappedTokenAmount } from '@/config/relay-chains'; import AddressWithCopyUI from '@/legacy-components/AddressWithCopyUI'; import Hr2 from '@/legacy-components/hrs/Hr2'; @@ -52,7 +53,7 @@ const IssueUI = ({ issue }: Props): JSX.Element => { const sentBackingTokenAmount = receivedWrappedTokenAmount.add(bridgeFee); return ( -
+
{/* TODO: could componentize */} @@ -184,7 +185,7 @@ const IssueUI = ({ issue }: Props): JSX.Element => {

<>{renderModalStatusPanel(issue)} -
+
); }; diff --git a/src/legacy-components/RedeemUI/ReimburseStatusUI/index.tsx b/src/legacy-components/RedeemUI/ReimburseStatusUI/index.tsx index 2e2cc3b19e..541f4ef875 100644 --- a/src/legacy-components/RedeemUI/ReimburseStatusUI/index.tsx +++ b/src/legacy-components/RedeemUI/ReimburseStatusUI/index.tsx @@ -1,14 +1,12 @@ import { newMonetaryAmount } from '@interlay/interbtc-api'; -import { ISubmittableResult } from '@polkadot/types/types'; import Big from 'big.js'; import clsx from 'clsx'; import * as React from 'react'; import { useErrorHandler, withErrorBoundary } from 'react-error-boundary'; import { useTranslation } from 'react-i18next'; import { FaExclamationCircle } from 'react-icons/fa'; -import { useMutation, useQueryClient } from 'react-query'; +import { useQueryClient } from 'react-query'; import { useSelector } from 'react-redux'; -import { toast } from 'react-toastify'; import { StoreType } from '@/common/types/util.types'; import { displayMonetaryAmount, displayMonetaryAmountInUSDFormat } from '@/common/utils/utils'; @@ -22,10 +20,10 @@ import RequestWrapper from '@/pages/Bridge/RequestWrapper'; import { REDEEMS_FETCHER } from '@/services/fetchers/redeems-fetcher'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import { getColorShade } from '@/utils/helpers/colors'; -import { submitExtrinsic } from '@/utils/helpers/extrinsic'; import { getExchangeRate } from '@/utils/helpers/oracle'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; interface Props { redeem: any; // TODO: should type properly (`Relay`) @@ -45,6 +43,20 @@ const ReimburseStatusUI = ({ redeem, onClose }: Props): JSX.Element => { ); const { t } = useTranslation(); const handleError = useErrorHandler(); + const queryClient = useQueryClient(); + + const [cancelType, setCancelType] = React.useState<'reimburse' | 'retry'>(); + + const transaction = useTransaction(Transaction.REDEEM_CANCEL, { + onSuccess: () => { + queryClient.invalidateQueries([REDEEMS_FETCHER]); + setCancelType(undefined); + onClose(); + }, + onError: () => { + setCancelType(undefined); + } + }); React.useEffect(() => { if (!bridgeLoaded) return; @@ -67,48 +79,13 @@ const ReimburseStatusUI = ({ redeem, onClose }: Props): JSX.Element => { })(); }, [redeem, bridgeLoaded, handleError]); - const queryClient = useQueryClient(); - // TODO: should type properly (`Relay`) - const retryMutation = useMutation( - (variables: any) => { - return submitExtrinsic(window.bridge.redeem.cancel(variables.id, false)); - }, - { - onSuccess: () => { - queryClient.invalidateQueries([REDEEMS_FETCHER]); - toast.success(t('redeem_page.successfully_cancelled_redeem')); - onClose(); - }, - onError: (error) => { - console.log('[useMutation] error => ', error); - toast.error(t('redeem_page.error_cancelling_redeem')); - } - } - ); - // TODO: should type properly (`Relay`) - const reimburseMutation = useMutation( - (variables: any) => { - return submitExtrinsic(window.bridge.redeem.cancel(variables.id, true)); - }, - { - onSuccess: () => { - queryClient.invalidateQueries([REDEEMS_FETCHER]); - toast.success(t('redeem_page.successfully_cancelled_redeem')); - onClose(); - }, - onError: (error) => { - console.log('[useMutation] error => ', error); - toast.error(t('redeem_page.error_cancelling_redeem')); - } - } - ); - const handleRetry = () => { if (!bridgeLoaded) { throw new Error('Bridge is not loaded!'); } - retryMutation.mutate(redeem); + setCancelType('retry'); + transaction.execute(redeem.id, false); }; const handleReimburse = () => { @@ -116,7 +93,8 @@ const ReimburseStatusUI = ({ redeem, onClose }: Props): JSX.Element => { throw new Error('Bridge is not loaded!'); } - reimburseMutation.mutate(redeem); + setCancelType('reimburse'); + transaction.execute(redeem.id, true); }; const isOwner = selectedAccount?.address === redeem.userParachainAddress; @@ -198,8 +176,8 @@ const ReimburseStatusUI = ({ redeem, onClose }: Props): JSX.Element => {

{t('retry')} @@ -239,8 +217,8 @@ const ReimburseStatusUI = ({ redeem, onClose }: Props): JSX.Element => {

{t('redeem_page.reimburse')} diff --git a/src/legacy-components/RedeemUI/index.tsx b/src/legacy-components/RedeemUI/index.tsx index 34820878c5..229f1f3619 100644 --- a/src/legacy-components/RedeemUI/index.tsx +++ b/src/legacy-components/RedeemUI/index.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { ReactComponent as BitcoinLogoIcon } from '@/assets/img/bitcoin-logo.svg'; import { displayMonetaryAmountInUSDFormat, formatNumber } from '@/common/utils/utils'; +import { Flex } from '@/component-library'; import { WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains'; import AddressWithCopyUI from '@/legacy-components/AddressWithCopyUI'; import Hr2 from '@/legacy-components/hrs/Hr2'; @@ -43,7 +44,7 @@ const RedeemUI = ({ redeem, onClose }: Props): JSX.Element => { }; return ( -
+

@@ -160,7 +161,7 @@ const RedeemUI = ({ redeem, onClose }: Props): JSX.Element => {

<>{renderModalStatusPanel(redeem)} -
+ ); }; diff --git a/src/lib/substrate/components/SubstrateLoadingAndErrorHandlingWrapper/index.tsx b/src/lib/substrate/components/SubstrateLoadingAndErrorHandlingWrapper/index.tsx index 1c378b87cf..0e6a048b99 100644 --- a/src/lib/substrate/components/SubstrateLoadingAndErrorHandlingWrapper/index.tsx +++ b/src/lib/substrate/components/SubstrateLoadingAndErrorHandlingWrapper/index.tsx @@ -1,5 +1,5 @@ import { useDispatch } from 'react-redux'; -import { toast, ToastContainer } from 'react-toastify'; +import { toast } from 'react-toastify'; import { isBridgeLoaded } from '@/common/actions/general.actions'; import FullLoadingSpinner from '@/legacy-components/FullLoadingSpinner'; @@ -66,7 +66,6 @@ const SubstrateLoadingAndErrorHandlingWrapper = ({ return ( <> - {children} ); diff --git a/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx b/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx index 4baf947a1b..6b7387aeee 100644 --- a/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx +++ b/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx @@ -3,7 +3,6 @@ import { mergeProps } from '@react-aria/utils'; import Big from 'big.js'; import { ChangeEventHandler, RefObject, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { toast } from 'react-toastify'; import { displayMonetaryAmountInUSDFormat, newSafeMonetaryAmount } from '@/common/utils/utils'; import { Alert, Dd, DlGroup, Dt, Flex, TokenInput } from '@/component-library'; @@ -35,10 +34,11 @@ const isCustomAmountsMode = (form: ReturnType) => type DepositFormProps = { pool: LiquidityPool; slippageModalRef: RefObject; - onDeposit?: () => void; + onSuccess?: () => void; + onSigning?: () => void; }; -const DepositForm = ({ pool, slippageModalRef, onDeposit }: DepositFormProps): JSX.Element => { +const DepositForm = ({ pool, slippageModalRef, onSuccess, onSigning }: DepositFormProps): JSX.Element => { const { pooledCurrencies } = pool; const defaultValues = pooledCurrencies.reduce((acc, amount) => ({ ...acc, [amount.currency.ticker]: '' }), {}); @@ -52,13 +52,8 @@ const DepositForm = ({ pool, slippageModalRef, onDeposit }: DepositFormProps): J const governanceBalance = getBalance(GOVERNANCE_TOKEN.ticker)?.free || newMonetaryAmount(0, GOVERNANCE_TOKEN); const transaction = useTransaction(Transaction.AMM_ADD_LIQUIDITY, { - onSuccess: () => { - onDeposit?.(); - toast.success('Deposit successful'); - }, - onError: (error) => { - toast.error(error.message); - } + onSuccess, + onSigning }); const handleSubmit = async (data: DepositLiquidityPoolFormData) => { @@ -72,8 +67,8 @@ const DepositForm = ({ pool, slippageModalRef, onDeposit }: DepositFormProps): J const deadline = await window.bridge.system.getFutureBlockNumber(AMM_DEADLINE_INTERVAL); return transaction.execute(amounts, pool, slippage, deadline, accountId); - } catch (err: any) { - toast.error(err.toString()); + } catch (error: any) { + transaction.reject(error); } }; @@ -91,8 +86,7 @@ const DepositForm = ({ pool, slippageModalRef, onDeposit }: DepositFormProps): J const form = useForm({ initialValues: defaultValues, validationSchema: depositLiquidityPoolSchema({ transactionFee: TRANSACTION_FEE_AMOUNT, governanceBalance, tokens }), - onSubmit: handleSubmit, - disableValidation: transaction.isLoading + onSubmit: handleSubmit }); const handleChange: ChangeEventHandler = (e) => { @@ -189,7 +183,7 @@ const DepositForm = ({ pool, slippageModalRef, onDeposit }: DepositFormProps): J - + {t('amm.pools.add_liquidity')} diff --git a/src/pages/AMM/Pools/components/PoolModal/PoolModal.tsx b/src/pages/AMM/Pools/components/PoolModal/PoolModal.tsx index 2e1086a25b..b768873cff 100644 --- a/src/pages/AMM/Pools/components/PoolModal/PoolModal.tsx +++ b/src/pages/AMM/Pools/components/PoolModal/PoolModal.tsx @@ -26,11 +26,6 @@ const PoolModal = ({ pool, onClose, ...props }: PoolModalProps): JSX.Element | n return null; } - const handleAction = () => { - refetch(); - onClose?.(); - }; - return ( - + - + diff --git a/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx b/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx index 1689c20a32..961db4e3c0 100644 --- a/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx +++ b/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx @@ -1,7 +1,6 @@ import { LiquidityPool } from '@interlay/interbtc-api'; import Big from 'big.js'; import { useTranslation } from 'react-i18next'; -import { toast } from 'react-toastify'; import { formatUSD } from '@/common/utils/utils'; import { Card, Dl, DlGroup } from '@/component-library'; @@ -49,13 +48,8 @@ const PoolsInsights = ({ pools, accountPoolsData, refetch }: PoolsInsightsProps) const totalClaimableRewardUSD = calculateClaimableFarmingRewardUSD(accountPoolsData?.claimableRewards, prices); - const handleSuccess = () => { - toast.success(t('successfully_claimed_rewards')); - refetch(); - }; - const transaction = useTransaction(Transaction.AMM_CLAIM_REWARDS, { - onSuccess: handleSuccess + onSuccess: refetch }); const handleClickClaimRewards = () => accountPoolsData && transaction.execute(accountPoolsData.claimableRewards); diff --git a/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx b/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx index 2d74a356af..10d7f0fa85 100644 --- a/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx +++ b/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx @@ -2,7 +2,6 @@ import { LiquidityPool, newMonetaryAmount } from '@interlay/interbtc-api'; import Big from 'big.js'; import { RefObject, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { toast } from 'react-toastify'; import { convertMonetaryAmountToValueInUSD, @@ -28,10 +27,11 @@ import { StyledDl } from './WithdrawForm.styles'; type WithdrawFormProps = { pool: LiquidityPool; slippageModalRef: RefObject; - onWithdraw?: () => void; + onSuccess?: () => void; + onSigning?: () => void; }; -const WithdrawForm = ({ pool, slippageModalRef, onWithdraw }: WithdrawFormProps): JSX.Element => { +const WithdrawForm = ({ pool, slippageModalRef, onSuccess, onSigning }: WithdrawFormProps): JSX.Element => { const [slippage, setSlippage] = useState(0.1); const accountId = useAccountId(); @@ -40,13 +40,8 @@ const WithdrawForm = ({ pool, slippageModalRef, onWithdraw }: WithdrawFormProps) const { getBalance } = useGetBalances(); const transaction = useTransaction(Transaction.AMM_REMOVE_LIQUIDITY, { - onSuccess: () => { - onWithdraw?.(); - toast.success('Withdraw successful'); - }, - onError: (err) => { - toast.error(err.message); - } + onSuccess, + onSigning }); const { lpToken } = pool; @@ -70,8 +65,8 @@ const WithdrawForm = ({ pool, slippageModalRef, onWithdraw }: WithdrawFormProps) const deadline = await window.bridge.system.getFutureBlockNumber(AMM_DEADLINE_INTERVAL); return transaction.execute(amount, pool, slippage, deadline, accountId); - } catch (err: any) { - toast.error(err.toString()); + } catch (error: any) { + transaction.reject(error); } }; @@ -141,7 +136,7 @@ const WithdrawForm = ({ pool, slippageModalRef, onWithdraw }: WithdrawFormProps) - + {t('amm.pools.remove_liquidity')} diff --git a/src/pages/AMM/Swap/components/SwapForm/SwapCTA.tsx b/src/pages/AMM/Swap/components/SwapForm/SwapCTA.tsx index 3ef5503393..a817c120ff 100644 --- a/src/pages/AMM/Swap/components/SwapForm/SwapCTA.tsx +++ b/src/pages/AMM/Swap/components/SwapForm/SwapCTA.tsx @@ -45,8 +45,7 @@ const getProps = ( } return { - children: t('amm.swap'), - disabled: false + children: t('amm.swap') }; }; @@ -54,15 +53,14 @@ type SwapCTAProps = { pair: SwapPair; trade: Trade | null | undefined; errors: FormErrors; - loading: boolean; }; -const SwapCTA = ({ pair, trade, errors, loading }: SwapCTAProps): JSX.Element | null => { +const SwapCTA = ({ pair, trade, errors }: SwapCTAProps): JSX.Element | null => { const { t } = useTranslation(); const otherProps = getProps(pair, trade, errors, t); - return ; + return ; }; export { SwapCTA }; diff --git a/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx b/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx index a4d60bbcf6..eeccf0d580 100644 --- a/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx +++ b/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx @@ -4,7 +4,6 @@ import Big from 'big.js'; import { ChangeEventHandler, Key, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { toast } from 'react-toastify'; import { useDebounce } from 'react-use'; import { StoreType } from '@/common/types/util.types'; @@ -113,15 +112,12 @@ const SwapForm = ({ const { data: currencies } = useGetCurrencies(bridgeLoaded); const transaction = useTransaction(Transaction.AMM_SWAP, { - onSuccess: () => { - toast.success('Swap successful'); - setTrade(undefined); + onSigning: () => { setInputAmount(undefined); - onSwap(); + form.setFieldValue(SWAP_INPUT_AMOUNT_FIELD, '', true); + setTrade(undefined); }, - onError: (err) => { - toast.error(err.message); - } + onSuccess: onSwap }); useDebounce( @@ -157,12 +153,11 @@ const SwapForm = ({ try { const minimumAmountOut = trade.getMinimumOutputAmount(slippage); - const deadline = await window.bridge.system.getFutureBlockNumber(30 * 60); return transaction.execute(trade, minimumAmountOut, accountId, deadline); - } catch (err: any) { - toast.error(err.toString()); + } catch (error: any) { + transaction.reject(error); } }; @@ -193,7 +188,6 @@ const SwapForm = ({ initialValues, validationSchema: swapSchema({ [SWAP_INPUT_AMOUNT_FIELD]: inputSchemaParams }), onSubmit: handleSubmit, - disableValidation: transaction.isLoading, validateOnMount: true }); @@ -216,16 +210,6 @@ const SwapForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [pair]); - // MEMO: amount field cleaned up after successful swap - useEffect(() => { - const isAmountFieldEmpty = form.values[SWAP_INPUT_AMOUNT_FIELD] === ''; - - if (isAmountFieldEmpty || !transaction.isSuccess) return; - - form.setFieldValue(SWAP_INPUT_AMOUNT_FIELD, ''); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [transaction.isSuccess]); - const handleChangeInput: ChangeEventHandler = (e) => { setInputAmount(e.target.value); setTrade(undefined); @@ -322,7 +306,7 @@ const SwapForm = ({ /> {trade && } - + diff --git a/src/pages/Bridge/BurnForm/index.tsx b/src/pages/Bridge/BurnForm/index.tsx index 2063218a57..4699f19aa7 100644 --- a/src/pages/Bridge/BurnForm/index.tsx +++ b/src/pages/Bridge/BurnForm/index.tsx @@ -6,7 +6,6 @@ import { useErrorHandler, withErrorBoundary } from 'react-error-boundary'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { toast } from 'react-toastify'; import { ParachainStatus, StoreType } from '@/common/types/util.types'; import { displayMonetaryAmountInUSDFormat } from '@/common/utils/utils'; @@ -15,7 +14,6 @@ import { AuthCTA } from '@/components'; import { WRAPPED_TOKEN, WRAPPED_TOKEN_SYMBOL, WrappedTokenLogoIcon } from '@/config/relay-chains'; import { BALANCE_MAX_INTEGER_LENGTH } from '@/constants'; import ErrorFallback from '@/legacy-components/ErrorFallback'; -import ErrorModal from '@/legacy-components/ErrorModal'; import FormTitle from '@/legacy-components/FormTitle'; import Hr2 from '@/legacy-components/hrs/Hr2'; import PriceInfo from '@/legacy-components/PriceInfo'; @@ -70,10 +68,12 @@ const BurnForm = (): JSX.Element | null => { const [burnableCollateral, setBurnableCollateral] = React.useState(); const [selectedCollateral, setSelectedCollateral] = React.useState(); - const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE); - const [submitError, setSubmitError] = React.useState(null); - - const transaction = useTransaction(Transaction.REDEEM_BURN); + const transaction = useTransaction(Transaction.REDEEM_BURN, { + onSuccess: () => + reset({ + [WRAPPED_TOKEN_AMOUNT]: '' + }) + }); const handleUpdateCollateral = (collateral: TokenOption) => { const selectedCollateral = burnableCollateral?.find( @@ -128,18 +128,6 @@ const BurnForm = (): JSX.Element | null => { })(); }, [bridgeLoaded, collateralCurrencies, handleError]); - // This ensures that triggering the notification and clearing - // the form happen at the same time. - React.useEffect(() => { - if (submitStatus !== STATUSES.RESOLVED) return; - - toast.success(t('burn_page.successfully_burned')); - - reset({ - [WRAPPED_TOKEN_AMOUNT]: '' - }); - }, [submitStatus, reset, t]); - if (status === STATUSES.IDLE || status === STATUSES.PENDING) { return ; } @@ -149,18 +137,8 @@ const BurnForm = (): JSX.Element | null => { throw new Error('Something went wrong!'); } - const onSubmit = async (data: BurnFormData) => { - try { - setSubmitStatus(STATUSES.PENDING); - - await transaction.executeAsync(new BitcoinAmount(data[WRAPPED_TOKEN_AMOUNT]), selectedCollateral.currency); - - setSubmitStatus(STATUSES.RESOLVED); - } catch (error) { - setSubmitStatus(STATUSES.REJECTED); - setSubmitError(error); - } - }; + const onSubmit = async (data: BurnFormData) => + transaction.execute(new BitcoinAmount(data[WRAPPED_TOKEN_AMOUNT]), selectedCollateral.currency); const validateForm = (value: string): string | undefined => { // TODO: should use wrapped token amount type (e.g. InterBtcAmount or KBtcAmount) @@ -305,23 +283,12 @@ const BurnForm = (): JSX.Element | null => { fullWidth size='large' type='submit' - loading={submitStatus === STATUSES.PENDING} + loading={transaction.isLoading} disabled={parachainStatus === ParachainStatus.Loading || parachainStatus === ParachainStatus.Shutdown} > {t('burn')} - {submitStatus === STATUSES.REJECTED && submitError && ( - { - setSubmitStatus(STATUSES.IDLE); - setSubmitError(null); - }} - title='Error' - description={typeof submitError === 'string' ? submitError : submitError.message} - /> - )} ); } diff --git a/src/pages/Bridge/IssueForm/SubmittedIssueRequestModal/index.tsx b/src/pages/Bridge/IssueForm/SubmittedIssueRequestModal/index.tsx index b8569c7ee6..e5afdc0146 100644 --- a/src/pages/Bridge/IssueForm/SubmittedIssueRequestModal/index.tsx +++ b/src/pages/Bridge/IssueForm/SubmittedIssueRequestModal/index.tsx @@ -1,12 +1,11 @@ import { Issue } from '@interlay/interbtc-api'; import clsx from 'clsx'; -import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import CloseIconButton from '@/legacy-components/buttons/CloseIconButton'; +import { Modal, ModalBody, ModalFooter } from '@/component-library'; import InterlayDefaultContainedButton from '@/legacy-components/buttons/InterlayDefaultContainedButton'; import BTCPaymentPendingStatusUI from '@/legacy-components/IssueUI/BTCPaymentPendingStatusUI'; -import InterlayModal, { InterlayModalInnerWrapper, Props as ModalProps } from '@/legacy-components/UI/InterlayModal'; +import { Props as ModalProps } from '@/legacy-components/UI/InterlayModal'; import InterlayRouterLink from '@/legacy-components/UI/InterlayRouterLink'; import { PAGES, QUERY_PARAMETERS } from '@/utils/constants/links'; import { getColorShade } from '@/utils/helpers/colors'; @@ -24,32 +23,31 @@ const SubmittedIssueRequestModal = ({ }: CustomProps & Omit): JSX.Element => { const { t } = useTranslation(); - const focusRef = React.useRef(null); - return ( - - - + +

{t('issue_page.deposit')}

- - - {t('issue_page.i_have_made_the_payment')} - -
-
-
+ + + + + {t('issue_page.i_have_made_the_payment')} + + {' '} + +
); }; diff --git a/src/pages/Bridge/IssueForm/index.tsx b/src/pages/Bridge/IssueForm/index.tsx index 2e3b83db45..fbffaaae14 100644 --- a/src/pages/Bridge/IssueForm/index.tsx +++ b/src/pages/Bridge/IssueForm/index.tsx @@ -42,7 +42,6 @@ import { } from '@/config/relay-chains'; import AvailableBalanceUI from '@/legacy-components/AvailableBalanceUI'; import ErrorFallback from '@/legacy-components/ErrorFallback'; -import ErrorModal from '@/legacy-components/ErrorModal'; import FormTitle from '@/legacy-components/FormTitle'; import Hr2 from '@/legacy-components/hrs/Hr2'; import PriceInfo from '@/legacy-components/PriceInfo'; @@ -126,7 +125,6 @@ const IssueForm = (): JSX.Element | null => { ); const [dustValue, setDustValue] = React.useState(new BitcoinAmount(DEFAULT_ISSUE_DUST_AMOUNT)); const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE); - const [submitError, setSubmitError] = React.useState(null); const [submittedRequest, setSubmittedRequest] = React.useState(); const [selectVaultManually, setSelectVaultManually] = React.useState(false); const [selectedVault, setSelectedVault] = React.useState(); @@ -142,7 +140,7 @@ const IssueForm = (): JSX.Element | null => { }); useErrorHandler(requestLimitsError); - const transaction = useTransaction(Transaction.ISSUE_REQUEST); + const transaction = useTransaction(Transaction.ISSUE_REQUEST, { showSuccessModal: false }); React.useEffect(() => { if (!bridgeLoaded) return; @@ -303,43 +301,38 @@ const IssueForm = (): JSX.Element | null => { }; const onSubmit = async (data: IssueFormData) => { - try { - setSubmitStatus(STATUSES.PENDING); - await requestLimitsRefetch(); - await trigger(BTC_AMOUNT); - - const monetaryBtcAmount = new BitcoinAmount(data[BTC_AMOUNT] || '0'); - const vaults = await window.bridge.vaults.getVaultsWithIssuableTokens(); - - let vaultId: InterbtcPrimitivesVaultId; - if (selectVaultManually) { - if (!selectedVault) { - throw new Error('Specific vault is not selected!'); - } - vaultId = selectedVault[0]; - } else { - vaultId = getRandomVaultIdWithCapacity(Array.from(vaults), monetaryBtcAmount); - } + setSubmitStatus(STATUSES.PENDING); + await requestLimitsRefetch(); + await trigger(BTC_AMOUNT); - const collateralToken = await currencyIdToMonetaryCurrency(window.bridge.api, vaultId.currencies.collateral); - - const result = await transaction.executeAsync( - monetaryBtcAmount, - vaultId.accountId, - collateralToken, - false, // default - vaults - ); - const issueRequests = await getIssueRequestsFromExtrinsicResult(window.bridge, result); - - // TODO: handle issue aggregation - const issueRequest = issueRequests[0]; - handleSubmittedRequestModalOpen(issueRequest); - setSubmitStatus(STATUSES.RESOLVED); - } catch (error) { - setSubmitStatus(STATUSES.REJECTED); - setSubmitError(error); + const monetaryBtcAmount = new BitcoinAmount(data[BTC_AMOUNT] || '0'); + const vaults = await window.bridge.vaults.getVaultsWithIssuableTokens(); + + let vaultId: InterbtcPrimitivesVaultId; + if (selectVaultManually) { + if (!selectedVault) { + throw new Error('Specific vault is not selected!'); + } + vaultId = selectedVault[0]; + } else { + vaultId = getRandomVaultIdWithCapacity(Array.from(vaults), monetaryBtcAmount); } + + const collateralToken = await currencyIdToMonetaryCurrency(window.bridge.api, vaultId.currencies.collateral); + + const result = await transaction.executeAsync( + monetaryBtcAmount, + vaultId.accountId, + collateralToken, + false, // default + vaults + ); + const issueRequests = await getIssueRequestsFromExtrinsicResult(window.bridge, result.data); + + // TODO: handle issue aggregation + const issueRequest = issueRequests[0]; + handleSubmittedRequestModalOpen(issueRequest); + setSubmitStatus(STATUSES.RESOLVED); }; const monetaryBtcAmount = new BitcoinAmount(btcAmount); @@ -536,17 +529,6 @@ const IssueForm = (): JSX.Element | null => { {t('confirm')}
- {submitStatus === STATUSES.REJECTED && submitError && ( - { - setSubmitStatus(STATUSES.IDLE); - setSubmitError(null); - }} - title='Error' - description={typeof submitError === 'string' ? submitError : submitError.message} - /> - )} {submittedRequest && ( - - + +

{t('redeem_page.redeem')} @@ -114,8 +110,8 @@ const SubmittedRedeemRequestModal = ({

-
- + + ); }; diff --git a/src/pages/Bridge/RedeemForm/index.tsx b/src/pages/Bridge/RedeemForm/index.tsx index 357bfcf540..f934ae0a99 100644 --- a/src/pages/Bridge/RedeemForm/index.tsx +++ b/src/pages/Bridge/RedeemForm/index.tsx @@ -35,7 +35,6 @@ import { import { BALANCE_MAX_INTEGER_LENGTH, BTC_ADDRESS_REGEX } from '@/constants'; import AvailableBalanceUI from '@/legacy-components/AvailableBalanceUI'; import ErrorFallback from '@/legacy-components/ErrorFallback'; -import ErrorModal from '@/legacy-components/ErrorModal'; import FormTitle from '@/legacy-components/FormTitle'; import Hr2 from '@/legacy-components/hrs/Hr2'; import PriceInfo from '@/legacy-components/PriceInfo'; @@ -112,14 +111,13 @@ const RedeemForm = (): JSX.Element | null => { const [premiumRedeemFee, setPremiumRedeemFee] = React.useState(new Big(0)); const [currentInclusionFee, setCurrentInclusionFee] = React.useState(BitcoinAmount.zero()); const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE); - const [submitError, setSubmitError] = React.useState(null); const [submittedRequest, setSubmittedRequest] = React.useState(); const [selectVaultManually, setSelectVaultManually] = React.useState(false); const [selectedVault, setSelectedVault] = React.useState(); - const transaction = useTransaction(Transaction.REDEEM_REQUEST); + const transaction = useTransaction(Transaction.REDEEM_REQUEST, { showSuccessModal: false }); React.useEffect(() => { if (!monetaryWrappedTokenAmount) return; @@ -305,7 +303,7 @@ const RedeemForm = (): JSX.Element | null => { const result = await transaction.executeAsync(monetaryWrappedTokenAmount, data[BTC_ADDRESS], vaultId); - const redeemRequests = await getRedeemRequestsFromExtrinsicResult(window.bridge, result); + const redeemRequests = await getRedeemRequestsFromExtrinsicResult(window.bridge, result.data); // TODO: handle redeem aggregator const redeemRequest = redeemRequests[0]; @@ -313,7 +311,6 @@ const RedeemForm = (): JSX.Element | null => { setSubmitStatus(STATUSES.RESOLVED); } catch (error) { setSubmitStatus(STATUSES.REJECTED); - setSubmitError(error); } }; @@ -533,17 +530,6 @@ const RedeemForm = (): JSX.Element | null => { {t('confirm')} - {submitStatus === STATUSES.REJECTED && submitError && ( - { - setSubmitStatus(STATUSES.IDLE); - setSubmitError(null); - }} - title='Error' - description={typeof submitError === 'string' ? submitError : submitError.message} - /> - )} {submittedRequest && ( { - toast.success('Successfully toggled collateral'); - onClose?.(); - refetch(); - } + onSigning: onClose, + onSuccess: refetch }); if (!asset || !position) { @@ -94,31 +89,21 @@ const CollateralModal = ({ asset, position, onClose, ...props }: CollateralModal }; return ( - <> - - {content.title} - - - {content.description} - - {variant !== 'disable-error' && } - - - - - {content.buttonLabel} - - - - {transaction.isError && ( - transaction.reset()} - title='Error' - description={transaction.error?.message || ''} - /> - )} - + + {content.title} + + + {content.description} + + {variant !== 'disable-error' && } + + + + + {content.buttonLabel} + + + ); }; diff --git a/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx b/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx index edc7763901..390be8cba5 100644 --- a/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx +++ b/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx @@ -3,7 +3,6 @@ import { MonetaryAmount } from '@interlay/monetary-js'; import { mergeProps } from '@react-aria/utils'; import { ChangeEventHandler, useState } from 'react'; import { TFunction, useTranslation } from 'react-i18next'; -import { toast } from 'react-toastify'; import { useDebounce } from 'react-use'; import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/common/utils/utils'; @@ -116,42 +115,29 @@ const LoanForm = ({ asset, variant, position, onChangeLoan }: LoanFormProps): JS [inputAmount] ); - const transaction = useTransaction({ - onSuccess: () => { - toast.success(`Successful ${content.title.toLowerCase()}`); - onChangeLoan?.(); - refetch(); - }, - onError: (error: Error) => { - toast.error(error.message); - } - }); + const transaction = useTransaction({ onSigning: onChangeLoan, onSuccess: refetch }); const handleSubmit = (data: LoanFormData) => { - try { - const amount = data[variant] || 0; - const monetaryAmount = newMonetaryAmount(amount, asset.currency, true); - - switch (variant) { - case 'lend': - return transaction.execute(Transaction.LOANS_LEND, monetaryAmount.currency, monetaryAmount); - case 'withdraw': - if (isMaxAmount) { - return transaction.execute(Transaction.LOANS_WITHDRAW_ALL, monetaryAmount.currency); - } else { - return transaction.execute(Transaction.LOANS_WITHDRAW, monetaryAmount.currency, monetaryAmount); - } - case 'borrow': - return transaction.execute(Transaction.LOANS_BORROW, monetaryAmount.currency, monetaryAmount); - case 'repay': - if (isMaxAmount) { - return transaction.execute(Transaction.LOANS_REPAY_ALL, monetaryAmount.currency); - } else { - return transaction.execute(Transaction.LOANS_REPAY, monetaryAmount.currency, monetaryAmount); - } - } - } catch (err: any) { - toast.error(err.toString()); + const amount = data[variant] || 0; + const monetaryAmount = newMonetaryAmount(amount, asset.currency, true); + + switch (variant) { + case 'lend': + return transaction.execute(Transaction.LOANS_LEND, monetaryAmount.currency, monetaryAmount); + case 'withdraw': + if (isMaxAmount) { + return transaction.execute(Transaction.LOANS_WITHDRAW_ALL, monetaryAmount.currency); + } else { + return transaction.execute(Transaction.LOANS_WITHDRAW, monetaryAmount.currency, monetaryAmount); + } + case 'borrow': + return transaction.execute(Transaction.LOANS_BORROW, monetaryAmount.currency, monetaryAmount); + case 'repay': + if (isMaxAmount) { + return transaction.execute(Transaction.LOANS_REPAY_ALL, monetaryAmount.currency); + } else { + return transaction.execute(Transaction.LOANS_REPAY, monetaryAmount.currency, monetaryAmount); + } } }; @@ -216,7 +202,7 @@ const LoanForm = ({ asset, variant, position, onChangeLoan }: LoanFormProps): JS - + {content.title} diff --git a/src/pages/Loans/LoansOverview/components/LoansInsights/LoansInsights.tsx b/src/pages/Loans/LoansOverview/components/LoansInsights/LoansInsights.tsx index 6ad85f8d82..2ef53674f4 100644 --- a/src/pages/Loans/LoansOverview/components/LoansInsights/LoansInsights.tsx +++ b/src/pages/Loans/LoansOverview/components/LoansInsights/LoansInsights.tsx @@ -1,10 +1,6 @@ -import { useTranslation } from 'react-i18next'; -import { toast } from 'react-toastify'; - import { formatNumber, formatPercentage, formatUSD } from '@/common/utils/utils'; import { Card, Dl, DlGroup } from '@/component-library'; import { AuthCTA } from '@/components'; -import ErrorModal from '@/legacy-components/ErrorModal'; import { AccountLendingStatistics } from '@/utils/hooks/api/loans/use-get-account-lending-statistics'; import { useGetAccountSubsidyRewards } from '@/utils/hooks/api/loans/use-get-account-subsidy-rewards'; import { Transaction, useTransaction } from '@/utils/hooks/transaction'; @@ -16,14 +12,10 @@ type LoansInsightsProps = { }; const LoansInsights = ({ statistics }: LoansInsightsProps): JSX.Element => { - const { t } = useTranslation(); const { data: subsidyRewards, refetch } = useGetAccountSubsidyRewards(); const transaction = useTransaction(Transaction.LOANS_CLAIM_REWARDS, { - onSuccess: () => { - toast.success(t('successfully_claimed_rewards')); - refetch(); - } + onSuccess: refetch }); const handleClickClaimRewards = () => transaction.execute(); @@ -76,14 +68,6 @@ const LoansInsights = ({ statistics }: LoansInsightsProps): JSX.Element => { )}
- {transaction.isError && ( - transaction.reset()} - title='Error' - description={transaction.error?.message || ''} - /> - )} ); }; diff --git a/src/pages/Staking/ClaimRewardsButton/index.tsx b/src/pages/Staking/ClaimRewardsButton/index.tsx index 442da162c0..e7ab257735 100644 --- a/src/pages/Staking/ClaimRewardsButton/index.tsx +++ b/src/pages/Staking/ClaimRewardsButton/index.tsx @@ -5,7 +5,6 @@ import { GOVERNANCE_TOKEN_SYMBOL } from '@/config/relay-chains'; import InterlayDenimOrKintsugiSupernovaContainedButton, { Props as InterlayDenimOrKintsugiMidnightContainedButtonProps } from '@/legacy-components/buttons/InterlayDenimOrKintsugiSupernovaContainedButton'; -import ErrorModal from '@/legacy-components/ErrorModal'; import { useSubstrateSecureState } from '@/lib/substrate'; import { GENERIC_FETCHER } from '@/services/fetchers/generic-fetcher'; import { Transaction, useTransaction } from '@/utils/hooks/transaction'; @@ -35,26 +34,14 @@ const ClaimRewardsButton = ({ }; return ( - <> - - Claim {claimableRewardAmount} {GOVERNANCE_TOKEN_SYMBOL} Rewards - - {transaction.isError && ( - { - transaction.reset(); - }} - title='Error' - description={transaction.error?.message || ''} - /> - )} - + + Claim {claimableRewardAmount} {GOVERNANCE_TOKEN_SYMBOL} Rewards + ); }; diff --git a/src/pages/Staking/WithdrawButton/index.tsx b/src/pages/Staking/WithdrawButton/index.tsx index 7093017a52..190d2a628c 100644 --- a/src/pages/Staking/WithdrawButton/index.tsx +++ b/src/pages/Staking/WithdrawButton/index.tsx @@ -1,19 +1,17 @@ -import { ISubmittableResult } from '@polkadot/types/types'; import clsx from 'clsx'; import { add, format } from 'date-fns'; -import { useMutation, useQueryClient } from 'react-query'; +import { useQueryClient } from 'react-query'; import { BLOCK_TIME } from '@/config/parachain'; import { GOVERNANCE_TOKEN_SYMBOL } from '@/config/relay-chains'; import InterlayDenimOrKintsugiSupernovaContainedButton, { Props as InterlayDenimOrKintsugiMidnightContainedButtonProps } from '@/legacy-components/buttons/InterlayDenimOrKintsugiSupernovaContainedButton'; -import ErrorModal from '@/legacy-components/ErrorModal'; import InformationTooltip from '@/legacy-components/tooltips/InformationTooltip'; import { useSubstrateSecureState } from '@/lib/substrate'; import { GENERIC_FETCHER } from '@/services/fetchers/generic-fetcher'; import { YEAR_MONTH_DAY_PATTERN } from '@/utils/constants/date-time'; -import { submitExtrinsic } from '@/utils/helpers/extrinsic'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; const getFormattedUnlockDate = (remainingBlockNumbersToUnstake: number, formatPattern: string) => { const unlockDate = add(new Date(), { @@ -36,22 +34,15 @@ const WithdrawButton = ({ }: CustomProps & InterlayDenimOrKintsugiMidnightContainedButtonProps): JSX.Element => { const { selectedAccount } = useSubstrateSecureState(); - const queryClient = useQueryClient(); - - const withdrawMutation = useMutation( - () => { - return submitExtrinsic(window.bridge.escrow.withdraw()); - }, - { - onSuccess: () => { - queryClient.invalidateQueries([GENERIC_FETCHER, 'escrow', 'getStakedBalance', selectedAccount?.address]); - } + const transaction = useTransaction(Transaction.ESCROW_WITHDRAW, { + onSuccess: () => { + queryClient.invalidateQueries([GENERIC_FETCHER, 'escrow', 'getStakedBalance', selectedAccount?.address]); } - ); + }); - const handleUnstake = () => { - withdrawMutation.mutate(); - }; + const queryClient = useQueryClient(); + + const handleUnstake = () => transaction.execute(); const disabled = remainingBlockNumbersToUnstake ? remainingBlockNumbersToUnstake > 0 : false; @@ -79,22 +70,12 @@ const WithdrawButton = ({ /> } onClick={handleUnstake} - pending={withdrawMutation.isLoading} + pending={transaction.isLoading} disabled={disabled} {...rest} > Withdraw Staked {GOVERNANCE_TOKEN_SYMBOL} {renderUnlockDateLabel()} - {withdrawMutation.isError && ( - { - withdrawMutation.reset(); - }} - title='Error' - description={withdrawMutation.error?.message || ''} - /> - )} ); }; diff --git a/src/pages/Staking/index.tsx b/src/pages/Staking/index.tsx index 043d6b1185..df2f0b697f 100644 --- a/src/pages/Staking/index.tsx +++ b/src/pages/Staking/index.tsx @@ -29,7 +29,6 @@ import { } from '@/config/relay-chains'; import AvailableBalanceUI from '@/legacy-components/AvailableBalanceUI'; import ErrorFallback from '@/legacy-components/ErrorFallback'; -import ErrorModal from '@/legacy-components/ErrorModal'; import Panel from '@/legacy-components/Panel'; import TitleWithUnderline from '@/legacy-components/TitleWithUnderline'; import TokenField from '@/legacy-components/TokenField'; @@ -837,17 +836,6 @@ const Staking = (): JSX.Element => { - {(initialStakeTransaction.isError || existingStakeTransaction.isError) && ( - { - initialStakeTransaction.reset(); - existingStakeTransaction.reset(); - }} - title='Error' - description={initialStakeTransaction.error?.message || existingStakeTransaction.error?.message || ''} - /> - )} ); }; diff --git a/src/pages/Transactions/IssueRequestsTable/IssueRequestModal/index.tsx b/src/pages/Transactions/IssueRequestsTable/IssueRequestModal/index.tsx index c1457d5a8d..765659e2d9 100644 --- a/src/pages/Transactions/IssueRequestsTable/IssueRequestModal/index.tsx +++ b/src/pages/Transactions/IssueRequestsTable/IssueRequestModal/index.tsx @@ -1,13 +1,8 @@ -import clsx from 'clsx'; -import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import CloseIconButton from '@/legacy-components/buttons/CloseIconButton'; -import Hr1 from '@/legacy-components/hrs/Hr1'; +import { Modal, ModalBody, ModalHeader } from '@/component-library'; import IssueUI from '@/legacy-components/IssueUI'; -import InterlayModal, { InterlayModalInnerWrapper, Props as ModalProps } from '@/legacy-components/UI/InterlayModal'; - -import RequestModalTitle from '../../RequestModalTitle'; +import { Props as ModalProps } from '@/legacy-components/UI/InterlayModal'; interface CustomProps { request: any; // TODO: should type properly (`Relay`) @@ -16,17 +11,13 @@ interface CustomProps { const IssueRequestModal = ({ open, onClose, request }: CustomProps & Omit): JSX.Element => { const { t } = useTranslation(); - const focusRef = React.useRef(null); - return ( - - - {t('issue_page.request', { id: request.id })} - - + + {t('issue_page.request', { id: request.id })} + - - + + ); }; diff --git a/src/pages/Transactions/RedeemRequestsTable/RedeemRequestModal/index.tsx b/src/pages/Transactions/RedeemRequestsTable/RedeemRequestModal/index.tsx index ccc3fba223..fee187b468 100644 --- a/src/pages/Transactions/RedeemRequestsTable/RedeemRequestModal/index.tsx +++ b/src/pages/Transactions/RedeemRequestsTable/RedeemRequestModal/index.tsx @@ -1,13 +1,8 @@ -import clsx from 'clsx'; -import * as React from 'react'; import { useTranslation } from 'react-i18next'; -import CloseIconButton from '@/legacy-components/buttons/CloseIconButton'; -import Hr1 from '@/legacy-components/hrs/Hr1'; +import { Modal, ModalBody, ModalHeader } from '@/component-library'; import RedeemUI from '@/legacy-components/RedeemUI'; -import InterlayModal, { InterlayModalInnerWrapper, Props as ModalProps } from '@/legacy-components/UI/InterlayModal'; - -import RequestModalTitle from '../../RequestModalTitle'; +import { Props as ModalProps } from '@/legacy-components/UI/InterlayModal'; interface CustomProps { // TODO: should type properly (`Relay`) @@ -21,17 +16,13 @@ const RedeemRequestModal = ({ }: CustomProps & Omit): JSX.Element | null => { const { t } = useTranslation(); - const focusRef = React.useRef(null); - return ( - - - {t('issue_page.request', { id: request.id })} - - + + {t('issue_page.request', { id: request.id })} + - - + + ); }; diff --git a/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx b/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx index 1e7a864185..fbd54a3cd8 100644 --- a/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx +++ b/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx @@ -1,12 +1,9 @@ -import { FixedPointNumber } from '@acala-network/sdk-core'; -import { ChainName, CrossChainTransferParams } from '@interlay/bridge'; +import { ChainName } from '@interlay/bridge'; import { newMonetaryAmount } from '@interlay/interbtc-api'; import { web3FromAddress } from '@polkadot/extension-dapp'; import { mergeProps } from '@react-aria/utils'; import { ChangeEventHandler, Key, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useMutation } from 'react-query'; -import { toast } from 'react-toastify'; import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/common/utils/utils'; import { Dd, DlGroup, Dt, Flex, LoadingSpinner, TokenInput } from '@/component-library'; @@ -25,11 +22,11 @@ import { } from '@/lib/form'; import { useSubstrateSecureState } from '@/lib/substrate'; import { Chains } from '@/types/chains'; -import { submitExtrinsic } from '@/utils/helpers/extrinsic'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetCurrencies } from '@/utils/hooks/api/use-get-currencies'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; import { useXCMBridge, XCMTokenData } from '@/utils/hooks/api/xcm/use-xcm-bridge'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import useAccountId from '@/utils/hooks/use-account-id'; import { ChainSelect } from './components'; @@ -65,37 +62,36 @@ const CrossChainTransferForm = (): JSX.Element => { } }; - const mutateXcmTransfer = async (formData: CrossChainTransferFormData) => { + const transaction = useTransaction(Transaction.XCM_TRANSFER, { + onSuccess: () => { + setTokenData(form.values[CROSS_CHAIN_TRANSFER_TO_FIELD] as ChainName); + form.setFieldValue(CROSS_CHAIN_TRANSFER_AMOUNT_FIELD, ''); + } + }); + + const handleSubmit = async (formData: CrossChainTransferFormData) => { if (!data || !formData || !currentToken) return; - const { signer } = await web3FromAddress(formData[CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD] as string); + const address = formData[CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD] as string; + + const { signer } = await web3FromAddress(address); const adapter = data.bridge.findAdapter(formData[CROSS_CHAIN_TRANSFER_FROM_FIELD] as ChainName); const apiPromise = data.provider.getApiPromise(formData[CROSS_CHAIN_TRANSFER_FROM_FIELD] as string); apiPromise.setSigner(signer); adapter.setApi(apiPromise); + const transferCurrency = getCurrencyFromTicker(currentToken.value); const transferAmount = newMonetaryAmount( form.values[CROSS_CHAIN_TRANSFER_AMOUNT_FIELD] || 0, - getCurrencyFromTicker(currentToken.value), + transferCurrency, true ); - const transferAmountString = transferAmount.toString(true); - const transferAmountDecimals = transferAmount.currency.decimals; - - const tx = adapter.createTx({ - amount: FixedPointNumber.fromInner(transferAmountString, transferAmountDecimals), - to: formData[CROSS_CHAIN_TRANSFER_TO_FIELD], - token: formData[CROSS_CHAIN_TRANSFER_TOKEN_FIELD], - address: formData[CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD] - } as CrossChainTransferParams); - - await submitExtrinsic({ extrinsic: tx }); - }; + const fromChain = formData[CROSS_CHAIN_TRANSFER_FROM_FIELD] as ChainName; + const toChain = formData[CROSS_CHAIN_TRANSFER_TO_FIELD] as ChainName; - const handleSubmit = (formData: CrossChainTransferFormData) => { - xcmTransferMutation.mutate(formData); + transaction.execute(adapter, fromChain, toChain, address, transferAmount); }; const form = useForm({ @@ -108,18 +104,6 @@ const CrossChainTransferForm = (): JSX.Element => { validationSchema: crossChainTransferSchema(schema, t) }); - const xcmTransferMutation = useMutation(mutateXcmTransfer, { - onSuccess: async () => { - toast.success('Transfer successful'); - - setTokenData(form.values[CROSS_CHAIN_TRANSFER_TO_FIELD] as ChainName); - form.setFieldValue(CROSS_CHAIN_TRANSFER_AMOUNT_FIELD, ''); - }, - onError: (err) => { - toast.error(err.message); - } - }); - const handleOriginatingChainChange = (chain: ChainName, name: string) => { form.setFieldValue(name, chain); @@ -238,9 +222,7 @@ const CrossChainTransferForm = (): JSX.Element => { onSelectionChange={(chain: Key) => handleOriginatingChainChange(chain as ChainName, CROSS_CHAIN_TRANSFER_FROM_FIELD) } - {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_FROM_FIELD, false), { - onChange: handleOriginatingChainChange - })} + {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_FROM_FIELD, false))} /> { onSelectionChange={(chain: Key) => handleDestinationChainChange(chain as ChainName, CROSS_CHAIN_TRANSFER_TO_FIELD) } - {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_TO_FIELD, false), { - onChange: handleDestinationChainChange - })} + {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_TO_FIELD, false))} />
@@ -290,7 +270,7 @@ const CrossChainTransferForm = (): JSX.Element => {
{`${currentToken?.destFee.toString()} ${currentToken?.value}`}
- + {isCTADisabled ? 'Enter transfer amount' : t('transfer')} diff --git a/src/pages/Transfer/TransferForm/index.tsx b/src/pages/Transfer/TransferForm/index.tsx index a488cd288f..2bc9ed3f19 100644 --- a/src/pages/Transfer/TransferForm/index.tsx +++ b/src/pages/Transfer/TransferForm/index.tsx @@ -5,19 +5,16 @@ import { withErrorBoundary } from 'react-error-boundary'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { toast } from 'react-toastify'; import { ParachainStatus, StoreType } from '@/common/types/util.types'; import { formatNumber } from '@/common/utils/utils'; import { AuthCTA } from '@/components'; import ErrorFallback from '@/legacy-components/ErrorFallback'; -import ErrorModal from '@/legacy-components/ErrorModal'; import FormTitle from '@/legacy-components/FormTitle'; import TextField from '@/legacy-components/TextField'; import Tokens, { TokenOption } from '@/legacy-components/Tokens'; import InterlayButtonBase from '@/legacy-components/UI/InterlayButtonBase'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; -import STATUSES from '@/utils/constants/statuses'; import isValidPolkadotAddress from '@/utils/helpers/is-valid-polkadot-address'; import { Transaction, useTransaction } from '@/utils/hooks/transaction'; @@ -47,28 +44,21 @@ const TransferForm = (): JSX.Element => { }); const [activeToken, setActiveToken] = React.useState(undefined); - const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE); - const [submitError, setSubmitError] = React.useState(null); - const transaction = useTransaction(Transaction.TOKENS_TRANSFER); + const transaction = useTransaction(Transaction.TOKENS_TRANSFER, { + onSigning: () => { + reset({ + [TRANSFER_AMOUNT]: '', + [RECIPIENT_ADDRESS]: '' + }); + } + }); const onSubmit = async (data: TransferFormData) => { if (!activeToken) return; if (data[TRANSFER_AMOUNT] === undefined) return; - try { - setSubmitStatus(STATUSES.PENDING); - - await transaction.executeAsync( - data[RECIPIENT_ADDRESS], - newMonetaryAmount(data[TRANSFER_AMOUNT], activeToken.token, true) - ); - - setSubmitStatus(STATUSES.RESOLVED); - } catch (error) { - setSubmitStatus(STATUSES.REJECTED); - setSubmitError(error); - } + transaction.execute(data[RECIPIENT_ADDRESS], newMonetaryAmount(data[TRANSFER_AMOUNT], activeToken.token, true)); }; const validateTransferAmount = React.useCallback( @@ -96,19 +86,6 @@ const TransferForm = (): JSX.Element => { const handleClickBalance = () => setValue(TRANSFER_AMOUNT, activeToken?.transferableBalance || ''); - // This ensures that triggering the notification and clearing - // the form happen at the same time. - React.useEffect(() => { - if (submitStatus !== STATUSES.RESOLVED) return; - - toast.success(t('transfer_page.successfully_transferred')); - - reset({ - [TRANSFER_AMOUNT]: '', - [RECIPIENT_ADDRESS]: '' - }); - }, [submitStatus, reset, t]); - return ( <>
@@ -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..4b7aa388e4 100644 --- a/src/parts/Topbar/index.tsx +++ b/src/parts/Topbar/index.tsx @@ -9,7 +9,7 @@ import { toast } from 'react-toastify'; import { showAccountModalAction, showSignTermsModalAction } from '@/common/actions/general.actions'; import { StoreType } from '@/common/types/util.types'; -import { FundWallet } from '@/components'; +import { FundWallet, NotificationsPopover } from '@/components'; import { AuthModal, SignTermsModal } from '@/components/AuthModal'; import { ACCOUNT_ID_TYPE_NAME } from '@/config/general'; import { GOVERNANCE_TOKEN } from '@/config/relay-chains'; @@ -21,6 +21,7 @@ import Tokens from '@/legacy-components/Tokens'; import InterlayLink from '@/legacy-components/UI/InterlayLink'; import { KeyringPair, useSubstrate, useSubstrateSecureState } from '@/lib/substrate'; import { BitcoinNetwork } from '@/types/bitcoin'; +import { useNotifications } from '@/utils/context/Notifications'; import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; import { FeatureFlags, useFeatureFlag } from '@/utils/hooks/use-feature-flag'; import { useSignMessage } from '@/utils/hooks/use-sign-message'; @@ -38,6 +39,7 @@ const Topbar = (): JSX.Element => { const isBanxaEnabled = useFeatureFlag(FeatureFlags.BANXA); const { setSelectedAccount, removeSelectedAccount } = useSubstrate(); const { selectProps } = useSignMessage(); + const { list } = useNotifications(); const kintBalanceIsZero = getAvailableBalance('KINT')?.isZero(); @@ -47,6 +49,7 @@ const Topbar = (): JSX.Element => { try { const receiverId = window.bridge.api.createType(ACCOUNT_ID_TYPE_NAME, selectedAccount.address); await window.faucet.fundAccount(receiverId, GOVERNANCE_TOKEN); + // TODO: show new notification toast.success('Your account has been funded.'); } catch (error) { toast.error(`Funding failed. ${error.message}`); @@ -134,6 +137,7 @@ const Topbar = (): JSX.Element => { )} + {accountLabel} diff --git a/src/utils/constants/links.ts b/src/utils/constants/links.ts index c6cd038371..7a62ca51f9 100644 --- a/src/utils/constants/links.ts +++ b/src/utils/constants/links.ts @@ -1,4 +1,5 @@ import { BANXA_LINK } from '@/config/links'; +import { SUBSCAN_LINK } from '@/config/relay-chains'; const URL_PARAMETERS = Object.freeze({ VAULT: { @@ -35,8 +36,24 @@ const PAGES = Object.freeze({ WALLET: '/wallet' }); +const EXTERNAL_URL_PARAMETERS = Object.freeze({ + SUBSCAN: { + BLOCK: { + HASH: 'hash' + }, + ACCOUNT: { + ADDRESS: 'address' + } + } +}); + const EXTERNAL_PAGES = Object.freeze({ - BANXA: `${BANXA_LINK}` + BANXA: `${BANXA_LINK}`, + SUBSCAN: { + BLOCKS: `${SUBSCAN_LINK}/block`, + BLOCK: `${SUBSCAN_LINK}/block/:${EXTERNAL_URL_PARAMETERS.SUBSCAN.BLOCK.HASH}`, + ACCOUNT: `${SUBSCAN_LINK}/account/:${EXTERNAL_URL_PARAMETERS.SUBSCAN.ACCOUNT.ADDRESS}` + } }); const QUERY_PARAMETERS = Object.freeze({ @@ -60,4 +77,4 @@ const EXTERNAL_QUERY_PARAMETERS = Object.freeze({ } }); -export { EXTERNAL_PAGES, EXTERNAL_QUERY_PARAMETERS, PAGES, QUERY_PARAMETERS, URL_PARAMETERS }; +export { EXTERNAL_PAGES, EXTERNAL_QUERY_PARAMETERS, EXTERNAL_URL_PARAMETERS, PAGES, QUERY_PARAMETERS, URL_PARAMETERS }; diff --git a/src/utils/context/Notifications.tsx b/src/utils/context/Notifications.tsx new file mode 100644 index 0000000000..3dd7f48752 --- /dev/null +++ b/src/utils/context/Notifications.tsx @@ -0,0 +1,141 @@ +import { Overlay } from '@react-aria/overlays'; +import { mergeProps } from '@react-aria/utils'; +import React, { useEffect, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Id as NotificationId, toast, ToastOptions } from 'react-toastify'; + +import { addNotification } from '@/common/actions/general.actions'; +import { Notification, StoreType } from '@/common/types/util.types'; +import { ToastContainer, TransactionToast, TransactionToastProps } from '@/components'; + +import { useWallet } from '../hooks/use-wallet'; + +// Allows the introduction of diferent +// notifications toast beyond transactions +// i.e. claiming faucet funds or sign T&Cs +enum NotificationToast { + TRANSACTION +} + +type NotificationToastAction = { type: NotificationToast.TRANSACTION; props: TransactionToastProps }; + +const toastComponentMap = { [NotificationToast.TRANSACTION]: TransactionToast }; + +type ToastMap = Record; + +type NotifcationInfo = { + // NotificationId - toast is on the screen + // null - toast has been dismissed + // undefined - toast never existed + id: NotificationId | null | undefined; + hasRendered: boolean; + isOnScreen: boolean; +}; + +type NotificationOptions = ToastOptions; + +const toastConfig: NotificationOptions = { + closeButton: false, + autoClose: false, + closeOnClick: false, + draggable: false, + icon: false +}; + +type NotificationsConfig = { + list: Notification[]; + // gets notification meta data + get: (id: number | string) => NotifcationInfo; + // adds to the redux notifications list + add: (notification: Omit) => void; + // renders toast + show: (id: number | string, action: NotificationToastAction) => void; + // removes toast from the screen + dismiss: (id: number | string) => void; +}; + +const defaultContext: NotificationsConfig = {} as NotificationsConfig; + +const NotificationsContext = React.createContext(defaultContext); + +const useNotifications = (): NotificationsConfig => React.useContext(NotificationsContext); + +const NotificationsProvider: React.FC = ({ children }) => { + const toastContainerRef = useRef(null); + + const dispatch = useDispatch(); + + const { account } = useWallet(); + const { notifications } = useSelector((state: StoreType) => state.general); + + const idsMap = useRef({}); + + const get = (id: number | string) => { + const toastId = idsMap.current[id]; + + return { + id: toastId, + hasRendered: toastId === null, + isOnScreen: !!toastId + }; + }; + + const add = (notification: Omit) => + dispatch(addNotification(account?.toString() as string, { ...notification, date: new Date() })); + + const show = (id: number | string, action: NotificationToastAction) => { + const toastInfo = get(id); + + const ToastComponent = toastComponentMap[action.type]; + + const onDismiss = () => dismiss(id); + + const render = ; + + if (toastInfo.id) { + return toast.update(toastInfo.id, { render, ...toastConfig }); + } + + const newToastId = toast(render, toastConfig); + idsMap.current[id] = newToastId; + }; + + const dismiss = (id: number | string) => { + const toasInfo = get(id); + + if (!toasInfo.id) return; + + toast.dismiss(toasInfo.id); + // Set to null, meaning that this toast should never appear again, even if updated + idsMap.current[id] = null; + }; + + // Applying data-react-aria-top-layer="true" makes react-aria overlay consider the element as a visible element. + // Non-visible elements get forced with aria-hidden=true. + // Check: https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/overlays/src/ariaHideOutside.ts#L32 + useEffect(() => { + if (!toastContainerRef.current) return; + + toastContainerRef.current.setAttribute('data-react-aria-top-layer', 'true'); + }, [toastContainerRef]); + + return ( + + {children} + + + + + ); +}; + +export { NotificationsContext, NotificationsProvider, NotificationToast, useNotifications }; +export type { NotificationToastAction }; diff --git a/src/utils/hooks/transaction/extrinsics/extrinsics.ts b/src/utils/hooks/transaction/extrinsics/extrinsics.ts new file mode 100644 index 0000000000..cf63d868c9 --- /dev/null +++ b/src/utils/hooks/transaction/extrinsics/extrinsics.ts @@ -0,0 +1,46 @@ +import { ExtrinsicData } from '@interlay/interbtc-api'; +import { ExtrinsicStatus } from '@polkadot/types/interfaces'; + +import { Transaction, TransactionActions } from '../types'; +import { getLibExtrinsic } from './lib'; +import { getXCMExtrinsic } from './xcm'; + +/** + * SUMMARY: Maps each transaction to the correct lib call, + * while maintaining a safe-type check. + * HOW TO ADD NEW TRANSACTION: find the correct module to add the transaction + * in the types folder. In case you are adding a new type to the loans modules, go + * to types/loans and add your new transaction as an action. This actions needs to also be added to the + * types/index TransactionActions type. After that, you should be able to add it to the function. + * @param {TransactionActions} params contains the type of transaction and + * the related args to call the mapped lib call + * @return {Promise} every transaction return an extrinsic + */ +const getExtrinsic = async (params: TransactionActions): Promise => { + switch (params.type) { + case Transaction.XCM_TRANSFER: + return getXCMExtrinsic(params); + default: + return getLibExtrinsic(params); + } +}; + +/** + * The status where we want to be notified on the transaction completion + * @param {Transaction} type type of transaction + * @return {ExtrinsicStatus.type} transaction status + */ +const getStatus = (type: Transaction): ExtrinsicStatus['type'] => { + switch (type) { + // When requesting a replace, wait for the finalized event because we cannot revert BTC transactions. + // For more details see: https://github.com/interlay/interbtc-api/pull/373#issuecomment-1058949000 + case Transaction.ISSUE_REQUEST: + case Transaction.REDEEM_REQUEST: + case Transaction.REPLACE_REQUEST: + return 'Finalized'; + default: + return 'InBlock'; + } +}; + +export { getExtrinsic, getStatus }; diff --git a/src/utils/hooks/transaction/extrinsics/index.ts b/src/utils/hooks/transaction/extrinsics/index.ts new file mode 100644 index 0000000000..ff986fb28c --- /dev/null +++ b/src/utils/hooks/transaction/extrinsics/index.ts @@ -0,0 +1 @@ +export { getExtrinsic, getStatus } from './extrinsics'; diff --git a/src/utils/hooks/transaction/utils/extrinsic.ts b/src/utils/hooks/transaction/extrinsics/lib.ts similarity index 70% rename from src/utils/hooks/transaction/utils/extrinsic.ts rename to src/utils/hooks/transaction/extrinsics/lib.ts index 23346819db..0d2b90a727 100644 --- a/src/utils/hooks/transaction/utils/extrinsic.ts +++ b/src/utils/hooks/transaction/extrinsics/lib.ts @@ -1,20 +1,8 @@ import { ExtrinsicData } from '@interlay/interbtc-api'; -import { ExtrinsicStatus } from '@polkadot/types/interfaces'; -import { Transaction, TransactionActions } from '../types'; +import { LibActions, Transaction } from '../types'; -/** - * SUMMARY: Maps each transaction to the correct lib call, - * while maintaining a safe-type check. - * HOW TO ADD NEW TRANSACTION: find the correct module to add the transaction - * in the types folder. In case you are adding a new type to the loans modules, go - * to types/loans and add your new transaction as an action. This actions needs to also be added to the - * types/index TransactionActions type. After that, you should be able to add it to the function. - * @param {TransactionActions} params contains the type of transaction and - * the related args to call the mapped lib call - * @return {Promise} every transaction return an extrinsic - */ -const getExtrinsic = async (params: TransactionActions): Promise => { +const getLibExtrinsic = async (params: LibActions): Promise => { switch (params.type) { /* START - AMM */ case Transaction.AMM_SWAP: @@ -74,18 +62,19 @@ const getExtrinsic = async (params: TransactionActions): Promise return window.bridge.loans.enableAsCollateral(...params.args); /* END - LOANS */ - /* START - LOANS */ + /* START - VAULTS */ case Transaction.VAULTS_DEPOSIT_COLLATERAL: return window.bridge.vaults.depositCollateral(...params.args); case Transaction.VAULTS_WITHDRAW_COLLATERAL: return window.bridge.vaults.withdrawCollateral(...params.args); case Transaction.VAULTS_REGISTER_NEW_COLLATERAL: return window.bridge.vaults.registerNewCollateralVault(...params.args); + /* END - VAULTS */ + /* START - REWARDS */ case Transaction.REWARDS_WITHDRAW: return window.bridge.rewards.withdrawRewards(...params.args); /* START - REWARDS */ - /* END - LOANS */ /* START - ESCROW */ case Transaction.ESCROW_CREATE_LOCK: @@ -109,25 +98,12 @@ const getExtrinsic = async (params: TransactionActions): Promise return { extrinsic: batch }; } /* END - ESCROW */ - } -}; -/** - * The status where we want to be notified on the transaction completion - * @param {Transaction} type type of transaction - * @return {ExtrinsicStatus.type} transaction status - */ -const getStatus = (type: Transaction): ExtrinsicStatus['type'] => { - switch (type) { - // When requesting a replace, wait for the finalized event because we cannot revert BTC transactions. - // For more details see: https://github.com/interlay/interbtc-api/pull/373#issuecomment-1058949000 - case Transaction.ISSUE_REQUEST: - case Transaction.REDEEM_REQUEST: - case Transaction.REPLACE_REQUEST: - return 'Finalized'; - default: - return 'InBlock'; + /* START - VESTING */ + case Transaction.VESTING_CLAIM: + return { extrinsic: window.bridge.api.tx.vesting.claim() }; + /* END - VESTING */ } }; -export { getExtrinsic, getStatus }; +export { getLibExtrinsic }; diff --git a/src/utils/hooks/transaction/extrinsics/xcm.ts b/src/utils/hooks/transaction/extrinsics/xcm.ts new file mode 100644 index 0000000000..785369df31 --- /dev/null +++ b/src/utils/hooks/transaction/extrinsics/xcm.ts @@ -0,0 +1,27 @@ +import { FixedPointNumber } from '@acala-network/sdk-core'; +import { CrossChainTransferParams } from '@interlay/bridge'; +import { ExtrinsicData } from '@interlay/interbtc-api'; + +import { Transaction } from '../types'; +import { XCMActions } from '../types/xcm'; + +const getXCMExtrinsic = async (params: XCMActions): Promise => { + switch (params.type) { + case Transaction.XCM_TRANSFER: { + const [adapter, , toChain, address, transferAmount] = params.args; + + const transferAmountString = transferAmount.toString(true); + const transferAmountDecimals = transferAmount.currency.decimals; + const tx = adapter.createTx({ + amount: FixedPointNumber.fromInner(transferAmountString, transferAmountDecimals), + to: toChain, + token: transferAmount.currency.ticker, + address + } as CrossChainTransferParams); + + return { extrinsic: tx }; + } + } +}; + +export { getXCMExtrinsic }; diff --git a/src/utils/hooks/transaction/types/index.ts b/src/utils/hooks/transaction/types/index.ts index 538f820678..81d43097a0 100644 --- a/src/utils/hooks/transaction/types/index.ts +++ b/src/utils/hooks/transaction/types/index.ts @@ -9,6 +9,8 @@ import { ReplaceActions } from './replace'; import { RewardsActions } from './rewards'; import { TokensActions } from './tokens'; import { VaultsActions } from './vaults'; +import { VestingActions } from './vesting'; +import { XCMActions } from './xcm'; enum Transaction { // Issue @@ -29,6 +31,8 @@ enum Transaction { ESCROW_WITHDRAW = 'ESCROW_WITHDRAW', // Tokens TOKENS_TRANSFER = 'TOKENS_TRANSFER', + // XCM + XCM_TRANSFER = 'XCM_TRANSFER', // Vaults VAULTS_DEPOSIT_COLLATERAL = 'VAULTS_DEPOSIT_COLLATERAL', VAULTS_WITHDRAW_COLLATERAL = 'VAULTS_WITHDRAW_COLLATERAL', @@ -49,7 +53,11 @@ enum Transaction { AMM_SWAP = 'AMM_SWAP', AMM_ADD_LIQUIDITY = 'AMM_ADD_LIQUIDITY', AMM_REMOVE_LIQUIDITY = 'AMM_REMOVE_LIQUIDITY', - AMM_CLAIM_REWARDS = 'AMM_CLAIM_REWARDS' + AMM_CLAIM_REWARDS = 'AMM_CLAIM_REWARDS', + // Vesting + VESTING_CLAIM = 'VESTING_CLAIM', + // Faucet + FAUCET_FUND_WALLET = 'FAUCET_FUND_WALLET' } type TransactionEvents = { @@ -59,10 +67,11 @@ type TransactionEvents = { interface TransactionAction { accountAddress: string; events: TransactionEvents; + timestamp: number; customStatus?: ExtrinsicStatus['type']; } -type TransactionActions = +type LibActions = | EscrowActions | IssueActions | RedeemActions @@ -71,9 +80,19 @@ type TransactionActions = | LoansActions | AMMActions | VaultsActions - | RewardsActions; + | RewardsActions + | VestingActions; + +type TransactionActions = XCMActions | LibActions; type TransactionArgs = Extract['args']; -export { Transaction }; -export type { TransactionAction, TransactionActions, TransactionArgs, TransactionEvents }; +enum TransactionStatus { + CONFIRM, + SUBMITTING, + SUCCESS, + ERROR +} + +export { Transaction, TransactionStatus }; +export type { LibActions, TransactionAction, TransactionActions, TransactionArgs, TransactionEvents, XCMActions }; diff --git a/src/utils/hooks/transaction/types/vesting.ts b/src/utils/hooks/transaction/types/vesting.ts new file mode 100644 index 0000000000..ab4ce9a00e --- /dev/null +++ b/src/utils/hooks/transaction/types/vesting.ts @@ -0,0 +1,13 @@ +import { InterBtcApi } from '@interlay/interbtc-api'; + +import { Transaction } from '.'; +import { TransactionAction } from '.'; + +interface VestingClaimAction extends TransactionAction { + type: Transaction.VESTING_CLAIM; + args: Parameters; +} + +type VestingActions = VestingClaimAction; + +export type { VestingActions }; diff --git a/src/utils/hooks/transaction/types/xcm.ts b/src/utils/hooks/transaction/types/xcm.ts new file mode 100644 index 0000000000..71b0276c11 --- /dev/null +++ b/src/utils/hooks/transaction/types/xcm.ts @@ -0,0 +1,21 @@ +import { ChainName } from '@interlay/bridge'; +import { BaseCrossChainAdapter } from '@interlay/bridge/build/base-chain-adapter'; +import { CurrencyExt } from '@interlay/interbtc-api'; +import { MonetaryAmount } from '@interlay/monetary-js'; + +import { Transaction, TransactionAction } from '.'; + +interface XCMTransferAction extends TransactionAction { + type: Transaction.XCM_TRANSFER; + args: [ + adapter: BaseCrossChainAdapter, + fromChain: ChainName, + toChain: ChainName, + destinatary: string, + transferAmount: MonetaryAmount + ]; +} + +type XCMActions = XCMTransferAction; + +export type { XCMActions }; diff --git a/src/utils/hooks/transaction/use-transaction-notifications.tsx b/src/utils/hooks/transaction/use-transaction-notifications.tsx new file mode 100644 index 0000000000..abcb7fda2e --- /dev/null +++ b/src/utils/hooks/transaction/use-transaction-notifications.tsx @@ -0,0 +1,107 @@ +import { ISubmittableResult } from '@polkadot/types/types'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; + +import { updateTransactionModal } from '@/common/actions/general.actions'; +import { TransactionModalData } from '@/common/types/util.types'; +import { EXTERNAL_PAGES, EXTERNAL_URL_PARAMETERS } from '@/utils/constants/links'; +import { NotificationToast, NotificationToastAction, useNotifications } from '@/utils/context/Notifications'; + +import { TransactionActions, TransactionStatus } from './types'; +import { TransactionResult } from './use-transaction'; +import { getTransactionDescription } from './utils/description'; + +type TransactionNotificationsOptions = { + showSuccessModal?: boolean; +}; + +type UseTransactionNotificationsResult = { + onReject: (error?: Error) => void; + mutationProps: { + onMutate: (variables: TransactionActions) => void; + onSigning: (variables: TransactionActions) => void; + onSuccess: (data: TransactionResult, variables: TransactionActions) => void; + onError: (error: Error, variables: TransactionActions, context: unknown) => void; + }; +}; + +// Handles both transactions notifications and modal +const useTransactionNotifications = ({ + showSuccessModal = true +}: TransactionNotificationsOptions): UseTransactionNotificationsResult => { + const { t } = useTranslation(); + + const notifications = useNotifications(); + + const dispatch = useDispatch(); + + const handleModalOrToast = ( + status: TransactionStatus, + variables: TransactionActions, + data?: ISubmittableResult, + error?: Error + ) => { + const toastInfo = notifications.get(variables.timestamp); + + const url = + data?.txHash && + EXTERNAL_PAGES.SUBSCAN.BLOCK.replace(`:${EXTERNAL_URL_PARAMETERS.SUBSCAN.BLOCK.HASH}`, data.txHash.toString()); + + const description = getTransactionDescription(variables, status, t); + + // Add notification to history if status is SUCCESS or ERROR + if (description && (status === TransactionStatus.SUCCESS || status === TransactionStatus.ERROR)) { + notifications.add({ description, status, url }); + } + + // If toast already rendered, it means that the user did already dismiss the transaction modal and the toast + if (toastInfo.hasRendered) return; + + // creating or updating notification + if (toastInfo.isOnScreen) { + const toastAction: NotificationToastAction = { + type: NotificationToast.TRANSACTION, + props: { + variant: status, + url, + errorMessage: error?.message, + description + } + }; + + return notifications.show(variables.timestamp, toastAction); + } + + // only reach here if the modal has not been dismissed + const modalData: TransactionModalData = { + url, + description, + variant: status, + errorMessage: error?.message, + timestamp: variables?.timestamp + }; + + const isModalOpen = status === TransactionStatus.SUCCESS ? showSuccessModal : true; + + return dispatch(updateTransactionModal(isModalOpen, modalData)); + }; + + const handleSuccess = (result: TransactionResult, variables: TransactionActions) => { + const status = result.status === 'error' ? TransactionStatus.ERROR : TransactionStatus.SUCCESS; + + handleModalOrToast(status, variables, result.data, result.error); + }; + + return { + onReject: (error) => + dispatch(updateTransactionModal(true, { variant: TransactionStatus.ERROR, errorMessage: error?.message })), + mutationProps: { + onMutate: (variables) => handleModalOrToast(TransactionStatus.CONFIRM, variables), + onSigning: (variables) => handleModalOrToast(TransactionStatus.SUBMITTING, variables), + onSuccess: (result, variables) => handleSuccess(result, variables), + onError: (error, variables) => handleModalOrToast(TransactionStatus.ERROR, variables, undefined, error) + } + }; +}; + +export { useTransactionNotifications }; diff --git a/src/utils/hooks/transaction/use-transaction.ts b/src/utils/hooks/transaction/use-transaction.ts index d18291f94c..3fa2cda32e 100644 --- a/src/utils/hooks/transaction/use-transaction.ts +++ b/src/utils/hooks/transaction/use-transaction.ts @@ -1,51 +1,62 @@ import { ExtrinsicStatus } from '@polkadot/types/interfaces'; import { ISubmittableResult } from '@polkadot/types/types'; -import { useCallback } from 'react'; +import { mergeProps } from '@react-aria/utils'; +import { useCallback, useState } from 'react'; import { MutationFunction, useMutation, UseMutationOptions, UseMutationResult } from 'react-query'; import { useSubstrate } from '@/lib/substrate'; +import { getExtrinsic, getStatus } from './extrinsics'; import { Transaction, TransactionActions, TransactionArgs } from './types'; -import { getExtrinsic, getStatus } from './utils/extrinsic'; +import { useTransactionNotifications } from './use-transaction-notifications'; import { submitTransaction } from './utils/submit'; -type UseTransactionOptions = Omit< - UseMutationOptions, - 'mutationFn' -> & { - customStatus?: ExtrinsicStatus['type']; -}; +type TransactionResult = { status: 'success' | 'error'; data: ISubmittableResult; error?: Error }; // TODO: add feeEstimate and feeEstimateAsync type ExecuteArgs = { // Executes the transaction execute(...args: TransactionArgs): void; // Similar to execute but returns a promise which can be awaited. - executeAsync(...args: TransactionArgs): Promise; + executeAsync(...args: TransactionArgs): Promise; }; // TODO: add feeEstimate and feeEstimateAsync type ExecuteTypeArgs = { execute(type: D, ...args: TransactionArgs): void; - executeAsync(type: D, ...args: TransactionArgs): Promise; + executeAsync(type: D, ...args: TransactionArgs): Promise; }; -type InheritAttrs = Omit< - UseMutationResult, +type ExecuteFunctions = ExecuteArgs | ExecuteTypeArgs; + +type ReactQueryUseMutationResult = Omit< + UseMutationResult, 'mutate' | 'mutateAsync' >; -type UseTransactionResult = InheritAttrs & (ExecuteArgs | ExecuteTypeArgs); +type UseTransactionResult = { + reject: (error?: Error) => void; + isSigned: boolean; +} & ReactQueryUseMutationResult & + ExecuteFunctions; -const mutateTransaction: MutationFunction = async (params) => { +const mutateTransaction: MutationFunction = async (params) => { const extrinsics = await getExtrinsic(params); const expectedStatus = params.customStatus || getStatus(params.type); return submitTransaction(window.bridge.api, params.accountAddress, extrinsics, expectedStatus, params.events); }; +type UseTransactionOptions = Omit< + UseMutationOptions, + 'mutationFn' +> & { + customStatus?: ExtrinsicStatus['type']; + onSigning?: (variables: TransactionActions) => void; + showSuccessModal?: boolean; +}; + // The three declared functions are use to infer types on diferent implementations -// TODO: missing xcm transaction function useTransaction( type: T, options?: UseTransactionOptions @@ -59,13 +70,31 @@ function useTransaction( ): UseTransactionResult { const { state } = useSubstrate(); - const hasOnlyOptions = typeof typeOrOptions !== 'string'; + const [isSigned, setSigned] = useState(false); + + const { showSuccessModal, customStatus, ...mutateOptions } = + (typeof typeOrOptions === 'string' ? options : typeOrOptions) || {}; - const { mutate, mutateAsync, ...transactionMutation } = useMutation( - mutateTransaction, - (hasOnlyOptions ? typeOrOptions : options) as UseTransactionOptions + const notifications = useTransactionNotifications({ showSuccessModal }); + + const handleMutate = () => setSigned(false); + + const handleSigning = () => setSigned(true); + + const handleError = (error: Error) => console.error(error.message); + + const { onSigning, ...optionsProp } = mergeProps( + mutateOptions, + { + onMutate: handleMutate, + onSigning: handleSigning, + onError: handleError + }, + notifications.mutationProps ); + const { mutate, mutateAsync, ...transactionMutation } = useMutation(mutateTransaction, optionsProp); + // Handles params for both type of implementations const getParams = useCallback( (args: Parameters['execute']>) => { @@ -83,14 +112,21 @@ function useTransaction( // Execution should only ran when authenticated const accountAddress = state.selectedAccount?.address; - // TODO: add event `onReady` - return { + const variables = { ...params, accountAddress, - customStatus: options?.customStatus + timestamp: new Date().getTime(), + customStatus } as TransactionActions; + + return { + ...variables, + events: { + onReady: () => onSigning(variables) + } + }; }, - [options?.customStatus, state.selectedAccount?.address, typeOrOptions] + [onSigning, customStatus, state.selectedAccount?.address, typeOrOptions] ); const handleExecute = useCallback( @@ -111,12 +147,23 @@ function useTransaction( [getParams, mutateAsync] ); + const handleReject = (error?: Error) => { + notifications.onReject(error); + setSigned(false); + + if (error) { + console.error(error.message); + } + }; + return { ...transactionMutation, + isSigned, + reject: handleReject, execute: handleExecute, executeAsync: handleExecuteAsync }; } export { useTransaction }; -export type { UseTransactionResult }; +export type { TransactionResult, UseTransactionResult }; diff --git a/src/utils/hooks/transaction/utils/description.ts b/src/utils/hooks/transaction/utils/description.ts new file mode 100644 index 0000000000..f79c121332 --- /dev/null +++ b/src/utils/hooks/transaction/utils/description.ts @@ -0,0 +1,363 @@ +import { StringMap, TOptions } from 'i18next'; +import { TFunction } from 'react-i18next'; + +import { shortAddress } from '@/common/utils/utils'; + +import { Transaction, TransactionActions, TransactionStatus } from '../types'; + +const getTranslationArgs = ( + params: TransactionActions, + status: TransactionStatus +): { key: string; args?: TOptions } | undefined => { + const isPast = status === TransactionStatus.SUCCESS; + + switch (params.type) { + /* START - AMM */ + case Transaction.AMM_SWAP: { + const [trade] = params.args; + + return { + key: isPast ? 'transaction.swapped_to' : 'transaction.swapping_to', + args: { + fromAmount: trade.inputAmount.toHuman(), + fromCurrency: trade.inputAmount.currency.ticker, + toAmount: trade.outputAmount.toHuman(), + toCurrency: trade.outputAmount.currency.ticker + } + }; + } + case Transaction.AMM_ADD_LIQUIDITY: { + const [, pool] = params.args; + + return { + key: isPast ? 'transaction.added_liquidity_to_pool' : 'transaction.adding_liquidity_to_pool', + args: { + poolName: pool.lpToken.ticker + } + }; + } + case Transaction.AMM_REMOVE_LIQUIDITY: { + const [, pool] = params.args; + + return { + key: isPast ? 'transaction.removed_liquidity_from_pool' : 'transaction.removing_liquidity_from_pool', + args: { + poolName: pool.lpToken.ticker + } + }; + } + case Transaction.AMM_CLAIM_REWARDS: { + return { + key: isPast ? 'transaction.claimed_pool_rewards' : 'transaction.claiming_pool_rewards' + }; + } + /* END - AMM */ + + /* START - ISSUE */ + case Transaction.ISSUE_REQUEST: { + const [amount] = params.args; + + return { + key: isPast ? 'transaction.issued_amount' : 'transaction.issuing_amount', + args: { + amount: amount.toHuman(), + currency: amount.currency.ticker + } + }; + } + case Transaction.ISSUE_EXECUTE: { + return { + key: isPast ? 'transaction.executed_issue' : 'transaction.executing_issue' + }; + } + /* END - ISSUE */ + + /* START - REDEEM */ + case Transaction.REDEEM_CANCEL: { + const [redeemId, isReimburse] = params.args; + + const args = { + requestId: shortAddress(redeemId) + }; + + if (isReimburse) { + return { + key: isPast ? 'transaction.reimbersed_redeem_id' : 'transaction.reimbursing_redeem_id', + args + }; + } + + return { + key: isPast ? 'transaction.retried_redeem_id' : 'transaction.retrying_redeem_id', + args + }; + } + case Transaction.REDEEM_BURN: { + const [amount] = params.args; + + return { + key: isPast ? 'transaction.burned_amount' : 'transaction.burning_amount', + args: { + amount: amount.toHuman(), + currency: amount.currency.ticker + } + }; + } + case Transaction.REDEEM_REQUEST: { + const [amount] = params.args; + + return { + key: isPast ? 'transaction.redeemed_amount' : 'transaction.redeeming_amount', + args: { + amount: amount.toHuman(), + currency: amount.currency.ticker + } + }; + } + /* END - REDEEM */ + + /* START - REPLACE */ + case Transaction.REPLACE_REQUEST: { + return { + key: isPast ? 'transaction.requested_vault_replacement' : 'transaction.requesting_vault_replacement' + }; + } + /* END - REPLACE */ + + /* START - TOKENS */ + case Transaction.TOKENS_TRANSFER: { + const [destination, amount] = params.args; + + return { + key: isPast ? 'transaction.transfered_amount_to_address' : 'transaction.transfering_amount_to_address', + args: { + amount: amount.toHuman(), + currency: amount.currency.ticker, + address: shortAddress(destination) + } + }; + } + /* END - TOKENS */ + + /* START - XCM */ + case Transaction.XCM_TRANSFER: { + const [, fromChain, toChain, , transferAmount] = params.args; + + return { + key: isPast + ? 'transaction.transfered_amount_from_chain_to_chain' + : 'transaction.transfering_amount_from_chain_to_chain', + args: { + amount: transferAmount.toHuman(), + currency: transferAmount.currency.ticker, + fromChain: fromChain.toUpperCase(), + toChain: toChain.toUpperCase() + } + }; + } + /* END - XCM */ + + /* START - LOANS */ + case Transaction.LOANS_CLAIM_REWARDS: { + return { + key: isPast ? 'transaction.claimed_lending_rewards' : 'transaction.claiming_lending_rewards' + }; + } + case Transaction.LOANS_BORROW: { + const [currency, amount] = params.args; + + return { + key: isPast ? 'transaction.borrowed_amount' : 'transaction.borrowing_amount', + args: { + amount: amount.toHuman(), + currency: currency.ticker + } + }; + } + case Transaction.LOANS_LEND: { + const [currency, amount] = params.args; + + return { + key: isPast ? 'transaction.lent_amount' : 'transaction.lending_amount', + args: { + amount: amount.toHuman(), + currency: currency.ticker + } + }; + } + case Transaction.LOANS_REPAY: { + const [currency, amount] = params.args; + + return { + key: isPast ? 'transaction.repaid_amount' : 'transaction.repaying_amount', + args: { + amount: amount.toHuman(), + currency: currency.ticker + } + }; + } + case Transaction.LOANS_REPAY_ALL: { + const [currency] = params.args; + + return { + key: isPast ? 'transaction.repaid' : 'transaction.repaying', + args: { + currency: currency.ticker + } + }; + } + case Transaction.LOANS_WITHDRAW: { + const [currency, amount] = params.args; + + return { + key: isPast ? 'transaction.withdrew_amount' : 'transaction.withdrawing_amount', + args: { + amount: amount.toHuman(), + currency: currency.ticker + } + }; + } + case Transaction.LOANS_WITHDRAW_ALL: { + const [currency] = params.args; + + return { + key: isPast ? 'transaction.withdrew' : 'transaction.withdrawing', + args: { + currency: currency.ticker + } + }; + } + case Transaction.LOANS_DISABLE_COLLATERAL: { + const [currency] = params.args; + + return { + key: isPast ? 'transaction.disabled_loan_as_collateral' : 'transaction.disabling_loan_as_collateral', + args: { + currency: currency.ticker + } + }; + } + case Transaction.LOANS_ENABLE_COLLATERAL: { + const [currency] = params.args; + + return { + key: isPast ? 'transaction.enabled_loan_as_collateral' : 'transaction.enabling_loan_as_collateral', + args: { + currency: currency.ticker + } + }; + } + /* END - LOANS */ + + /* START - VAULTS */ + case Transaction.VAULTS_DEPOSIT_COLLATERAL: { + const [amount] = params.args; + + return { + key: isPast ? 'transaction.deposited_amount_to_vault' : 'transaction.depositing_amount_to_vault', + args: { + amount: amount.toHuman(), + currency: amount.currency.ticker + } + }; + } + case Transaction.VAULTS_WITHDRAW_COLLATERAL: { + const [amount] = params.args; + + return { + key: isPast ? 'transaction.withdrew_amount_from_vault' : 'transaction.withdrawing_amount_from_vault', + args: { + amount: amount.toHuman(), + currency: amount.currency.ticker + } + }; + } + case Transaction.VAULTS_REGISTER_NEW_COLLATERAL: { + const [collateralAmount] = params.args; + + return { + key: isPast ? 'transaction.created_currency_vault' : 'transaction.creating_currency_vault', + args: { + currency: collateralAmount.currency.ticker + } + }; + } + /* END - VAULTS */ + + /* START - REWARDS */ + case Transaction.REWARDS_WITHDRAW: { + return { + key: isPast ? 'transaction.claimed_vault_rewards' : 'transaction.claiming_vault_rewards' + }; + } + /* START - REWARDS */ + + /* START - ESCROW */ + case Transaction.ESCROW_CREATE_LOCK: { + const [amount] = params.args; + + return { + key: isPast ? 'transaction.staked_amount' : 'transaction.staking_amount', + args: { + amount: amount.toHuman(), + currency: amount.currency.ticker + } + }; + } + case Transaction.ESCROW_INCREASE_LOCKED_AMOUNT: { + const [amount] = params.args; + + return { + key: isPast ? 'transaction.added_amount_to_staked_amount' : 'transaction.adding_amount_to_staked_amount', + args: { + amount: amount.toHuman(), + currency: amount.currency.ticker + } + }; + } + case Transaction.ESCROW_INCREASE_LOCKED_TIME: { + return { + key: isPast ? 'transaction.increased_stake_lock_time' : 'transaction.increasing_stake_lock_time' + }; + } + case Transaction.ESCROW_WITHDRAW: { + return { + key: isPast ? 'transaction.withdrew_stake' : 'transaction.withdrawing_stake' + }; + } + case Transaction.ESCROW_WITHDRAW_REWARDS: { + return { + key: isPast ? 'transaction.claimed_staking_rewards' : 'transaction.claiming_staking_rewards' + }; + } + case Transaction.ESCROW_INCREASE_LOOKED_TIME_AND_AMOUNT: { + return { + key: isPast + ? 'transaction.increased_stake_locked_time_amount' + : 'transaction.increasing_stake_locked_time_amount' + }; + } + /* END - ESCROW */ + /* START - VESTING */ + case Transaction.VESTING_CLAIM: { + return { + key: isPast ? 'transaction.claimed_vesting' : 'transaction.claiming_vesting' + }; + } + /* END - VESTING */ + } +}; + +const getTransactionDescription = ( + params: TransactionActions, + status: TransactionStatus, + t: TFunction +): string | undefined => { + const translation = getTranslationArgs(params, status); + + if (!translation) return; + + return t(translation.key, translation.args); +}; + +export { getTransactionDescription }; diff --git a/src/utils/hooks/transaction/utils/submit.ts b/src/utils/hooks/transaction/utils/submit.ts index d1c832b023..21c88b0cd5 100644 --- a/src/utils/hooks/transaction/utils/submit.ts +++ b/src/utils/hooks/transaction/utils/submit.ts @@ -6,11 +6,11 @@ import { ExtrinsicStatus } from '@polkadot/types/interfaces/author'; import { ISubmittableResult } from '@polkadot/types/types'; import { TransactionEvents } from '../types'; +import { TransactionResult } from '../use-transaction'; type HandleTransactionResult = { result: ISubmittableResult; unsubscribe: () => void }; -// When passing { nonce: -1 } to signAndSend the API will use system.accountNextIndex to determine the nonce -const transactionOptions = { nonce: -1 }; +let nonce: number | undefined; const handleTransaction = async ( account: AddressOrPair, @@ -23,11 +23,16 @@ const handleTransaction = async ( // Extrinsic status let isReady = false; + if (!nonce) { + const lastestNonce = await window.bridge.api.rpc.system.accountNextIndex(account.toString()); + nonce = lastestNonce.toNumber(); + } + return new Promise((resolve, reject) => { let unsubscribe: () => void; (extrinsicData.extrinsic as SubmittableExtrinsic<'promise'>) - .signAndSend(account, transactionOptions, callback) + .signAndSend(account, { nonce }, callback) .then((unsub) => (unsubscribe = unsub)) .catch((error) => reject(error)); @@ -43,35 +48,34 @@ const handleTransaction = async ( isComplete = expectedStatus === result.status.type; } - if (isComplete) { + if (isComplete || result.status.isUsurped) { resolve({ unsubscribe, result }); } } + + if (nonce) { + nonce++; + } }); }; const getErrorMessage = (api: ApiPromise, dispatchError: DispatchError) => { const { isModule, asModule, isBadOrigin } = dispatchError; - // Construct error message - const message = 'The transaction failed.'; - // Runtime error in one of the parachain modules if (isModule) { // for module errors, we have the section indexed, lookup const decoded = api.registry.findMetaError(asModule); const { docs, name, section } = decoded; - return message.concat(` The error code is ${section}.${name}. ${docs.join(' ')}`); + return `The error code is ${section}.${name}. ${docs.join(' ')}.`; } // Bad origin if (isBadOrigin) { - return message.concat( - ` The error is caused by using an incorrect account. The error code is BadOrigin ${dispatchError}.` - ); + return `The error is caused by using an incorrect account. The error code is BadOrigin ${dispatchError}.`; } - return message.concat(` The error is ${dispatchError}.`); + return `The error is ${dispatchError}.`; }; /** @@ -89,19 +93,29 @@ const submitTransaction = async ( extrinsicData: ExtrinsicData, expectedStatus?: ExtrinsicStatus['type'], callbacks?: TransactionEvents -): Promise => { +): Promise => { const { result, unsubscribe } = await handleTransaction(account, extrinsicData, expectedStatus, callbacks); unsubscribe(); + let error: Error | undefined; + const { dispatchError } = result; if (dispatchError) { - const message = getErrorMessage(api, dispatchError); - throw new Error(message); + error = new Error(getErrorMessage(api, dispatchError)); + } + + // TODO: determine a description to when transaction ends up usurped + if (result.status.isUsurped) { + error = new Error(); } - return result; + return { + status: error ? 'error' : 'success', + data: result, + error + }; }; export { submitTransaction }; diff --git a/src/utils/hooks/use-copy-tooltip.tsx b/src/utils/hooks/use-copy-tooltip.tsx index 36ec13ccd4..42ce3c1fcc 100644 --- a/src/utils/hooks/use-copy-tooltip.tsx +++ b/src/utils/hooks/use-copy-tooltip.tsx @@ -15,6 +15,7 @@ type CopyTooltipResult = { }; }; +// FIX: is openning tooltip too fast const useCopyTooltip = (props?: CopyTooltipProp): CopyTooltipResult => { const { t } = useTranslation(); diff --git a/src/utils/hooks/use-countdown.ts b/src/utils/hooks/use-countdown.ts new file mode 100644 index 0000000000..5430a4cf99 --- /dev/null +++ b/src/utils/hooks/use-countdown.ts @@ -0,0 +1,69 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useInterval } from 'react-use'; + +import { theme } from '@/component-library'; +import { useWindowFocus } from '@/utils/hooks/use-window-focus'; + +type UseCountdownProps = { + value?: number; + timeout?: number; + disabled?: boolean; + onEndCountdown?: () => void; +}; + +type UseCountdownResult = { + value: number; + start: () => void; + stop: () => void; +}; + +const useCountdown = ({ + value = 100, + timeout = 8000, + disabled, + onEndCountdown +}: UseCountdownProps): UseCountdownResult => { + const windowFocused = useWindowFocus(); + + const [countdown, setProgress] = useState(value); + const [isRunning, setRunning] = useState(disabled); + + // handles the countdown + useInterval( + () => setProgress((prev) => prev - 1), + isRunning ? timeout / theme.transition.duration.duration100 : null + ); + + const handleStartCountdown = useCallback(() => { + const shouldRun = !disabled && countdown > 0; + setRunning(shouldRun); + }, [countdown, disabled]); + + const handleStopCountdown = () => setRunning(false); + + useEffect(() => { + if (isRunning && countdown === 0) { + onEndCountdown?.(); + handleStopCountdown(); + } + }, [isRunning, countdown, onEndCountdown]); + + useEffect(() => { + if (windowFocused && !disabled) { + handleStartCountdown(); + } else { + handleStopCountdown(); + } + }, [windowFocused, handleStartCountdown, disabled]); + + console.log(countdown, isRunning); + + return { + value: countdown, + start: handleStartCountdown, + stop: handleStopCountdown + }; +}; + +export { useCountdown }; +export type { UseCountdownProps, UseCountdownResult }; diff --git a/src/utils/hooks/use-sign-message.ts b/src/utils/hooks/use-sign-message.ts index 78ef745257..ef57dbdfd0 100644 --- a/src/utils/hooks/use-sign-message.ts +++ b/src/utils/hooks/use-sign-message.ts @@ -96,6 +96,7 @@ const useSignMessage = (): UseSignMessageResult => { queryFn: () => selectedAccount && getSignature(selectedAccount) }); + // TODO: add new notification const signMessageMutation = useMutation((account: KeyringPair) => postSignature(account), { onError: (_, variables) => { setSignature(variables.address, false); diff --git a/src/utils/hooks/use-window-focus.ts b/src/utils/hooks/use-window-focus.ts new file mode 100644 index 0000000000..27d63b7c54 --- /dev/null +++ b/src/utils/hooks/use-window-focus.ts @@ -0,0 +1,26 @@ +import { useEffect, useState } from 'react'; + +const hasFocus = () => typeof document !== 'undefined' && document.hasFocus(); + +const useWindowFocus = (): boolean => { + const [focused, setFocused] = useState(hasFocus); // Focus for first render + + useEffect(() => { + setFocused(hasFocus()); // Focus for additional renders + + const onFocus = () => setFocused(true); + const onBlur = () => setFocused(false); + + window.addEventListener('focus', onFocus); + window.addEventListener('blur', onBlur); + + return () => { + window.removeEventListener('focus', onFocus); + window.removeEventListener('blur', onBlur); + }; + }, []); + + return focused; +}; + +export { useWindowFocus }; diff --git a/yarn.lock b/yarn.lock index f662a8bcb9..bf4d250f24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17669,16 +17669,14 @@ react-table@^7.6.3: resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.7.0.tgz#e2ce14d7fe3a559f7444e9ecfe8231ea8373f912" integrity sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA== -react-toastify@^6.0.5: - version "6.2.0" - resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-6.2.0.tgz#f2d76747c70b9de91f71f253d9feae6b53dc836c" - integrity sha512-XpjFrcBhQ0/nBOL4syqgP/TywFnOyxmstYLWgSQWcj39qpp+WU4vPt3C/ayIDx7RFyxRWfzWTdR2qOcDGo7G0w== +react-toastify@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.1.2.tgz#293aa1f952240129fe485ae5cb2f8d09c652cf3f" + integrity sha512-PBfzXO5jMGEtdYR5jxrORlNZZe/EuOkwvwKijMatsZZm8IZwLj01YvobeJYNjFcA6uy6CVrx2fzL9GWbhWPTDA== dependencies: clsx "^1.1.1" - prop-types "^15.7.2" - react-transition-group "^4.4.1" -react-transition-group@^4.4.1, react-transition-group@^4.4.5: +react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== From 74eb7ee6996be74a37b340db13f0ad611fa590b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sim=C3=A3o?= Date: Thu, 1 Jun 2023 17:36:07 +0100 Subject: [PATCH 026/241] chore: remove console.log (#1262) --- src/utils/hooks/use-countdown.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/hooks/use-countdown.ts b/src/utils/hooks/use-countdown.ts index 5430a4cf99..49e74aa05d 100644 --- a/src/utils/hooks/use-countdown.ts +++ b/src/utils/hooks/use-countdown.ts @@ -56,8 +56,6 @@ const useCountdown = ({ } }, [windowFocused, handleStartCountdown, disabled]); - console.log(countdown, isRunning); - return { value: countdown, start: handleStartCountdown, From 869efa9ebe18ff755c67649e139defdc2d2949ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sim=C3=A3o?= Date: Fri, 2 Jun 2023 09:10:29 +0100 Subject: [PATCH 027/241] fix(TokenInput): adorment ticker (#1257) --- src/component-library/TokenInput/TokenInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From f178d582d3971670228fc38e589677c72d84fe69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sim=C3=A3o?= Date: Fri, 2 Jun 2023 10:58:00 +0100 Subject: [PATCH 028/241] fix: get vesting data (#1264) --- src/utils/hooks/api/use-get-vesting-data.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From 7283d1ec5e8b020160ecb9eb610908dd81fbbad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Slan=C3=BD?= <47864599+peterslany@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:15:37 +0200 Subject: [PATCH 029/241] Peter/chore update lib 2.3.0 (#1267) * chore: update monetary to latest 0.7.3 * chore: update lib version --- package.json | 2 +- yarn.lock | 74 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 5e804dcbdd..f767f3fd8f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@headlessui/react": "^1.1.1", "@heroicons/react": "^2.0.18", "@interlay/bridge": "^0.3.11", - "@interlay/interbtc-api": "2.2.4", + "@interlay/interbtc-api": "2.3.0", "@interlay/monetary-js": "0.7.3", "@polkadot/api": "9.14.2", "@polkadot/extension-dapp": "0.44.1", diff --git a/yarn.lock b/yarn.lock index bf4d250f24..644ab3521e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-2.3.0.tgz#54ca0a80550c1b8ec93224d5e29b960b0d607d74" + integrity sha512-r+GU1jfTpapjG5bgdJMXAVWok1msSqJPA/ldcPfN1d7v8BnY26F2LekeJjO77Birc5tDAfeGMqYvRkNcmcqkUA== 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" @@ -20551,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== From f9afbb724a5108d5006664a84800221e4ab15807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sim=C3=A3o?= Date: Tue, 6 Jun 2023 12:12:45 +0100 Subject: [PATCH 030/241] fix: sort notifications (#1270) --- src/components/NotificationsPopover/NotificationsList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/NotificationsPopover/NotificationsList.tsx b/src/components/NotificationsPopover/NotificationsList.tsx index 97d51a2c9a..a84dc4091e 100644 --- a/src/components/NotificationsPopover/NotificationsList.tsx +++ b/src/components/NotificationsPopover/NotificationsList.tsx @@ -20,7 +20,7 @@ const NotificationsList = ({ items }: NotificationsListProps): JSX.Element => { ); } - const latestTransactions = items.slice(-5); + const latestTransactions = items.slice(-5).sort((a, b) => b.date.getTime() - a.date.getTime()); return ( From f6be34f9813ccbae7df5adb4d1504d82747c410d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sim=C3=A3o?= Date: Wed, 7 Jun 2023 09:39:33 +0100 Subject: [PATCH 031/241] fix: transaction none (#1271) --- src/utils/hooks/transaction/utils/submit.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/utils/hooks/transaction/utils/submit.ts b/src/utils/hooks/transaction/utils/submit.ts index 21c88b0cd5..ceb930d7db 100644 --- a/src/utils/hooks/transaction/utils/submit.ts +++ b/src/utils/hooks/transaction/utils/submit.ts @@ -10,8 +10,6 @@ import { TransactionResult } from '../use-transaction'; type HandleTransactionResult = { result: ISubmittableResult; unsubscribe: () => void }; -let nonce: number | undefined; - const handleTransaction = async ( account: AddressOrPair, extrinsicData: ExtrinsicData, @@ -23,16 +21,11 @@ const handleTransaction = async ( // Extrinsic status let isReady = false; - if (!nonce) { - const lastestNonce = await window.bridge.api.rpc.system.accountNextIndex(account.toString()); - nonce = lastestNonce.toNumber(); - } - return new Promise((resolve, reject) => { let unsubscribe: () => void; (extrinsicData.extrinsic as SubmittableExtrinsic<'promise'>) - .signAndSend(account, { nonce }, callback) + .signAndSend(account, { nonce: -1 }, callback) .then((unsub) => (unsubscribe = unsub)) .catch((error) => reject(error)); @@ -52,10 +45,6 @@ const handleTransaction = async ( resolve({ unsubscribe, result }); } } - - if (nonce) { - nonce++; - } }); }; From 36c41c66586093b18b86f2c344caaa550a4cc045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sim=C3=A3o?= Date: Thu, 8 Jun 2023 13:37:48 +0100 Subject: [PATCH 032/241] fix(Loans): apy label (#1275) --- src/components/LoanPositionsTable/ApyCell.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/LoanPositionsTable/ApyCell.tsx b/src/components/LoanPositionsTable/ApyCell.tsx index 1860138278..4e1e2ffa19 100644 --- a/src/components/LoanPositionsTable/ApyCell.tsx +++ b/src/components/LoanPositionsTable/ApyCell.tsx @@ -35,7 +35,8 @@ const ApyCell = ({ 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 totalApyLabel = getApyLabel(totalApy); const earnedAsset = accumulatedDebt || earnedInterest; From 4bf3e13e600709a889ba7b6fce5bd68c9d93d9d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Slan=C3=BD?= <47864599+peterslany@users.noreply.github.com> Date: Fri, 9 Jun 2023 10:26:01 +0200 Subject: [PATCH 033/241] 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 --- package.json | 2 +- src/components/LoanApyTooltip/LoanApyTooltip.tsx | 9 +++------ src/components/LoanPositionsTable/ApyCell.tsx | 3 --- src/components/LoanPositionsTable/LoanPositionsTable.tsx | 8 ++------ .../components/BorrowAssetsTable/BorrowAssetsTable.tsx | 6 +----- .../components/LendAssetsTable/LendAssetsTable.tsx | 6 +----- .../api/loans/use-get-account-lending-statistics.tsx | 1 + yarn.lock | 8 ++++---- 8 files changed, 13 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index f767f3fd8f..1b61e88e75 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@headlessui/react": "^1.1.1", "@heroicons/react": "^2.0.18", "@interlay/bridge": "^0.3.11", - "@interlay/interbtc-api": "2.3.0", + "@interlay/interbtc-api": "2.3.1", "@interlay/monetary-js": "0.7.3", "@polkadot/api": "9.14.2", "@polkadot/extension-dapp": "0.44.1", 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 4e1e2ffa19..97cba39349 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, @@ -55,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/pages/Loans/LoansOverview/components/BorrowAssetsTable/BorrowAssetsTable.tsx b/src/pages/Loans/LoansOverview/components/BorrowAssetsTable/BorrowAssetsTable.tsx index 15ef0668d0..93e5de076e 100644 --- a/src/pages/Loans/LoansOverview/components/BorrowAssetsTable/BorrowAssetsTable.tsx +++ b/src/pages/Loans/LoansOverview/components/BorrowAssetsTable/BorrowAssetsTable.tsx @@ -8,7 +8,6 @@ import { 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 { useGetPrices } from '@/utils/hooks/api/use-get-prices'; import { StyledAssetCell } from './BorrowAssetsTable.style'; @@ -48,20 +47,17 @@ const BorrowAssetsTable = ({ assets, onRowAction, ...props }: BorrowAssetsTableP const titleId = useId(); const { t } = useTranslation(); const prices = useGetPrices(); - const { data: subsidyRewards } = useGetAccountSubsidyRewards(); const rows: BorrowAssetsTableRow[] = useMemo( () => Object.values(assets).map(({ borrowApy, currency, availableCapacity, totalBorrows, borrowReward }) => { const asset = ; - const accruedRewards = subsidyRewards ? subsidyRewards.perMarket[currency.ticker].borrow : null; const apy = ( 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/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/yarn.lock b/yarn.lock index 644ab3521e..bdfcd1b1a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2120,10 +2120,10 @@ dependencies: axios "^0.21.1" -"@interlay/interbtc-api@2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-2.3.0.tgz#54ca0a80550c1b8ec93224d5e29b960b0d607d74" - integrity sha512-r+GU1jfTpapjG5bgdJMXAVWok1msSqJPA/ldcPfN1d7v8BnY26F2LekeJjO77Birc5tDAfeGMqYvRkNcmcqkUA== +"@interlay/interbtc-api@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-2.3.1.tgz#99bd9058453f6125d0fe1aa7bae208acda3191ed" + integrity sha512-XTbFNz0W/ev9cLfO0hKOfoPa79ARUrhKItrNolA98n055DMWHS7Lu9P1HUBG9KfKvgoiB5hQBo1Gc4hG+oPKQg== dependencies: "@interlay/esplora-btc-api" "0.4.0" "@interlay/interbtc-types" "1.12.0" From 6f2972dc39e87c7387a13a337958c5a72be0a8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Slan=C3=BD?= <47864599+peterslany@users.noreply.github.com> Date: Fri, 9 Jun 2023 19:49:49 +0200 Subject: [PATCH 034/241] 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 --- src/components/LoanApyTooltip/BreakdownGroup.tsx | 2 +- .../LoansOverview/components/LoanActionInfo/RewardsGroup.tsx | 2 +- src/utils/helpers/loans.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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/utils/helpers/loans.ts b/src/utils/helpers/loans.ts index c7241abb5e..a731bfaed5 100644 --- a/src/utils/helpers/loans.ts +++ b/src/utils/helpers/loans.ts @@ -41,7 +41,7 @@ const getSubsidyRewardApy = ( } const exchangeRate = rewardCurrencyPriceUSD / positionCurrencyPriceUSD; - const apy = reward.toBig().mul(exchangeRate); + const apy = reward.toBig().mul(exchangeRate).mul(100); return apy; }; From 0fb9217a33f8977fb426b8dc8969a42df7fad9b8 Mon Sep 17 00:00:00 2001 From: Thomas Jeatt Date: Mon, 12 Jun 2023 14:15:43 +0100 Subject: [PATCH 035/241] chore: release v2.33.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b61e88e75..80b15375cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interbtc-ui", - "version": "2.32.6", + "version": "2.33.0", "private": true, "dependencies": { "@craco/craco": "^6.1.1", From d5818f34dbfd60297130ea4468c877cb690eb57f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Slan=C3=BD?= <47864599+peterslany@users.noreply.github.com> Date: Mon, 12 Jun 2023 18:16:22 +0200 Subject: [PATCH 036/241] Peter/chore update lib 2.3.3 (#1282) * chore: update monetary to latest 0.7.3 * chore: update lib to 2.3.3. --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 80b15375cc..f4eff5e25a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@headlessui/react": "^1.1.1", "@heroicons/react": "^2.0.18", "@interlay/bridge": "^0.3.11", - "@interlay/interbtc-api": "2.3.1", + "@interlay/interbtc-api": "2.3.3", "@interlay/monetary-js": "0.7.3", "@polkadot/api": "9.14.2", "@polkadot/extension-dapp": "0.44.1", diff --git a/yarn.lock b/yarn.lock index bdfcd1b1a2..130a22bd57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2120,10 +2120,10 @@ dependencies: axios "^0.21.1" -"@interlay/interbtc-api@2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-2.3.1.tgz#99bd9058453f6125d0fe1aa7bae208acda3191ed" - integrity sha512-XTbFNz0W/ev9cLfO0hKOfoPa79ARUrhKItrNolA98n055DMWHS7Lu9P1HUBG9KfKvgoiB5hQBo1Gc4hG+oPKQg== +"@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" From c4f05dc11f4d28bb2638c0f6ad6ce478679013c3 Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Wed, 14 Jun 2023 10:54:22 +0100 Subject: [PATCH 037/241] fix: enable faucet on Interlay testnet (#1289) * fix: enable faucet on Interlay testnet * fix: prefer governance token ticker to symbol --- src/parts/Topbar/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parts/Topbar/index.tsx b/src/parts/Topbar/index.tsx index 4b7aa388e4..63098f3442 100644 --- a/src/parts/Topbar/index.tsx +++ b/src/parts/Topbar/index.tsx @@ -41,7 +41,7 @@ const Topbar = (): JSX.Element => { 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; @@ -106,7 +106,7 @@ const Topbar = (): JSX.Element => { {isBanxaEnabled ? : } {selectedAccount !== undefined && ( <> - {process.env.REACT_APP_FAUCET_URL && kintBalanceIsZero && ( + {process.env.REACT_APP_FAUCET_URL && governanceTokenBalanceIsZero && ( <> Date: Wed, 14 Jun 2023 10:57:40 +0100 Subject: [PATCH 038/241] chore: bump bridge (#1285) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f4eff5e25a..edcbe63f5b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@craco/craco": "^6.1.1", "@headlessui/react": "^1.1.1", "@heroicons/react": "^2.0.18", - "@interlay/bridge": "^0.3.11", + "@interlay/bridge": "^0.3.13", "@interlay/interbtc-api": "2.3.3", "@interlay/monetary-js": "0.7.3", "@polkadot/api": "9.14.2", diff --git a/yarn.lock b/yarn.lock index 130a22bd57..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" From 1b486854eb0af10d179805a0e3b2516efd73c029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sim=C3=A3o?= Date: Thu, 15 Jun 2023 16:35:05 +0100 Subject: [PATCH 039/241] fix(Swap): update trade object on each block (#1297) --- .../AMM/Swap/components/SwapForm/SwapForm.tsx | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx b/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx index eeccf0d580..25753b06ca 100644 --- a/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx +++ b/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx @@ -4,7 +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 { 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'; @@ -20,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'; @@ -120,20 +121,21 @@ const SwapForm = ({ 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); - }, - 500, - [inputAmount, pair] - ); + setTrade(trade); + }; + + // 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); From 766918179adf585638be8d04701bb7811a269bdf Mon Sep 17 00:00:00 2001 From: ns212 <73105077+ns212@users.noreply.github.com> Date: Mon, 19 Jun 2023 09:13:49 +0100 Subject: [PATCH 040/241] 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> --- api/market_data.py | 60 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/api/market_data.py b/api/market_data.py index f63b4b7cd6..358fdc6f81 100644 --- a/api/market_data.py +++ b/api/market_data.py @@ -15,11 +15,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 +24,60 @@ 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() + + return { + data["Name"].lower(): { + "usd": data["Price"], + } + } + + +@app.route("/marketdata/price", methods=["GET"]) +def get_price(): + args = request.args + + price_source = request.headers.get('x-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) From 4a1922608c7a9a9c8d5f2777b1831fc6428967af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Slan=C3=BD?= <47864599+peterslany@users.noreply.github.com> Date: Mon, 19 Jun 2023 12:09:23 +0200 Subject: [PATCH 041/241] 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 --- src/components/LoanPositionsTable/ApyCell.tsx | 2 +- .../AMM/Pools/components/DepositForm/DepositOutputAssets.tsx | 2 +- src/utils/helpers/loans.ts | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/LoanPositionsTable/ApyCell.tsx b/src/components/LoanPositionsTable/ApyCell.tsx index 97cba39349..d36911f7be 100644 --- a/src/components/LoanPositionsTable/ApyCell.tsx +++ b/src/components/LoanPositionsTable/ApyCell.tsx @@ -32,7 +32,7 @@ const ApyCell = ({ }: ApyCellProps): JSX.Element => { const rewardsApy = getSubsidyRewardApy(currency, rewardsPerYear, prices); - const totalApy = isBorrow ? apy.sub(rewardsApy || 0) : apy.add(rewardsApy || 0); + const totalApy = isBorrow ? (rewardsApy || Big(0)).sub(apy) : apy.add(rewardsApy || 0); const totalApyLabel = getApyLabel(totalApy); 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/utils/helpers/loans.ts b/src/utils/helpers/loans.ts index a731bfaed5..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); From 4e1c721e9265a48419595ca66a479166fbca066a Mon Sep 17 00:00:00 2001 From: ns212 <73105077+ns212@users.noreply.github.com> Date: Mon, 19 Jun 2023 13:07:08 +0100 Subject: [PATCH 042/241] api: select price source via query param and ticker renaming (#1307) --- api/market_data.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/api/market_data.py b/api/market_data.py index 358fdc6f81..85866687c8 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): @@ -47,8 +50,11 @@ def dia(asset): resp = requests.get(url, headers=headers_dict) data = resp.json() + # optionally rename the ticker + ticker = tickers.get(data["Name"], data["Name"]).lower() + return { - data["Name"].lower(): { + ticker: { "usd": data["Price"], } } @@ -58,7 +64,7 @@ def dia(asset): def get_price(): args = request.args - price_source = request.headers.get('x-price-source') + price_source = args.get('price-source') data = {} From a6885d820da61ca3f8e12b17ccda2ba1a5f88489 Mon Sep 17 00:00:00 2001 From: ns212 <73105077+ns212@users.noreply.github.com> Date: Mon, 19 Jun 2023 16:24:59 +0100 Subject: [PATCH 043/241] api: fix tether label for dia (#1309) --- api/market_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/market_data.py b/api/market_data.py index 85866687c8..82fd2d076f 100644 --- a/api/market_data.py +++ b/api/market_data.py @@ -9,7 +9,7 @@ api_key = os.environ.get("CG_API_KEY") tickers = { - "tether usd": "tether", + "Tether USD": "tether", } @app.after_request From bf818bd5593d3cda5bfa196ebc5d7302b695f5e7 Mon Sep 17 00:00:00 2001 From: Thomas Jeatt Date: Tue, 20 Jun 2023 10:43:19 +0100 Subject: [PATCH 044/241] chore: release v2.34.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index edcbe63f5b..138d888dde 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interbtc-ui", - "version": "2.33.0", + "version": "2.34.0", "private": true, "dependencies": { "@craco/craco": "^6.1.1", From 1b9ce37ec939f1db113c348d484bd4321f7b8e9a Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Thu, 22 Jun 2023 10:11:34 +0100 Subject: [PATCH 045/241] chore: update XCM RPCs (#1324) --- src/utils/hooks/api/xcm/xcm-endpoints.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/utils/hooks/api/xcm/xcm-endpoints.ts b/src/utils/hooks/api/xcm/xcm-endpoints.ts index fe751d615b..73fbbe80c7 100644 --- a/src/utils/hooks/api/xcm/xcm-endpoints.ts +++ b/src/utils/hooks/api/xcm/xcm-endpoints.ts @@ -20,8 +20,16 @@ const XCMEndpoints: XCMEndpointsRecord = { kusama: ['wss://kusama-rpc.polkadot.io', 'wss://kusama-rpc.dwellir.com'], parallel: ['wss://rpc.parallel.fi'], polkadot: ['wss://rpc.polkadot.io', 'wss://polkadot-rpc.dwellir.com'], - statemine: ['wss://statemine-rpc.polkadot.io', 'wss://statemine-rpc.dwellir.com'], - statemint: ['wss://statemint-rpc.polkadot.io', 'wss://statemint-rpc.dwellir.com'] + statemine: [ + 'wss://kusama-asset-hub-rpc.polkadot.io', + 'wss://statemine-rpc.dwellir.com', + 'wss://statemine-rpc-tn.dwellir.com' + ], + statemint: [ + 'wss://polkadot-asset-hub-rpc.polkadot.io', + 'wss://statemint-rpc.dwellir.com', + 'wss://statemint-rpc-tn.dwellir.com' + ] }; export { XCMEndpoints }; From 019777afe071a8c95b386dc0f112cee80ea1c0bb Mon Sep 17 00:00:00 2001 From: Thomas Jeatt Date: Thu, 22 Jun 2023 10:12:28 +0100 Subject: [PATCH 046/241] chore: release v2.34.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 138d888dde..ffc891796a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interbtc-ui", - "version": "2.34.0", + "version": "2.34.1", "private": true, "dependencies": { "@craco/craco": "^6.1.1", From dc2ffd669b5cc21ff399376625c4d0f78b92ddfb Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:09:46 +0100 Subject: [PATCH 047/241] fix: correct wallet balance (#1334) --- .../components/WalletInsights/WalletInsights.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/Wallet/WalletOverview/components/WalletInsights/WalletInsights.tsx b/src/pages/Wallet/WalletOverview/components/WalletInsights/WalletInsights.tsx index ddbfbe04d3..1cba2783bf 100644 --- a/src/pages/Wallet/WalletOverview/components/WalletInsights/WalletInsights.tsx +++ b/src/pages/Wallet/WalletOverview/components/WalletInsights/WalletInsights.tsx @@ -39,8 +39,7 @@ const WalletInsights = ({ balances }: WalletInsightsProps): JSX.Element => { ); const totalBalance = rawBalance - ?.add(accountLendingStatistics?.supplyAmountUSD || 0) - .sub(accountLendingStatistics?.borrowAmountUSD || 0) + ?.sub(accountLendingStatistics?.borrowAmountUSD || 0) .add(accountPools?.accountLiquidityUSD || 0); const totalBalanceLabel = totalBalance ? formatUSD(totalBalance.toNumber(), { compact: true }) : '-'; From 054763c3ec91c2c8cd1d1e1e93856971e63583ab Mon Sep 17 00:00:00 2001 From: ns212 <73105077+ns212@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:10:29 +0100 Subject: [PATCH 048/241] api: switch to coingecko pro url (#1321) --- api/market_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/market_data.py b/api/market_data.py index 82fd2d076f..4f8c1ce875 100644 --- a/api/market_data.py +++ b/api/market_data.py @@ -24,7 +24,7 @@ def coingecko(args): "accept": "application/json", "x-cg-pro-api-key": api_key, } - url = "https://api.coingecko.com/api/v3/simple/price" + url = "https://pro-api.coingecko.com/api/v3/simple/price" resp = requests.get(url, params=args, headers=headers_dict) data = resp.json() return data From bd49606cafe861d0c78bc6c96e6220c00969a2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sim=C3=A3o?= Date: Mon, 26 Jun 2023 10:39:09 +0100 Subject: [PATCH 049/241] Peter/feat tx fee with swapped currency (#1340) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update monetary to latest 0.7.3 * feat: refactor Transfer and theme (#1244) * wip: initial changes to move table * chore: remove unused component * Revert "chore: remove unused component" This reverts commit 0db71a15538b776c73b752a98d2e825d890d2ea1. * chore: remove unused component * chore: use translation file * fix: add missing p tags * wip * feat: refactor Transfer and theme (#1244) * feat(Bridge): revamp Issue and Redeem (#1279) * wip * feat(TransactionDetails): extend component to support fee selector (#1292) * feat: add tx fee estimation and swap for tx fee payment integration * fix: remove impossible condition * feat: integrate use-transaction with TransactionFeeDetails (#1294) * feat: integrate use-transaction with TransactionFeeDetails * fix: code review * refactor: code review * feat: add fee estimate loading state * Rui/fee estimate transfer form (#1296) * feat: add fee estimate to transfer form * Update src/pages/Transfer/TransferForms/components/TransferForm/TransferForm.tsx Co-authored-by: Peter Slaný <47864599+peterslany@users.noreply.github.com> --------- Co-authored-by: Peter Slaný <47864599+peterslany@users.noreply.github.com> * Feature/UI updates/navigation styling (#1293) * wip: initial navigation styling * refactor: remove icons from secondary navigation items * refactor: split navigation into primary/secondary * fix: add bg colour to nav to prevent problems on small screens * refactor: update accordion styles * refactor: remove redundant code and console log * refactor: change Kintsugi background colour * fix: show navigation item names * fix: remove redundant conditional * fix: code * fix: changes to list style and disable 0 balance fee tokens * feat(bringyourownfee): add check for existing trade path * Update src/utils/hooks/transaction/use-transaction.ts Co-authored-by: Dominik Harz * Update src/utils/hooks/transaction/use-transaction.ts Co-authored-by: Dominik Harz * refactor: move multiplier to constant * feat: add fee validation and other improvements to form validation (#1303) * Peter/feat griefing collateral multicurrency (#1310) * feat: add selectable griefing collateral currency to issue request form * feat: add oracle currency hook and wrap up griefing collateral work * feat(Swap): add custom fee (#1315) * Peter/feat byof bridge page (#1328) * wip * refactor: issue page with griefing collateral select * feat(bringyourownfees): redeem form * refactor: renaming * feat: add redeem request to getActionAmount * feat(Pools): add fee estimate (#1322) * feat(Loans): add fee estimate (#1332) * feat(Vaults): add fee estimate to vault creation (#1333) * fix(Redeem): add missing BTC address validation (#1336) * fix: redeem getActionAmount type mismatch * Tom/UI updates/minor changes (#1308) * refactor: add vault table background colour * fix: typo * refactor: styled card for vault selector * refactor: wrap vault transaction tables in card component * fix: typo * refactor: add shadowed prop to card component * refactor: use card component for transactions table * refactor: move request id in legacy issue/request modal * refactor: use request id dictionary item * chore: update Interlay logo * refactor: update icon and logo colours * feature: add bg image * wip: add background image to Layout component * refactor: add Wrapper component * wip: initial values for background image position * refactor: minor styling changes * refactor: revert unneeded change * refactor: move and rename Transaction component * feat: sort currencies by balance (#1338) --------- Co-authored-by: Peter Co-authored-by: Thomas Jeatt Co-authored-by: Peter Slaný <47864599+peterslany@users.noreply.github.com> Co-authored-by: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Co-authored-by: Dominik Harz --- .storybook/preview.js | 1 - package.json | 2 +- src/App.tsx | 9 +- src/assets/img/dysonsphere.svg | 1 + src/assets/img/interlay-logo-with-text.svg | 29 +- src/assets/locales/en/translation.json | 32 +- src/common/utils/utils.ts | 12 +- .../Accordion/Accordion.style.tsx | 4 +- .../Accordion/AccordionItem.tsx | 2 +- src/component-library/Alert/Alert.tsx | 18 +- src/component-library/Card/Card.style.tsx | 16 +- src/component-library/Card/Card.tsx | 12 +- src/component-library/Input/BaseInput.tsx | 7 +- src/component-library/Input/Input.style.tsx | 22 +- src/component-library/Label/Label.style.tsx | 2 +- src/component-library/List/List.style.tsx | 5 +- src/component-library/Meter/Meter.style.tsx | 4 +- src/component-library/Meter/Meter.tsx | 4 +- .../Select/Select.stories.tsx | 2 +- src/component-library/Select/Select.style.tsx | 2 + src/component-library/Select/Select.tsx | 45 +- src/component-library/Select/SelectModal.tsx | 10 +- .../Select/SelectTrigger.tsx | 13 +- src/component-library/Switch/Switch.style.tsx | 9 +- src/component-library/Switch/Switch.tsx | 17 +- src/component-library/Tabs/Tabs.style.tsx | 4 + .../TokenInput/TokenInput.style.tsx | 3 +- .../TokenInput/TokenListItem.tsx | 30 + .../TokenInput/TokenSelect.tsx | 31 +- src/component-library/TokenInput/index.tsx | 4 +- src/component-library/Tooltip/Tooltip.tsx | 2 +- src/component-library/index.tsx | 4 +- .../theme/theme.interlay.css | 14 +- .../theme/theme.kintsugi.css | 5 + src/component-library/theme/theme.ts | 23 +- src/component-library/utils/prop-types.ts | 10 +- .../PlusDivider/PlusDivider.styles.tsx | 30 + src/components/PlusDivider/PlusDivider.tsx | 19 + src/components/PlusDivider/index.tsx | 2 + .../TransactionDetails.style.tsx | 41 ++ .../TransactionDetails/TransactionDetails.tsx | 14 + .../TransactionDetailsDd.tsx | 10 + .../TransactionDetailsDt.tsx | 33 ++ .../TransactionDetailsGroup.tsx | 16 + .../TransactionSelectToken.tsx | 61 ++ src/components/TransactionDetails/index.tsx | 10 + .../TransactionFeeDetails.tsx | 94 +++ .../TransactionFeeDetails/index.tsx | 2 + src/components/index.tsx | 5 + .../CancelledIssueRequest/index.tsx | 2 +- .../CompletedIssueRequest/index.tsx | 2 +- .../ConfirmedIssueRequest/index.tsx | 2 +- .../ReceivedIssueRequest/index.tsx | 2 +- .../IssueUI/WhoopsStatusUI/index.tsx | 2 +- src/legacy-components/IssueUI/index.tsx | 11 + .../CompletedRedeemRequest/index.tsx | 2 +- .../DefaultRedeemRequest/index.tsx | 2 +- .../index.tsx | 2 +- .../ReimbursedRedeemRequest/index.tsx | 2 +- .../RetriedRedeemRequest/index.tsx | 2 +- .../RedeemUI/ReimburseStatusUI/index.tsx | 2 +- src/legacy-components/RedeemUI/index.tsx | 11 + .../RequestWrapper/index.tsx | 0 .../InterlayDefaultContainedButton/index.tsx | 5 +- src/lib/form/index.tsx | 6 - src/lib/form/schemas/amm.ts | 51 -- src/lib/form/schemas/bridge.ts | 105 ++++ src/lib/form/schemas/index.ts | 36 +- src/lib/form/schemas/loans.ts | 53 +- src/lib/form/schemas/pools.ts | 72 +++ src/lib/form/schemas/swap.ts | 21 +- src/lib/form/schemas/transfers.ts | 48 +- src/lib/form/schemas/vaults.ts | 32 +- src/lib/form/use-form.tsx | 131 +++-- src/lib/form/validate.ts | 21 + src/lib/form/yup.custom.ts | 56 +- .../components/DepositForm/DepositDivider.tsx | 15 - .../DepositForm/DepositForm.styles.tsx | 49 +- .../components/DepositForm/DepositForm.tsx | 158 ++--- .../Pools/components/PoolModal/PoolModal.tsx | 18 +- .../PoolsInsights/PoolsInsights.tsx | 149 ++++- .../components/WithdrawForm/WithdrawForm.tsx | 112 ++-- .../AMM/Swap/components/SwapForm/SwapCTA.tsx | 28 +- .../Swap/components/SwapForm/SwapDivider.tsx | 5 +- .../components/SwapForm/SwapForm.style.tsx | 10 +- .../AMM/Swap/components/SwapForm/SwapForm.tsx | 114 ++-- .../components/SwapInfo/SwapInfo.style.tsx | 7 +- .../AMM/Swap/components/SwapInfo/SwapInfo.tsx | 73 +-- .../ManualIssueExecutionActionsTable.tsx | 2 +- src/pages/Bridge/Bridge.tsx | 16 + .../BridgeOverview/BridgeOverview.styles.tsx | 19 + .../Bridge/BridgeOverview/BridgeOverview.tsx | 62 ++ .../components/IssueForm/IssueForm.styles.tsx | 23 + .../components/IssueForm/IssueForm.tsx | 289 +++++++++ .../components/IssueForm/index.tsx | 2 + .../LegacyBurnForm/LegacyBurnForm.tsx} | 12 +- .../components/LegacyBurnForm/index.tsx | 1 + .../LegacyIssueModal/LegacyIssueModal.tsx | 50 ++ .../components/LegacyIssueModal/index.tsx | 1 + .../LegacyRedeemModal/LegacyRedeemModal.tsx} | 10 +- .../components/LegacyRedeemModal/index.tsx | 1 + .../IssueRequestModal/index.tsx | 7 +- .../IssueRequestsTable/index.tsx | 114 ++-- .../RedeemRequestModal/index.tsx | 7 +- .../RedeemRequestsTable/index.tsx | 115 ++-- .../RequestModalTitle/index.tsx | 0 .../components/LegacyTransactions}/index.tsx | 14 +- .../RedeemForm/PremiumRedeemCard.tsx | 33 ++ .../RedeemForm/RedeemForm.styles.tsx | 23 + .../components/RedeemForm/RedeemForm.tsx | 335 +++++++++++ .../components/RedeemForm/index.tsx | 2 + .../RequestLimitsCard/RequestLimitsCard.tsx | 46 ++ .../components/RequestLimitsCard/index.tsx | 2 + .../SelectVaultCard/SelectVaultCard.tsx | 43 ++ .../SelectVaultCard/VaultListItem.tsx | 48 ++ .../SelectVaultCard/VaultSelect.style.tsx | 63 ++ .../SelectVaultCard/VaultSelect.tsx | 27 + .../components/SelectVaultCard/index.tsx | 2 + .../TransactionDetails.style.tsx | 11 + .../TransactionDetails/TransactionDetails.tsx | 123 ++++ .../components/TransactionDetails/index.tsx | 2 + .../BridgeOverview/components/index.tsx | 7 + src/pages/Bridge/BridgeOverview/index.tsx | 3 + src/pages/Bridge/IssueForm/index.tsx | 551 ----------------- .../Bridge/ManualVaultSelectUI/index.tsx | 54 -- .../Bridge/ParachainStatusInfo/index.tsx | 51 -- src/pages/Bridge/RedeemForm/index.tsx | 554 ------------------ src/pages/Bridge/index.tsx | 149 +---- .../CollateralModal/CollateralModal.tsx | 91 ++- .../LoanActionInfo/LoanActionInfo.style.tsx | 12 - .../LoanActionInfo/LoanActionInfo.tsx | 36 -- .../components/LoanActionInfo/LoanGroup.tsx | 42 -- .../LoanActionInfo/RewardsGroup.tsx | 40 -- .../components/LoanActionInfo/index.tsx | 2 - .../LoanDetails/LoanDetails.style.tsx} | 0 .../components/LoanDetails/LoanDetails.tsx | 47 ++ .../components/LoanDetails/RewardsDetails.tsx | 40 ++ .../components/LoanDetails/index.tsx | 2 + .../components/LoanForm/LoanForm.tsx | 110 +++- .../components/LoanModal/LoanModal.tsx | 18 +- .../LoansInsights/LoansInsights.tsx | 86 ++- .../components/LoansTables/LendTables.tsx | 14 +- .../hooks/use-loan-form-data.tsx | 10 +- .../components/index.tsx | 4 - .../Transfer/CrossChainTransferForm/index.tsx | 3 - src/pages/Transfer/TokenAmountField/index.tsx | 73 --- src/pages/Transfer/Transfer.style.tsx | 9 - src/pages/Transfer/Transfer.tsx | 16 + src/pages/Transfer/TransferForm/index.tsx | 164 ------ .../TransferForms/TransferForms.styles.tsx | 19 + .../Transfer/TransferForms/TransferForms.tsx | 30 + .../components/ChainIcon/ChainIcon.style.tsx | 0 .../components/ChainIcon/ChainIcon.tsx | 0 .../components/ChainIcon/icons/Acala.tsx | 0 .../components/ChainIcon/icons/Astar.tsx | 0 .../components/ChainIcon/icons/Bifrost.tsx | 0 .../components/ChainIcon/icons/Heiko.tsx | 0 .../components/ChainIcon/icons/Hydra.tsx | 0 .../components/ChainIcon/icons/Interlay.tsx | 0 .../components/ChainIcon/icons/Karura.tsx | 0 .../components/ChainIcon/icons/Kintsugi.tsx | 0 .../components/ChainIcon/icons/Kusama.tsx | 0 .../components/ChainIcon/icons/Parallel.tsx | 0 .../components/ChainIcon/icons/Polkadot.tsx | 0 .../components/ChainIcon/icons/Statemine.tsx | 0 .../components/ChainIcon/icons/Statemint.tsx | 0 .../components/ChainIcon/icons/index.ts | 0 .../components/ChainIcon/index.tsx | 0 .../ChainSelect/ChainSelect.style.tsx | 0 .../components/ChainSelect/ChainSelect.tsx | 4 +- .../components/ChainSelect/index.tsx | 0 .../CrossChainTransferForm.styles.tsx | 2 +- .../CrossChainTransferForm.tsx | 31 +- .../CrossChainTransferForm/index.tsx | 1 + .../components/TransferForm/TransferForm.tsx | 162 +++++ .../components/TransferForm/index.tsx | 1 + .../TransferForms/components/index.tsx | 4 + src/pages/Transfer/TransferForms/index.tsx | 3 + src/pages/Transfer/index.tsx | 33 +- .../Vaults/Vault/RequestIssueModal/index.tsx | 3 +- .../SubmittedIssueRequestModal/index.tsx | 2 +- src/pages/Vaults/Vault/VaultDashboard.tsx | 22 +- .../TransactionHistory.styles.tsx | 84 --- .../TransactionHistory/TransactionHistory.tsx | 76 --- .../TransactionStatusTag.tsx | 41 -- .../TransactionHistory/TransactionTable.tsx | 82 --- .../components/TransactionHistory/index.tsx | 2 - src/pages/Vaults/Vault/components/index.tsx | 13 +- .../CreateVaultWizard.styles.tsx | 7 - .../CreateVaultWizard/CreateVaultWizard.tsx | 6 +- .../DespositCollateralStep.tsx | 138 +++-- .../components/CreateVaults/CreateVaults.tsx | 13 +- .../utils/use-deposit-collateral.tsx | 24 +- .../Navigation/SidebarNavLink/index.tsx | 2 +- .../SidebarContent/Navigation/index.tsx | 99 +++- .../SocialMediaContainer/index.tsx | 12 +- src/parts/Sidebar/SidebarContent/index.tsx | 4 +- .../Topbar/GetGovernanceTokenUI/index.tsx | 9 +- src/parts/Topbar/index.tsx | 9 +- src/parts/Wrapper/Wrapper.style.tsx | 12 + src/parts/Wrapper/index.tsx | 12 + src/types/bridge.ts | 7 + src/utils/constants/general.ts | 2 +- src/utils/constants/links.ts | 1 - .../helpers/is-valid-polkadot-address.ts | 14 - .../hooks/api/bridge/use-get-issue-data.tsx | 77 +++ .../bridge/use-get-issue-request-limits.tsx | 30 + .../bridge/use-get-max-burnable-tokens.tsx | 59 ++ .../hooks/api/bridge/use-get-redeem-data.tsx | 98 ++++ src/utils/hooks/api/bridge/use-get-vaults.tsx | 132 +++++ .../api/oracle/use-get-oracle-currencies.ts | 37 ++ .../hooks/api/tokens/use-get-balances.tsx | 23 +- .../api/use-get-collateral-currencies.tsx | 1 + src/utils/hooks/api/use-get-exchange-rate.tsx | 26 + .../api/vaults/use-get-vault-transactions.tsx | 14 +- src/utils/hooks/transaction/extrinsics/lib.ts | 12 +- src/utils/hooks/transaction/types/hook.ts | 109 ++++ src/utils/hooks/transaction/types/index.ts | 2 +- src/utils/hooks/transaction/types/loans.ts | 7 +- .../hooks/transaction/use-transaction.ts | 206 ++++--- src/utils/hooks/transaction/utils/fee.ts | 179 ++++++ src/utils/hooks/transaction/utils/form.ts | 11 + src/utils/hooks/transaction/utils/params.ts | 29 + src/utils/hooks/use-select-currency.tsx | 103 ++++ src/utils/hooks/use-tab-page-location.tsx | 37 ++ yarn.lock | 8 +- 226 files changed, 4829 insertions(+), 3216 deletions(-) create mode 100644 src/assets/img/dysonsphere.svg create mode 100644 src/component-library/TokenInput/TokenListItem.tsx create mode 100644 src/components/PlusDivider/PlusDivider.styles.tsx create mode 100644 src/components/PlusDivider/PlusDivider.tsx create mode 100644 src/components/PlusDivider/index.tsx create mode 100644 src/components/TransactionDetails/TransactionDetails.style.tsx create mode 100644 src/components/TransactionDetails/TransactionDetails.tsx create mode 100644 src/components/TransactionDetails/TransactionDetailsDd.tsx create mode 100644 src/components/TransactionDetails/TransactionDetailsDt.tsx create mode 100644 src/components/TransactionDetails/TransactionDetailsGroup.tsx create mode 100644 src/components/TransactionDetails/TransactionSelectToken.tsx create mode 100644 src/components/TransactionDetails/index.tsx create mode 100644 src/components/TransactionFeeDetails/TransactionFeeDetails.tsx create mode 100644 src/components/TransactionFeeDetails/index.tsx rename src/{pages/Bridge => legacy-components}/RequestWrapper/index.tsx (100%) delete mode 100644 src/lib/form/schemas/amm.ts create mode 100644 src/lib/form/schemas/bridge.ts create mode 100644 src/lib/form/schemas/pools.ts create mode 100644 src/lib/form/validate.ts delete mode 100644 src/pages/AMM/Pools/components/DepositForm/DepositDivider.tsx create mode 100644 src/pages/Bridge/Bridge.tsx create mode 100644 src/pages/Bridge/BridgeOverview/BridgeOverview.styles.tsx create mode 100644 src/pages/Bridge/BridgeOverview/BridgeOverview.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/IssueForm/IssueForm.styles.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/IssueForm/IssueForm.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/IssueForm/index.tsx rename src/pages/Bridge/{BurnForm/index.tsx => BridgeOverview/components/LegacyBurnForm/LegacyBurnForm.tsx} (97%) create mode 100644 src/pages/Bridge/BridgeOverview/components/LegacyBurnForm/index.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/LegacyIssueModal/LegacyIssueModal.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/LegacyIssueModal/index.tsx rename src/pages/Bridge/{RedeemForm/SubmittedRedeemRequestModal/index.tsx => BridgeOverview/components/LegacyRedeemModal/LegacyRedeemModal.tsx} (95%) create mode 100644 src/pages/Bridge/BridgeOverview/components/LegacyRedeemModal/index.tsx rename src/pages/{Transactions => Bridge/BridgeOverview/components/LegacyTransactions}/IssueRequestsTable/IssueRequestModal/index.tsx (69%) rename src/pages/{Transactions => Bridge/BridgeOverview/components/LegacyTransactions}/IssueRequestsTable/index.tsx (81%) rename src/pages/{Transactions => Bridge/BridgeOverview/components/LegacyTransactions}/RedeemRequestsTable/RedeemRequestModal/index.tsx (70%) rename src/pages/{Transactions => Bridge/BridgeOverview/components/LegacyTransactions}/RedeemRequestsTable/index.tsx (84%) rename src/pages/{Transactions => Bridge/BridgeOverview/components/LegacyTransactions}/RequestModalTitle/index.tsx (100%) rename src/pages/{Transactions => Bridge/BridgeOverview/components/LegacyTransactions}/index.tsx (81%) create mode 100644 src/pages/Bridge/BridgeOverview/components/RedeemForm/PremiumRedeemCard.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/RedeemForm/RedeemForm.styles.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/RedeemForm/RedeemForm.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/RedeemForm/index.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/RequestLimitsCard/RequestLimitsCard.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/RequestLimitsCard/index.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/SelectVaultCard/SelectVaultCard.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/SelectVaultCard/VaultListItem.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/SelectVaultCard/VaultSelect.style.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/SelectVaultCard/VaultSelect.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/SelectVaultCard/index.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/TransactionDetails/TransactionDetails.style.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/TransactionDetails/TransactionDetails.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/TransactionDetails/index.tsx create mode 100644 src/pages/Bridge/BridgeOverview/components/index.tsx create mode 100644 src/pages/Bridge/BridgeOverview/index.tsx delete mode 100644 src/pages/Bridge/IssueForm/index.tsx delete mode 100644 src/pages/Bridge/ManualVaultSelectUI/index.tsx delete mode 100644 src/pages/Bridge/ParachainStatusInfo/index.tsx delete mode 100644 src/pages/Bridge/RedeemForm/index.tsx delete mode 100644 src/pages/Loans/LoansOverview/components/LoanActionInfo/LoanActionInfo.style.tsx delete mode 100644 src/pages/Loans/LoansOverview/components/LoanActionInfo/LoanActionInfo.tsx delete mode 100644 src/pages/Loans/LoansOverview/components/LoanActionInfo/LoanGroup.tsx delete mode 100644 src/pages/Loans/LoansOverview/components/LoanActionInfo/RewardsGroup.tsx delete mode 100644 src/pages/Loans/LoansOverview/components/LoanActionInfo/index.tsx rename src/pages/{AMM/Pools/components/WithdrawForm/WithdrawForm.styles.tsx => Loans/LoansOverview/components/LoanDetails/LoanDetails.style.tsx} (100%) create mode 100644 src/pages/Loans/LoansOverview/components/LoanDetails/LoanDetails.tsx create mode 100644 src/pages/Loans/LoansOverview/components/LoanDetails/RewardsDetails.tsx create mode 100644 src/pages/Loans/LoansOverview/components/LoanDetails/index.tsx delete mode 100644 src/pages/Transfer/CrossChainTransferForm/components/index.tsx delete mode 100644 src/pages/Transfer/CrossChainTransferForm/index.tsx delete mode 100644 src/pages/Transfer/TokenAmountField/index.tsx delete mode 100644 src/pages/Transfer/Transfer.style.tsx create mode 100644 src/pages/Transfer/Transfer.tsx delete mode 100644 src/pages/Transfer/TransferForm/index.tsx create mode 100644 src/pages/Transfer/TransferForms/TransferForms.styles.tsx create mode 100644 src/pages/Transfer/TransferForms/TransferForms.tsx rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/ChainIcon.style.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/ChainIcon.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/icons/Acala.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/icons/Astar.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/icons/Bifrost.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/icons/Heiko.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/icons/Hydra.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/icons/Interlay.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/icons/Karura.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/icons/Kintsugi.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/icons/Kusama.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/icons/Parallel.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/icons/Polkadot.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/icons/Statemine.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/icons/Statemint.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/icons/index.ts (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainIcon/index.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainSelect/ChainSelect.style.tsx (100%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainSelect/ChainSelect.tsx (93%) rename src/pages/Transfer/{CrossChainTransferForm => TransferForms}/components/ChainSelect/index.tsx (100%) rename src/pages/Transfer/{ => TransferForms/components}/CrossChainTransferForm/CrossChainTransferForm.styles.tsx (95%) rename src/pages/Transfer/{ => TransferForms/components}/CrossChainTransferForm/CrossChainTransferForm.tsx (91%) create mode 100644 src/pages/Transfer/TransferForms/components/CrossChainTransferForm/index.tsx create mode 100644 src/pages/Transfer/TransferForms/components/TransferForm/TransferForm.tsx create mode 100644 src/pages/Transfer/TransferForms/components/TransferForm/index.tsx create mode 100644 src/pages/Transfer/TransferForms/components/index.tsx create mode 100644 src/pages/Transfer/TransferForms/index.tsx rename src/pages/{Bridge/IssueForm => Vaults/Vault}/SubmittedIssueRequestModal/index.tsx (97%) delete mode 100644 src/pages/Vaults/Vault/components/TransactionHistory/TransactionHistory.styles.tsx delete mode 100644 src/pages/Vaults/Vault/components/TransactionHistory/TransactionHistory.tsx delete mode 100644 src/pages/Vaults/Vault/components/TransactionHistory/TransactionStatusTag.tsx delete mode 100644 src/pages/Vaults/Vault/components/TransactionHistory/TransactionTable.tsx delete mode 100644 src/pages/Vaults/Vault/components/TransactionHistory/index.tsx create mode 100644 src/parts/Wrapper/Wrapper.style.tsx create mode 100644 src/parts/Wrapper/index.tsx create mode 100644 src/types/bridge.ts delete mode 100644 src/utils/helpers/is-valid-polkadot-address.ts create mode 100644 src/utils/hooks/api/bridge/use-get-issue-data.tsx create mode 100644 src/utils/hooks/api/bridge/use-get-issue-request-limits.tsx create mode 100644 src/utils/hooks/api/bridge/use-get-max-burnable-tokens.tsx create mode 100644 src/utils/hooks/api/bridge/use-get-redeem-data.tsx create mode 100644 src/utils/hooks/api/bridge/use-get-vaults.tsx create mode 100644 src/utils/hooks/api/oracle/use-get-oracle-currencies.ts create mode 100644 src/utils/hooks/api/use-get-exchange-rate.tsx create mode 100644 src/utils/hooks/transaction/types/hook.ts create mode 100644 src/utils/hooks/transaction/utils/fee.ts create mode 100644 src/utils/hooks/transaction/utils/form.ts create mode 100644 src/utils/hooks/transaction/utils/params.ts create mode 100644 src/utils/hooks/use-select-currency.tsx create mode 100644 src/utils/hooks/use-tab-page-location.tsx diff --git a/.storybook/preview.js b/.storybook/preview.js index ce6935b51d..71eafc0e1c 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -2,7 +2,6 @@ import '../src/component-library/theme/theme.interlay.css'; import '../src/component-library/theme/theme.kintsugi.css'; import './sb-preview.css'; import '../src/i18n'; -import "../src/lib/form/yup.custom" import { withThemes } from 'storybook-addon-themes/react'; import { addDecorator } from "@storybook/react"; import { MemoryRouter } from "react-router-dom"; diff --git a/package.json b/package.json index ffc891796a..a4d532a34f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@headlessui/react": "^1.1.1", "@heroicons/react": "^2.0.18", "@interlay/bridge": "^0.3.13", - "@interlay/interbtc-api": "2.3.3", + "@interlay/interbtc-api": "2.3.4", "@interlay/monetary-js": "0.7.3", "@polkadot/api": "9.14.2", "@polkadot/extension-dapp": "0.44.1", diff --git a/src/App.tsx b/src/App.tsx index 1722f65d75..600e0efbc2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,6 +15,7 @@ import ErrorFallback from '@/legacy-components/ErrorFallback'; import FullLoadingSpinner from '@/legacy-components/FullLoadingSpinner'; import { useSubstrate, useSubstrateSecureState } from '@/lib/substrate'; import Layout from '@/parts/Layout'; +import Wrapper from '@/parts/Wrapper'; import graphqlFetcher, { GRAPHQL_FETCHER, GraphqlReturn } from '@/services/fetchers/graphql-fetcher'; import vaultsByAccountIdQuery from '@/services/queries/vaults-by-accountId-query'; import { BitcoinNetwork } from '@/types/bitcoin'; @@ -28,7 +29,6 @@ import { FeatureFlags, useFeatureFlag } from './utils/hooks/use-feature-flag'; const Bridge = React.lazy(() => import(/* webpackChunkName: 'bridge' */ '@/pages/Bridge')); const Strategies = React.lazy(() => import(/* webpackChunkName: 'strategies' */ '@/pages/Strategies')); const Transfer = React.lazy(() => import(/* webpackChunkName: 'transfer' */ '@/pages/Transfer')); -const Transactions = React.lazy(() => import(/* webpackChunkName: 'transactions' */ '@/pages/Transactions')); const TX = React.lazy(() => import(/* webpackChunkName: 'tx' */ '@/pages/TX')); const Staking = React.lazy(() => import(/* webpackChunkName: 'staking' */ '@/pages/Staking')); const Dashboard = React.lazy(() => import(/* webpackChunkName: 'dashboard' */ '@/pages/Dashboard')); @@ -159,7 +159,7 @@ const App = (): JSX.Element => { }, [setSelectedAccount, extensions.length]); return ( - <> + {process.env.REACT_APP_BITCOIN_NETWORK === BitcoinNetwork.Testnet && } { - - - @@ -235,7 +232,7 @@ const App = (): JSX.Element => { /> - + ); }; diff --git a/src/assets/img/dysonsphere.svg b/src/assets/img/dysonsphere.svg new file mode 100644 index 0000000000..e4b8acdd7b --- /dev/null +++ b/src/assets/img/dysonsphere.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/interlay-logo-with-text.svg b/src/assets/img/interlay-logo-with-text.svg index c7c1537eec..9679df8441 100644 --- a/src/assets/img/interlay-logo-with-text.svg +++ b/src/assets/img/interlay-logo-with-text.svg @@ -1,17 +1,14 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index e6cd47edf2..cc589b2767 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -157,6 +157,12 @@ "sign_t&cs": "Sign T&Cs", "receivable_assets": "Receivable Assets", "dismiss": "Dismiss", + "insufficient_token_balance": "Insufficient {{token}} balance", + "amount": "Amount", + "select_token": "Select Token", + "fee_token": "Fee token", + "claim_rewards": "Claim Rewards", + "tx_fees": "Tx fees", "redeem_page": { "maximum_in_single_request": "Max redeemable in single request", "redeem": "Redeem", @@ -349,7 +355,7 @@ "maximum_in_single_request": "Max issuable in single request", "maximum_total_request": "Max issuable in total", "maximum_in_single_request_error": "Please enter less than {{maxAmount}} {{wrappedTokenSymbol}}.", - "request": "Request id #{{id}}", + "request": "Request id", "you_received": "You have received", "contact_team": "Contact the team for debugging if you think this is an error.", "you_did_not_send": "You did not send the BTC transaction in time or transferred amount did not not meet the requested amount of {{wrappedTokenSymbol}}.", @@ -610,7 +616,9 @@ "amount_must_be_at_least": "Amount to {{action}} must be at least {{amount}} {{token}}", "amount_must_be_at_most": "Amount to {{action}} must be at most {{amount}}", "please_enter_no_higher_available_balance": "Please enter an amount no higher than your available balance", - "field_amount": "{{field}} amount" + "field_amount": "{{field}} amount", + "please_enter_a_valid_address": "Please enter a valid address", + "ensure_adequate_amount_left_to_cover_fees": "Ensure that an adequate number of coins are left to cover the fees" }, "xcm_transfer": { "validation": { @@ -712,5 +720,25 @@ "requested_vault_replacement": "Requested vault replacement", "claiming_vesting": "Claiming vesting", "claimed_vesting": "Claimed vesting" + }, + "bridge": { + "max_issuable": "Max issuable", + "max_redeemable": "Max redeemable", + "in_single_request": "In single request", + "in_total": "In total", + "manually_select_vault": "Manually Select Vault", + "select_vault": "Select Vault", + "select_a_vault": "Select a vault", + "compensation_amount": "Compensation amount", + "bridge_fee": "Bridge Fee", + "fee_paids_to_vaults_relayers_maintainers": "The bridge fee paid to the vaults, relayers and maintainers of the system", + "security_deposit": "Security Deposit", + "security_deposit_token": "Security deposit token", + "security_deposit_is_a_denial_of_service_protection": "The security deposit is a denial-of-service protection for Vaults that is refunded to you after the minting process is completed", + "bitcoin_network_fee": "Bitcoin Network Fee", + "transaction_fee": "Transaction Fee", + "fee_for_transaction_to_be_included_in_the_parachain": "The fee for the transaction to be included in the parachain", + "premium_redeem": "Premium Redeem", + "premium_redeem_info": "If you select premium redeem, you will try to redeem with a vault that is below the secure collateral threshold. This operation has a higher risk since the vault might be liquidated. For this higher risk, you will receive a compensation." } } diff --git a/src/common/utils/utils.ts b/src/common/utils/utils.ts index 7bd290766f..e753acbb28 100644 --- a/src/common/utils/utils.ts +++ b/src/common/utils/utils.ts @@ -1,6 +1,6 @@ import { CurrencyExt, InterbtcPrimitivesVaultId, newMonetaryAmount } from '@interlay/interbtc-api'; import { BitcoinAmount, MonetaryAmount } from '@interlay/monetary-js'; -import Big from 'big.js'; +import Big, { BigSource } from 'big.js'; import { PARACHAIN_URL } from '@/constants'; import { getTokenPrice } from '@/utils/helpers/prices'; @@ -177,6 +177,14 @@ const newSafeMonetaryAmount: typeof newMonetaryAmount = (...args) => { } }; +const safeBitcoinAmount = (amount: BigSource): BitcoinAmount => { + try { + return new BitcoinAmount(amount); + } catch (e) { + return new BitcoinAmount(0); + } +}; + export { convertMonetaryAmountToUsdBig as convertMonetaryAmountToBigUSD, convertMonetaryAmountToValueInUSD, @@ -190,9 +198,11 @@ export { formatUSD, getLastMidnightTimestamps, getPolkadotLink, + getRandomArrayElement, getRandomVaultIdWithCapacity, monetaryToNumber, newSafeMonetaryAmount, + safeBitcoinAmount, shortAddress, shortTxId }; diff --git a/src/component-library/Accordion/Accordion.style.tsx b/src/component-library/Accordion/Accordion.style.tsx index fc4916d609..270ef209cb 100644 --- a/src/component-library/Accordion/Accordion.style.tsx +++ b/src/component-library/Accordion/Accordion.style.tsx @@ -28,7 +28,7 @@ const StyledAccordionItemWrapper = styled.div` @@ -49,7 +49,7 @@ const StyledAccordionItemButton = styled.button` `; const StyledChevronDown = styled(ChevronDown)>` - transform: ${({ $isExpanded }) => $isExpanded && 'rotate(-90deg)'}; + transform: ${({ $isExpanded }) => $isExpanded && 'rotate(-180deg)'}; transition: transform ${theme.transition.duration.duration150}ms ease; `; diff --git a/src/component-library/Accordion/AccordionItem.tsx b/src/component-library/Accordion/AccordionItem.tsx index 5ec1dd7593..85c5fbac08 100644 --- a/src/component-library/Accordion/AccordionItem.tsx +++ b/src/component-library/Accordion/AccordionItem.tsx @@ -41,7 +41,7 @@ const AccordionItem = >({ $isFocusVisible={isFocusVisible} > {item.props.title} - + diff --git a/src/component-library/Alert/Alert.tsx b/src/component-library/Alert/Alert.tsx index e49d9d204b..2b58cfecf6 100644 --- a/src/component-library/Alert/Alert.tsx +++ b/src/component-library/Alert/Alert.tsx @@ -1,15 +1,17 @@ -import { ReactNode } from 'react'; - +import { FlexProps } from '../Flex'; import { Status } from '../utils/prop-types'; import { StyledAlert, StyledWarningIcon } from './Alert.style'; -interface AlertProps { - status: Status; - children: ReactNode; -} +type Props = { + status?: Status; +}; + +type InheritAttrs = Omit; + +type AlertProps = Props & InheritAttrs; -const Alert = ({ status, children }: AlertProps): JSX.Element => ( - +const Alert = ({ status = 'success', children, ...props }: AlertProps): JSX.Element => ( +
{children}
diff --git a/src/component-library/Card/Card.style.tsx b/src/component-library/Card/Card.style.tsx index 84379aa430..15ea888a55 100644 --- a/src/component-library/Card/Card.style.tsx +++ b/src/component-library/Card/Card.style.tsx @@ -2,25 +2,33 @@ import styled, { css } from 'styled-components'; import { Flex } from '../Flex'; import { theme } from '../theme'; -import { CardVariants, Variants } from '../utils/prop-types'; +import { BorderRadius, CardVariants, Spacing, Variants } from '../utils/prop-types'; type StyledCardProps = { $variant: CardVariants; + $rounded: BorderRadius; + $padding: Spacing; + $shadowed: boolean; $background: Variants; $isHoverable?: boolean; $isPressable?: boolean; }; const StyledCard = styled(Flex)` - box-shadow: ${theme.boxShadow.default}; color: ${theme.colors.textPrimary}; background-color: ${({ $background }) => theme.card.bg[$background]}; border: ${({ $variant }) => ($variant === 'bordered' ? theme.border.default : theme.card.outlined.border)}; - border-radius: ${theme.rounded.xl}; - padding: ${theme.spacing.spacing6}; + border-radius: ${({ $rounded }) => theme.rounded[$rounded]}; + padding: ${({ $padding }) => theme.spacing[$padding]}; cursor: ${({ $isPressable }) => $isPressable && 'pointer'}; outline: none; + ${({ $shadowed }) => + $shadowed && + css` + box-shadow: ${theme.boxShadow.default}; + `} + ${({ $isHoverable }) => $isHoverable && css` diff --git a/src/component-library/Card/Card.tsx b/src/component-library/Card/Card.tsx index e5671390c5..ff0f164c21 100644 --- a/src/component-library/Card/Card.tsx +++ b/src/component-library/Card/Card.tsx @@ -5,7 +5,7 @@ import { forwardRef } from 'react'; import { FlexProps } from '../Flex'; import { useDOMRef } from '../utils/dom'; -import { CardVariants, Variants } from '../utils/prop-types'; +import { BorderRadius, CardVariants, Spacing, Variants } from '../utils/prop-types'; import { StyledCard } from './Card.style'; type Props = { @@ -14,6 +14,10 @@ type Props = { isHoverable?: boolean; isPressable?: boolean; isDisabled?: boolean; + rounded?: BorderRadius; + padding?: Spacing; + shadowed?: boolean; + onPress?: (e: PressEvent) => void; }; @@ -33,6 +37,9 @@ const Card = forwardRef( children, elementType, isDisabled, + rounded = 'xl', + padding = 'spacing6', + shadowed = true, ...props }, ref @@ -50,6 +57,9 @@ const Card = forwardRef( $background={background} $isHoverable={isHoverable} $isPressable={isPressable} + $rounded={rounded} + $padding={padding} + $shadowed={shadowed} direction={direction} elementType={elementType} {...mergeProps(props, isPressable ? buttonProps : {})} diff --git a/src/component-library/Input/BaseInput.tsx b/src/component-library/Input/BaseInput.tsx index c058bc71c3..3f02528dd6 100644 --- a/src/component-library/Input/BaseInput.tsx +++ b/src/component-library/Input/BaseInput.tsx @@ -5,7 +5,7 @@ import { Field, useFieldProps } from '../Field'; import { HelperTextProps } from '../HelperText'; import { LabelProps } from '../Label'; import { hasError } from '../utils/input'; -import { Sizes } from '../utils/prop-types'; +import { Sizes, Spacing } from '../utils/prop-types'; import { Adornment, StyledBaseInput } from './Input.style'; type Props = { @@ -17,6 +17,8 @@ type Props = { value?: string | ReadonlyArray | number; defaultValue?: string | ReadonlyArray | number; size?: Sizes; + // TODO: temporary + padding?: { top?: Spacing; bottom?: Spacing; left?: Spacing; right?: Spacing }; validationState?: ValidationState; onChange?: (e: React.ChangeEvent) => void; }; @@ -29,7 +31,7 @@ type BaseInputProps = Props & NativeAttrs & InheritAttrs; const BaseInput = forwardRef( ( - { startAdornment, endAdornment, bottomAdornment, disabled, size = 'medium', validationState, ...props }, + { startAdornment, endAdornment, bottomAdornment, disabled, size = 'medium', validationState, padding, ...props }, ref ): JSX.Element => { const endAdornmentRef = useRef(null); @@ -57,6 +59,7 @@ const BaseInput = forwardRef( $hasError={error} $isDisabled={!!disabled} $endAdornmentWidth={endAdornmentWidth} + $padding={padding} {...elementProps} /> {bottomAdornment && {bottomAdornment}} diff --git a/src/component-library/Input/Input.style.tsx b/src/component-library/Input/Input.style.tsx index e65ed6be50..60fab0fa0f 100644 --- a/src/component-library/Input/Input.style.tsx +++ b/src/component-library/Input/Input.style.tsx @@ -1,11 +1,14 @@ import styled from 'styled-components'; import { theme } from '../theme'; -import { Placement, Sizes } from '../utils/prop-types'; +import { Placement, Sizes, Spacing } from '../utils/prop-types'; + +const getSpacing = (padding?: Spacing) => (padding ? theme.spacing[padding] : undefined); type BaseInputProps = { $size: Sizes; $adornments: { bottom: boolean; left: boolean; right: boolean }; + $padding?: { top: Spacing; bottom: Spacing; left: Spacing; right: Spacing }; $isDisabled: boolean; $hasError: boolean; $endAdornmentWidth: number; @@ -29,8 +32,9 @@ const StyledBaseInput = styled.input` font-size: ${({ $size, $adornments }) => $adornments.bottom ? theme.input.overflow.large.text : theme.input[$size].text}; line-height: ${theme.lineHeight.base}; + font-weight: ${({ $size }) => theme.input[$size].weight}; - background-color: ${theme.input.background}; + background-color: ${({ $isDisabled }) => ($isDisabled ? theme.input.disabled.bg : theme.input.background)}; overflow: hidden; border: ${(props) => @@ -39,15 +43,16 @@ const StyledBaseInput = styled.input` : props.$hasError ? theme.input.error.border : theme.border.default}; - border-radius: ${theme.rounded.md}; + border-radius: ${theme.rounded.lg}; transition: border-color ${theme.transition.duration.duration150}ms ease-in-out, box-shadow ${theme.transition.duration.duration150}ms ease-in-out; - padding-top: ${theme.spacing.spacing2}; - padding-left: ${({ $adornments }) => ($adornments.left ? theme.input.paddingX.md : theme.spacing.spacing2)}; + padding-top: ${({ $padding }) => getSpacing($padding?.top) || theme.spacing.spacing2}; + padding-left: ${({ $adornments, $padding }) => + getSpacing($padding?.left) || ($adornments.left ? theme.input.paddingX.md : theme.spacing.spacing2)}; - padding-right: ${({ $adornments, $endAdornmentWidth }) => { - if (!$adornments.right) return theme.spacing.spacing2; + padding-right: ${({ $adornments, $endAdornmentWidth, $padding }) => { + if (!$adornments.right) return getSpacing($padding?.right) || theme.spacing.spacing2; // MEMO: adding `spacing6` is a hacky solution because // the `endAdornmentWidth` does not update width correctly @@ -56,7 +61,8 @@ const StyledBaseInput = styled.input` // the input overlap the adornment. return `calc(${$endAdornmentWidth}px + ${theme.spacing.spacing6})`; }}; - padding-bottom: ${({ $adornments }) => ($adornments.bottom ? theme.spacing.spacing6 : theme.spacing.spacing2)}; + padding-bottom: ${({ $adornments, $padding }) => + getSpacing($padding?.bottom) || ($adornments.bottom ? theme.spacing.spacing6 : theme.spacing.spacing2)}; &:hover:not(:disabled):not(:focus) { border: ${(props) => !props.$isDisabled && !props.$hasError && theme.border.focus}; diff --git a/src/component-library/Label/Label.style.tsx b/src/component-library/Label/Label.style.tsx index d3209b0dee..242de77506 100644 --- a/src/component-library/Label/Label.style.tsx +++ b/src/component-library/Label/Label.style.tsx @@ -6,7 +6,7 @@ const StyledLabel = styled.label` font-weight: ${theme.fontWeight.medium}; line-height: ${theme.lineHeight.lg}; font-size: ${theme.text.xs}; - color: ${theme.colors.textTertiary}; + color: ${theme.label.text}; padding: ${theme.spacing.spacing1} 0; align-self: flex-start; `; diff --git a/src/component-library/List/List.style.tsx b/src/component-library/List/List.style.tsx index 1e6d436bfd..d252659b61 100644 --- a/src/component-library/List/List.style.tsx +++ b/src/component-library/List/List.style.tsx @@ -2,7 +2,7 @@ import styled, { css } from 'styled-components'; import { Flex } from '../Flex'; import { theme } from '../theme'; -import { ListVariants, Variants } from '../utils/prop-types'; +import { ListVariants } from '../utils/prop-types'; type StyledListProps = { $variant: ListVariants; @@ -16,7 +16,7 @@ const StyledList = styled(Flex)` `; type StyledListItemProps = { - $variant: Variants | 'card'; + $variant: ListVariants; $isDisabled: boolean; $isHovered: boolean; $isInteractable: boolean; @@ -34,6 +34,7 @@ const StyledListItem = styled.li` color: ${theme.colors.textPrimary}; cursor: ${({ $isInteractable }) => $isInteractable && 'pointer'}; outline: ${({ $isFocusVisible }) => !$isFocusVisible && 'none'}; + opacity: ${({ $isDisabled }) => $isDisabled && 0.5}; ${({ $variant }) => { if ($variant === 'card') { diff --git a/src/component-library/Meter/Meter.style.tsx b/src/component-library/Meter/Meter.style.tsx index 1d23a075a5..72c5c5ad83 100644 --- a/src/component-library/Meter/Meter.style.tsx +++ b/src/component-library/Meter/Meter.style.tsx @@ -3,7 +3,7 @@ import styled, { css } from 'styled-components'; import { Flex } from '../Flex'; import { Span } from '../Text'; import { theme } from '../theme'; -import { Status, Variants } from '../utils/prop-types'; +import { MeterVariants, Status, Variants } from '../utils/prop-types'; type StyledWrapperProps = { $variant: Variants; @@ -11,7 +11,7 @@ type StyledWrapperProps = { type StyledMeterProps = { $position: number; - $variant: Variants; + $variant: MeterVariants; $hasRanges: boolean; }; diff --git a/src/component-library/Meter/Meter.tsx b/src/component-library/Meter/Meter.tsx index a907c9ca26..9e331b88fd 100644 --- a/src/component-library/Meter/Meter.tsx +++ b/src/component-library/Meter/Meter.tsx @@ -1,7 +1,7 @@ import { HTMLAttributes, useEffect, useState } from 'react'; import { Span } from '../Text'; -import { Status, Variants } from '../utils/prop-types'; +import { MeterVariants, Status } from '../utils/prop-types'; import { Indicator } from './Indicator'; import { StyledContainer, StyledIndicatorWrapper, StyledMeter, StyledWrapper } from './Meter.style'; import { RangeIndicators } from './RangeIndicators'; @@ -12,7 +12,7 @@ const getPosition = (percentage: number) => (percentage > 100 ? 100 : percentage type MeterRanges = [number, number, number, number]; type Props = { - variant?: Variants; + variant?: MeterVariants; value?: number; ranges?: MeterRanges; showIndicator?: boolean; diff --git a/src/component-library/Select/Select.stories.tsx b/src/component-library/Select/Select.stories.tsx index f233b502d4..a596980646 100644 --- a/src/component-library/Select/Select.stories.tsx +++ b/src/component-library/Select/Select.stories.tsx @@ -6,7 +6,7 @@ import { Flex } from '../Flex'; import { Select, SelectProps } from './Select'; import { SelectTrigger, SelectTriggerProps } from './SelectTrigger'; -const SelectTemplate: Story> = (args) => { +const SelectTemplate: Story> = (args) => { return ( @@ -65,4 +46,4 @@ const TokenSelect = ({ label: labelProp, 'aria-label': ariaLabelProp, ...props } }; export { TokenSelect }; -export type { TokenSelectProps }; +export type { TokenData, TokenSelectProps }; diff --git a/src/component-library/TokenInput/index.tsx b/src/component-library/TokenInput/index.tsx index 3454a8938d..e1687df30e 100644 --- a/src/component-library/TokenInput/index.tsx +++ b/src/component-library/TokenInput/index.tsx @@ -1,3 +1,5 @@ export type { TokenInputProps } from './TokenInput'; export { TokenInput } from './TokenInput'; -export type { TokenSelectProps } from './TokenSelect'; +export type { TokenListItemProps } from './TokenListItem'; +export { TokenListItem } from './TokenListItem'; +export type { TokenData, TokenSelectProps } from './TokenSelect'; diff --git a/src/component-library/Tooltip/Tooltip.tsx b/src/component-library/Tooltip/Tooltip.tsx index 7e83274bb8..822f0f5022 100644 --- a/src/component-library/Tooltip/Tooltip.tsx +++ b/src/component-library/Tooltip/Tooltip.tsx @@ -12,7 +12,7 @@ import { StyledTooltip, StyledTooltipLabel, StyledTooltipTip } from './Tooltip.s // MEMO: https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/tooltip/src/TooltipTrigger.tsx#L22 const DEFAULT_OFFSET = -1; const DEFAULT_CROSS_OFFSET = 0; -const DEFAULT_DELAY = 500; +const DEFAULT_DELAY = 250; type Props = { label?: ReactNode; diff --git a/src/component-library/index.tsx b/src/component-library/index.tsx index 2a09fe612f..f71642c245 100644 --- a/src/component-library/index.tsx +++ b/src/component-library/index.tsx @@ -59,8 +59,8 @@ export type { TextLinkProps } from './TextLink'; export { TextLink } from './TextLink'; export type { ComponentLibraryTheme } from './theme'; export { theme } from './theme'; -export type { TokenInputProps, TokenSelectProps } from './TokenInput'; -export { TokenInput } from './TokenInput'; +export type { TokenData, TokenInputProps, TokenListItemProps, TokenSelectProps } from './TokenInput'; +export { TokenInput, TokenListItem } from './TokenInput'; export type { TokenStackProps } from './TokenStack'; export { TokenStack } from './TokenStack'; export type { TooltipProps } from './Tooltip'; diff --git a/src/component-library/theme/theme.interlay.css b/src/component-library/theme/theme.interlay.css index 05ca316875..10c70a1bfa 100644 --- a/src/component-library/theme/theme.interlay.css +++ b/src/component-library/theme/theme.interlay.css @@ -10,11 +10,12 @@ } .theme-interlay { - --colors-border: var(--colors-neutral-black-25); + --colors-border: #dee3f5; --colors-bg-primary: var(--colors-neutral-white); /* Card */ --color-card-primary-bg: var(--colors-neutral-white); --color-card-secondary-bg: var(--colors-light-blue-10); + --color-card-tertiary-bg: #fbfbfc; /* CTA */ --colors-cta-primary: var(--colors-light-blue); --colors-cta-primary-hover: var(--colors-light-blue-90); @@ -34,8 +35,8 @@ /* Tabs */ --colors-tabs-bg: var(--colors-neutral-white); --colors-tabs-text: var(--colors-neutral-black-9); - --colors-tabs-active-color: var(--colors-light-blue); - --colors-tabs-active-bg: var(--colors-lighter-blue); + --colors-tabs-active-color: var(--colors-neutral-white); + --colors-tabs-active-bg: var(--colors-light-blue); /* Loading Spinner */ --colors-indeterminate-primary-color: var(--colors-lighter-blue); --colors-indeterminate-primary-bg: var(--colors-neutral-black-20); @@ -54,14 +55,17 @@ --color-progress-circle-fill: var(--colors-light-blue); /* Input */ --colors-input-text: var(--colors-neutral-black); - --colors-input-default-border: var(--colors-neutral-light-grey); + --colors-input-default-border: #dee3f5; --colors-input-hover-border: var(--colors-lighter-blue); --colors-input-focus-border: var(--colors-light-blue-90); --colors-input-disabled-text: #9a9a9a; --colors-input-disabled-border: var(--colors-neutral-lighter-grey); --colors-input-background: var(--colors-neutral-white); + --colors-input-disabled-bg: #fbfbfc; + /* Label */ + --colors-label-text: var(--colors-neutral-black); /* Token Input */ - --colors-token-input-end-adornment-bg: var(--colors-neutral-lighter-grey); + --colors-token-input-end-adornment-bg: var(--colors-neutral-white); /* Token List */ --colors-token-list-item-text: var(--colors-neutral-black); --colors-token-list-item-select-text: var(--colors-neutral-white); diff --git a/src/component-library/theme/theme.kintsugi.css b/src/component-library/theme/theme.kintsugi.css index 5b3df686b3..3ab36b5e61 100644 --- a/src/component-library/theme/theme.kintsugi.css +++ b/src/component-library/theme/theme.kintsugi.css @@ -18,6 +18,8 @@ /* Card */ --color-card-primary-bg: var(--colors-dark-blue); --color-card-secondary-bg: var(--colors-neutral-white-06); + /* TODO: add */ + --color-card-tertiary-bg: var(--colors-neutral-white-06); /* CTA */ --colors-cta-primary: var(--colors-yellow); --colors-cta-primary-hover: var(--colors-dark-yellow); @@ -63,6 +65,9 @@ --colors-input-disabled-text: var(--colors-slate-gray); --colors-input-disabled-border: var(--colors-mid-blue); --colors-input-background: var(--colors-blue); + --colors-input-disabled-bg: var(--colors-blue); + /* Label */ + --colors-label-text: var(--colors-neutral-white); /* Token Input */ --colors-token-input-end-adornment-bg: var(--colors-neutral-white-25); /* Token List */ diff --git a/src/component-library/theme/theme.ts b/src/component-library/theme/theme.ts index 45f079bb66..22f6d5f2cf 100644 --- a/src/component-library/theme/theme.ts +++ b/src/component-library/theme/theme.ts @@ -110,6 +110,7 @@ const theme = { }, disabled: { color: 'var(--colors-input-disabled-text)', + bg: 'var(--colors-input-disabled-bg)', border: '1px solid var(--colors-input-disabled-border)' }, helperText: { @@ -119,15 +120,18 @@ const theme = { }, small: { text: 'var(--text-s)', - maxHeight: 'var(--spacing-8)' + maxHeight: 'var(--spacing-8)', + weight: 'var(--font-weights-book)' }, medium: { text: 'var(--text-base)', - maxHeight: 'var(--spacing-10)' + maxHeight: 'var(--spacing-10)', + weight: 'var(--font-weights-book)' }, large: { text: 'var(--text-4xl)', - maxHeight: 'var(--spacing-16)' + maxHeight: 'var(--spacing-16)', + weight: 'var(--font-weights-medium)' }, overflow: { large: { @@ -142,6 +146,9 @@ const theme = { xl2: '9.5rem' } }, + label: { + text: 'var(--colors-label-text)' + }, tokenInput: { endAdornment: { bg: 'var(--colors-token-input-end-adornment-bg)' @@ -161,7 +168,11 @@ const theme = { outlined: { border: '1px solid transparent' }, - bg: { primary: 'var(--color-card-primary-bg)', secondary: 'var(--color-card-secondary-bg)' } + bg: { + primary: 'var(--color-card-primary-bg)', + secondary: 'var(--color-card-secondary-bg)', + tertiary: 'var(--color-card-tertiary-bg)' + } }, cta: { primary: { @@ -566,8 +577,8 @@ const theme = { color: 'var(--color-select-text)', size: { small: { - padding: 'var(--spacing-1)', - text: 'var(--text-s)', + padding: 'var(--spacing-1) var(--spacing-2)', + text: 'var(--text-xs)', // TODO: to be determined maxHeight: 'calc(var(--spacing-6) - 1px)' }, diff --git a/src/component-library/utils/prop-types.ts b/src/component-library/utils/prop-types.ts index 2b5d4b629d..cdeb314c12 100644 --- a/src/component-library/utils/prop-types.ts +++ b/src/component-library/utils/prop-types.ts @@ -4,9 +4,9 @@ import { theme } from '../theme'; export const tuple = (...args: T): T => args; -export const variant = tuple('primary', 'secondary'); +export const variant = tuple('primary', 'secondary', 'tertiary'); -export const ctaVariant = tuple(...variant, 'outlined', 'text'); +export const ctaVariant = tuple('primary', 'secondary', 'outlined', 'text'); export const status = tuple('error', 'warning', 'success'); @@ -51,7 +51,9 @@ export type CTAVariants = typeof ctaVariant[number]; export type CardVariants = 'default' | 'bordered'; -export type ListVariants = Variants | 'card'; +export type ListVariants = Exclude | 'card'; + +export type MeterVariants = Exclude; export type DividerVariants = Colors | 'default'; @@ -108,3 +110,5 @@ export type Overflow = 'auto' | 'hidden' | 'scroll' | 'visible' | 'inherit'; export type BreakPoints = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; export type ProgressBarColors = 'default' | 'red'; + +export type BorderRadius = keyof typeof theme.rounded; diff --git a/src/components/PlusDivider/PlusDivider.styles.tsx b/src/components/PlusDivider/PlusDivider.styles.tsx new file mode 100644 index 0000000000..5fb117fae3 --- /dev/null +++ b/src/components/PlusDivider/PlusDivider.styles.tsx @@ -0,0 +1,30 @@ +import styled from 'styled-components'; + +import { Flex, theme } from '@/component-library'; + +const StyledWrapper = styled(Flex)` + position: relative; + height: ${theme.spacing.spacing10}; +`; + +const StyledCircle = styled.div` + display: inline-flex; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + padding: ${theme.spacing.spacing2}; + background-color: var(--colors-border); + border-radius: ${theme.rounded.full}; +`; + +const StyledBackground = styled.div` + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + padding: ${theme.spacing.spacing1} ${theme.spacing.spacing8}; + background-color: ${theme.colors.bgPrimary}; +`; + +export { StyledBackground, StyledCircle, StyledWrapper }; diff --git a/src/components/PlusDivider/PlusDivider.tsx b/src/components/PlusDivider/PlusDivider.tsx new file mode 100644 index 0000000000..37fa6bfbf8 --- /dev/null +++ b/src/components/PlusDivider/PlusDivider.tsx @@ -0,0 +1,19 @@ +import { PlusCircle } from '@/assets/icons'; +import { Divider, FlexProps } from '@/component-library'; + +import { StyledBackground, StyledCircle, StyledWrapper } from './PlusDivider.styles'; + +type PlusDividerProps = FlexProps; + +const PlusDivider = (props: PlusDividerProps): JSX.Element => ( + + + + + + + +); + +export { PlusDivider }; +export type { PlusDividerProps }; diff --git a/src/components/PlusDivider/index.tsx b/src/components/PlusDivider/index.tsx new file mode 100644 index 0000000000..234145d40b --- /dev/null +++ b/src/components/PlusDivider/index.tsx @@ -0,0 +1,2 @@ +export type { PlusDividerProps } from './PlusDivider'; +export { PlusDivider } from './PlusDivider'; diff --git a/src/components/TransactionDetails/TransactionDetails.style.tsx b/src/components/TransactionDetails/TransactionDetails.style.tsx new file mode 100644 index 0000000000..d93596d198 --- /dev/null +++ b/src/components/TransactionDetails/TransactionDetails.style.tsx @@ -0,0 +1,41 @@ +import styled from 'styled-components'; + +import { InformationCircle } from '@/assets/icons'; +import { Dl, DlGroup, Select, SelectProps, theme, TokenData } from '@/component-library'; + +const StyledDl = styled(Dl)` + border: ${theme.border.default}; + background-color: ${theme.card.bg.tertiary}; + padding: ${theme.spacing.spacing4}; + font-size: ${theme.text.xs}; + border-radius: ${theme.rounded.lg}; +`; + +const StyledInformationCircle = styled(InformationCircle)` + margin-left: ${theme.spacing.spacing2}; + vertical-align: text-top; +`; + +const SelectWrapper = ({ ...props }: SelectProps<'modal', TokenData>) => {...props} />; + +const StyledSelect = styled(SelectWrapper)` + flex-direction: row; + justify-content: space-between; +`; + +// This custom padding helps to keep harmony between normal elements and elements with small select +const StyledDlGroup = styled(DlGroup)` + &:first-of-type { + padding-bottom: 0.407rem; + } + + &:not(:first-of-type):not(:last-of-type) { + padding: 0.407rem 0; + } + + &:last-of-type { + padding-top: 0.407rem; + } +`; + +export { StyledDl, StyledDlGroup, StyledInformationCircle, StyledSelect }; diff --git a/src/components/TransactionDetails/TransactionDetails.tsx b/src/components/TransactionDetails/TransactionDetails.tsx new file mode 100644 index 0000000000..8ef48e9a0f --- /dev/null +++ b/src/components/TransactionDetails/TransactionDetails.tsx @@ -0,0 +1,14 @@ +import { DlProps } from '@/component-library'; + +import { StyledDl } from './TransactionDetails.style'; + +type TransactionDetailsProps = DlProps; + +const TransactionDetails = ({ children, direction = 'column', ...props }: TransactionDetailsProps): JSX.Element => ( + + {children} + +); + +export { TransactionDetails }; +export type { TransactionDetailsProps }; diff --git a/src/components/TransactionDetails/TransactionDetailsDd.tsx b/src/components/TransactionDetails/TransactionDetailsDd.tsx new file mode 100644 index 0000000000..9b0efe1280 --- /dev/null +++ b/src/components/TransactionDetails/TransactionDetailsDd.tsx @@ -0,0 +1,10 @@ +import { Dd, DdProps } from '@/component-library'; + +type TransactionDetailsDdProps = DdProps; + +const TransactionDetailsDd = ({ color = 'primary', size = 'xs', ...props }: TransactionDetailsDdProps): JSX.Element => ( +
+); + +export { TransactionDetailsDd }; +export type { TransactionDetailsDdProps }; diff --git a/src/components/TransactionDetails/TransactionDetailsDt.tsx b/src/components/TransactionDetails/TransactionDetailsDt.tsx new file mode 100644 index 0000000000..bf9f0bda48 --- /dev/null +++ b/src/components/TransactionDetails/TransactionDetailsDt.tsx @@ -0,0 +1,33 @@ +import { ReactNode } from 'react'; + +import { Dt, DtProps, Tooltip } from '@/component-library'; + +import { StyledInformationCircle } from './TransactionDetails.style'; + +type Props = { + tooltipLabel?: ReactNode; +}; + +type InheritAttrs = Omit; + +type TransactionDetailsDtProps = Props & InheritAttrs; + +const TransactionDetailsDt = ({ + color = 'primary', + size = 'xs', + tooltipLabel, + children, + ...props +}: TransactionDetailsDtProps): JSX.Element => ( +
+ {children} + {tooltipLabel && ( + + + + )} +
+); + +export { TransactionDetailsDt }; +export type { TransactionDetailsDtProps }; diff --git a/src/components/TransactionDetails/TransactionDetailsGroup.tsx b/src/components/TransactionDetails/TransactionDetailsGroup.tsx new file mode 100644 index 0000000000..764f582dd9 --- /dev/null +++ b/src/components/TransactionDetails/TransactionDetailsGroup.tsx @@ -0,0 +1,16 @@ +import { DlGroupProps } from '@/component-library'; + +import { StyledDlGroup } from './TransactionDetails.style'; + +type TransactionDetailsGroupProps = DlGroupProps; + +const TransactionDetailsGroup = ({ + flex = '1', + justifyContent = 'space-between', + ...props +}: TransactionDetailsGroupProps): JSX.Element => ( + +); + +export { TransactionDetailsGroup }; +export type { TransactionDetailsGroupProps }; diff --git a/src/components/TransactionDetails/TransactionSelectToken.tsx b/src/components/TransactionDetails/TransactionSelectToken.tsx new file mode 100644 index 0000000000..fe773eaa8a --- /dev/null +++ b/src/components/TransactionDetails/TransactionSelectToken.tsx @@ -0,0 +1,61 @@ +import { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Item, SelectProps, Span, TokenData, TokenListItem, Tooltip } from '@/component-library'; + +import { StyledInformationCircle, StyledSelect } from './TransactionDetails.style'; + +type Props = { + label?: ReactNode; + tooltipLabel?: ReactNode; +}; + +type InheritAttrs = Omit, keyof Props | 'children'>; + +type TransactionSelectTokenProps = Props & InheritAttrs; + +const TransactionSelectToken = ({ + label: labelProp, + tooltipLabel, + items, + ...props +}: TransactionSelectTokenProps): JSX.Element => { + const { t } = useTranslation(); + + const label = tooltipLabel ? ( + + {labelProp} + {tooltipLabel && ( + + + + )} + + ) : ( + labelProp + ); + + const disabledKeys = (items as TokenData[])?.filter((item) => Number(item.balance) === 0).map((item) => item.value); + + return ( + item.value.value} + disabledKeys={disabledKeys} + label={label} + items={items} + {...props} + > + {(data: TokenData) => ( + + + + )} + + ); +}; + +export { TransactionSelectToken }; +export type { TransactionSelectTokenProps }; diff --git a/src/components/TransactionDetails/index.tsx b/src/components/TransactionDetails/index.tsx new file mode 100644 index 0000000000..b81a57655b --- /dev/null +++ b/src/components/TransactionDetails/index.tsx @@ -0,0 +1,10 @@ +export type { TransactionDetailsProps } from './TransactionDetails'; +export { TransactionDetails } from './TransactionDetails'; +export type { TransactionDetailsDdProps } from './TransactionDetailsDd'; +export { TransactionDetailsDd } from './TransactionDetailsDd'; +export type { TransactionDetailsDtProps } from './TransactionDetailsDt'; +export { TransactionDetailsDt } from './TransactionDetailsDt'; +export type { TransactionDetailsGroupProps } from './TransactionDetailsGroup'; +export { TransactionDetailsGroup } from './TransactionDetailsGroup'; +export type { TransactionSelectTokenProps } from './TransactionSelectToken'; +export { TransactionSelectToken } from './TransactionSelectToken'; diff --git a/src/components/TransactionFeeDetails/TransactionFeeDetails.tsx b/src/components/TransactionFeeDetails/TransactionFeeDetails.tsx new file mode 100644 index 0000000000..d31aa12762 --- /dev/null +++ b/src/components/TransactionFeeDetails/TransactionFeeDetails.tsx @@ -0,0 +1,94 @@ +import { CurrencyExt } from '@interlay/interbtc-api'; +import { MonetaryAmount } from '@interlay/monetary-js'; +import { mergeProps, useId } from '@react-aria/utils'; +import { Key, ReactNode, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { displayMonetaryAmountInUSDFormat, formatUSD } from '@/common/utils/utils'; +import { Alert, Flex } from '@/component-library'; +import { getTokenPrice } from '@/utils/helpers/prices'; +import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { SelectCurrencyFilter, useSelectCurrency } from '@/utils/hooks/use-select-currency'; + +import { + TransactionDetails, + TransactionDetailsDd, + TransactionDetailsDt, + TransactionDetailsGroup, + TransactionDetailsProps, + TransactionSelectToken, + TransactionSelectTokenProps +} from '../TransactionDetails'; + +type Props = { + defaultCurrency?: CurrencyExt; + amount?: MonetaryAmount; + label?: ReactNode; + showInsufficientBalance?: boolean; + tooltipLabel?: ReactNode; + selectProps?: TransactionSelectTokenProps; +}; + +type InheritAttrs = Omit; + +type TransactionFeeDetailsProps = Props & InheritAttrs; + +const TransactionFeeDetails = ({ + amount, + defaultCurrency, + showInsufficientBalance, + selectProps, + label, + tooltipLabel, + className, + ...props +}: TransactionFeeDetailsProps): JSX.Element => { + const prices = useGetPrices(); + const { t } = useTranslation(); + const selectCurrency = useSelectCurrency(SelectCurrencyFilter.TRADEABLE_FOR_NATIVE_CURRENCY); + const id = useId(); + const [ticker, setTicker] = useState(defaultCurrency?.ticker); + + const amountLabel = amount + ? `${amount.toHuman()} ${amount.currency.ticker} (${displayMonetaryAmountInUSDFormat( + amount, + getTokenPrice(prices, amount.currency.ticker)?.usd + )})` + : `${0.0} ${ticker} (${formatUSD(0)})`; + + const errorMessage = showInsufficientBalance && t('forms.ensure_adequate_amount_left_to_cover_fees'); + + const handleSelectionChange = (key: Key) => setTicker(key as string); + + return ( + + + {selectProps && ( + + )} + + {label || t('tx_fees')} + {amountLabel} + + + {errorMessage && ( + + {errorMessage} + + )} + + ); +}; + +export { TransactionFeeDetails }; +export type { TransactionFeeDetailsProps }; diff --git a/src/components/TransactionFeeDetails/index.tsx b/src/components/TransactionFeeDetails/index.tsx new file mode 100644 index 0000000000..84405ffc89 --- /dev/null +++ b/src/components/TransactionFeeDetails/index.tsx @@ -0,0 +1,2 @@ +export type { TransactionFeeDetailsProps } from './TransactionFeeDetails'; +export { TransactionFeeDetails } from './TransactionFeeDetails'; diff --git a/src/components/index.tsx b/src/components/index.tsx index bb20578fa9..fbd8dc51df 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -12,10 +12,15 @@ export type { LoanPositionsTableProps } from './LoanPositionsTable'; export { LoanPositionsTable } from './LoanPositionsTable'; export type { NotificationsPopoverProps } from './NotificationsPopover'; export { NotificationsPopover } from './NotificationsPopover'; +export type { PlusDividerProps } from './PlusDivider'; +export { PlusDivider } from './PlusDivider'; export type { PoolsTableProps } from './PoolsTable'; export { PoolsTable } from './PoolsTable'; export { ReceivableAssets } from './ReceivableAssets'; export type { ToastContainerProps } from './ToastContainer'; export { ToastContainer } from './ToastContainer'; +export * from './TransactionDetails'; +export type { TransactionFeeDetailsProps } from './TransactionFeeDetails'; +export { TransactionFeeDetails } from './TransactionFeeDetails'; export type { TransactionToastProps } from './TransactionToast'; export { TransactionToast } from './TransactionToast'; diff --git a/src/legacy-components/IssueUI/IssueRequestStatusUI/CancelledIssueRequest/index.tsx b/src/legacy-components/IssueUI/IssueRequestStatusUI/CancelledIssueRequest/index.tsx index 80c887bdc6..eb8629f691 100644 --- a/src/legacy-components/IssueUI/IssueRequestStatusUI/CancelledIssueRequest/index.tsx +++ b/src/legacy-components/IssueUI/IssueRequestStatusUI/CancelledIssueRequest/index.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { FaExclamationCircle, FaTimesCircle } from 'react-icons/fa'; import { WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains'; -import RequestWrapper from '@/pages/Bridge/RequestWrapper'; +import RequestWrapper from '@/legacy-components/RequestWrapper'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import { getColorShade } from '@/utils/helpers/colors'; diff --git a/src/legacy-components/IssueUI/IssueRequestStatusUI/CompletedIssueRequest/index.tsx b/src/legacy-components/IssueUI/IssueRequestStatusUI/CompletedIssueRequest/index.tsx index 5ca296666e..69f4df6f0a 100644 --- a/src/legacy-components/IssueUI/IssueRequestStatusUI/CompletedIssueRequest/index.tsx +++ b/src/legacy-components/IssueUI/IssueRequestStatusUI/CompletedIssueRequest/index.tsx @@ -7,8 +7,8 @@ import { WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains'; import AddressWithCopyUI from '@/legacy-components/AddressWithCopyUI'; import ExternalLink from '@/legacy-components/ExternalLink'; import PrimaryColorSpan from '@/legacy-components/PrimaryColorSpan'; +import RequestWrapper from '@/legacy-components/RequestWrapper'; import Ring48, { Ring48Title, Ring48Value } from '@/legacy-components/Ring48'; -import RequestWrapper from '@/pages/Bridge/RequestWrapper'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import { getColorShade } from '@/utils/helpers/colors'; diff --git a/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx b/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx index 3472455342..8373dbff9b 100644 --- a/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx +++ b/src/legacy-components/IssueUI/IssueRequestStatusUI/ConfirmedIssueRequest/index.tsx @@ -6,7 +6,7 @@ import { BTC_EXPLORER_TRANSACTION_API } from '@/config/blockstream-explorer-link import { WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains'; import AddressWithCopyUI from '@/legacy-components/AddressWithCopyUI'; import ExternalLink from '@/legacy-components/ExternalLink'; -import RequestWrapper from '@/pages/Bridge/RequestWrapper'; +import RequestWrapper from '@/legacy-components/RequestWrapper'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import { getColorShade } from '@/utils/helpers/colors'; diff --git a/src/legacy-components/IssueUI/IssueRequestStatusUI/ReceivedIssueRequest/index.tsx b/src/legacy-components/IssueUI/IssueRequestStatusUI/ReceivedIssueRequest/index.tsx index c824c7e26f..e6fba19589 100644 --- a/src/legacy-components/IssueUI/IssueRequestStatusUI/ReceivedIssueRequest/index.tsx +++ b/src/legacy-components/IssueUI/IssueRequestStatusUI/ReceivedIssueRequest/index.tsx @@ -7,8 +7,8 @@ import { BTC_EXPLORER_TRANSACTION_API } from '@/config/blockstream-explorer-link import AddressWithCopyUI from '@/legacy-components/AddressWithCopyUI'; import ErrorFallback from '@/legacy-components/ErrorFallback'; import ExternalLink from '@/legacy-components/ExternalLink'; +import RequestWrapper from '@/legacy-components/RequestWrapper'; import Ring48, { Ring48Title, Ring48Value } from '@/legacy-components/Ring48'; -import RequestWrapper from '@/pages/Bridge/RequestWrapper'; import useCurrentActiveBlockNumber from '@/services/hooks/use-current-active-block-number'; import useStableBitcoinConfirmations from '@/services/hooks/use-stable-bitcoin-confirmations'; import useStableParachainConfirmations from '@/services/hooks/use-stable-parachain-confirmations'; diff --git a/src/legacy-components/IssueUI/WhoopsStatusUI/index.tsx b/src/legacy-components/IssueUI/WhoopsStatusUI/index.tsx index 922f9b5e30..f1e35721b8 100644 --- a/src/legacy-components/IssueUI/WhoopsStatusUI/index.tsx +++ b/src/legacy-components/IssueUI/WhoopsStatusUI/index.tsx @@ -9,7 +9,7 @@ import { WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains'; import AddressWithCopyUI from '@/legacy-components/AddressWithCopyUI'; import Hr2 from '@/legacy-components/hrs/Hr2'; import PriceInfo from '@/legacy-components/PriceInfo'; -import RequestWrapper from '@/pages/Bridge/RequestWrapper'; +import RequestWrapper from '@/legacy-components/RequestWrapper'; import { ForeignAssetIdLiteral } from '@/types/currency'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import { getColorShade } from '@/utils/helpers/colors'; diff --git a/src/legacy-components/IssueUI/index.tsx b/src/legacy-components/IssueUI/index.tsx index 4edd9b6878..640a417a15 100644 --- a/src/legacy-components/IssueUI/index.tsx +++ b/src/legacy-components/IssueUI/index.tsx @@ -169,6 +169,17 @@ const IssueUI = ({ issue }: Props): JSX.Element => { +
+ + {t('issue_page.request')} + + +

{t('note')}: diff --git a/src/legacy-components/RedeemUI/RedeemRequestStatusUI/CompletedRedeemRequest/index.tsx b/src/legacy-components/RedeemUI/RedeemRequestStatusUI/CompletedRedeemRequest/index.tsx index d7efad9ffc..fb0a7f7513 100644 --- a/src/legacy-components/RedeemUI/RedeemRequestStatusUI/CompletedRedeemRequest/index.tsx +++ b/src/legacy-components/RedeemUI/RedeemRequestStatusUI/CompletedRedeemRequest/index.tsx @@ -6,8 +6,8 @@ import { BTC_EXPLORER_TRANSACTION_API } from '@/config/blockstream-explorer-link import AddressWithCopyUI from '@/legacy-components/AddressWithCopyUI'; import ExternalLink from '@/legacy-components/ExternalLink'; import PrimaryColorSpan from '@/legacy-components/PrimaryColorSpan'; +import RequestWrapper from '@/legacy-components/RequestWrapper'; import Ring48, { Ring48Title, Ring48Value } from '@/legacy-components/Ring48'; -import RequestWrapper from '@/pages/Bridge/RequestWrapper'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import { getColorShade } from '@/utils/helpers/colors'; diff --git a/src/legacy-components/RedeemUI/RedeemRequestStatusUI/DefaultRedeemRequest/index.tsx b/src/legacy-components/RedeemUI/RedeemRequestStatusUI/DefaultRedeemRequest/index.tsx index a95aa49ae7..8b2ec9c856 100644 --- a/src/legacy-components/RedeemUI/RedeemRequestStatusUI/DefaultRedeemRequest/index.tsx +++ b/src/legacy-components/RedeemUI/RedeemRequestStatusUI/DefaultRedeemRequest/index.tsx @@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next'; import { formatNumber } from '@/common/utils/utils'; import AddressWithCopyUI from '@/legacy-components/AddressWithCopyUI'; +import RequestWrapper from '@/legacy-components/RequestWrapper'; import Ring48, { Ring48Title, Ring48Value } from '@/legacy-components/Ring48'; -import RequestWrapper from '@/pages/Bridge/RequestWrapper'; import useCurrentActiveBlockNumber from '@/services/hooks/use-current-active-block-number'; import useStableBitcoinConfirmations from '@/services/hooks/use-stable-bitcoin-confirmations'; import useStableParachainConfirmations from '@/services/hooks/use-stable-parachain-confirmations'; diff --git a/src/legacy-components/RedeemUI/RedeemRequestStatusUI/PendingWithBtcTxNotFoundRedeemRequest/index.tsx b/src/legacy-components/RedeemUI/RedeemRequestStatusUI/PendingWithBtcTxNotFoundRedeemRequest/index.tsx index 7eeac859db..4c0e1898ee 100644 --- a/src/legacy-components/RedeemUI/RedeemRequestStatusUI/PendingWithBtcTxNotFoundRedeemRequest/index.tsx +++ b/src/legacy-components/RedeemUI/RedeemRequestStatusUI/PendingWithBtcTxNotFoundRedeemRequest/index.tsx @@ -5,9 +5,9 @@ import { useSelector } from 'react-redux'; import { StoreType } from '@/common/types/util.types'; import { BLOCK_TIME } from '@/config/parachain'; +import RequestWrapper from '@/legacy-components/RequestWrapper'; import Ring48, { Ring48Title, Ring48Value } from '@/legacy-components/Ring48'; import Timer from '@/legacy-components/Timer'; -import RequestWrapper from '@/pages/Bridge/RequestWrapper'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; interface Props { diff --git a/src/legacy-components/RedeemUI/RedeemRequestStatusUI/ReimbursedRedeemRequest/index.tsx b/src/legacy-components/RedeemUI/RedeemRequestStatusUI/ReimbursedRedeemRequest/index.tsx index ccc50e5649..7d821b1deb 100644 --- a/src/legacy-components/RedeemUI/RedeemRequestStatusUI/ReimbursedRedeemRequest/index.tsx +++ b/src/legacy-components/RedeemUI/RedeemRequestStatusUI/ReimbursedRedeemRequest/index.tsx @@ -20,7 +20,7 @@ import ExternalLink from '@/legacy-components/ExternalLink'; import Hr2 from '@/legacy-components/hrs/Hr2'; import PriceInfo from '@/legacy-components/PriceInfo'; import PrimaryColorSpan from '@/legacy-components/PrimaryColorSpan'; -import RequestWrapper from '@/pages/Bridge/RequestWrapper'; +import RequestWrapper from '@/legacy-components/RequestWrapper'; import { ForeignAssetIdLiteral } from '@/types/currency'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import { getColorShade } from '@/utils/helpers/colors'; diff --git a/src/legacy-components/RedeemUI/RedeemRequestStatusUI/RetriedRedeemRequest/index.tsx b/src/legacy-components/RedeemUI/RedeemRequestStatusUI/RetriedRedeemRequest/index.tsx index 747b97f1c9..86f0f30879 100644 --- a/src/legacy-components/RedeemUI/RedeemRequestStatusUI/RetriedRedeemRequest/index.tsx +++ b/src/legacy-components/RedeemUI/RedeemRequestStatusUI/RetriedRedeemRequest/index.tsx @@ -19,7 +19,7 @@ import ExternalLink from '@/legacy-components/ExternalLink'; import Hr2 from '@/legacy-components/hrs/Hr2'; import PriceInfo from '@/legacy-components/PriceInfo'; import PrimaryColorSpan from '@/legacy-components/PrimaryColorSpan'; -import RequestWrapper from '@/pages/Bridge/RequestWrapper'; +import RequestWrapper from '@/legacy-components/RequestWrapper'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import { getColorShade } from '@/utils/helpers/colors'; import { getExchangeRate } from '@/utils/helpers/oracle'; diff --git a/src/legacy-components/RedeemUI/ReimburseStatusUI/index.tsx b/src/legacy-components/RedeemUI/ReimburseStatusUI/index.tsx index 541f4ef875..00d9d4bb24 100644 --- a/src/legacy-components/RedeemUI/ReimburseStatusUI/index.tsx +++ b/src/legacy-components/RedeemUI/ReimburseStatusUI/index.tsx @@ -15,8 +15,8 @@ import InterlayConiferOutlinedButton from '@/legacy-components/buttons/InterlayC import InterlayDenimOrKintsugiMidnightOutlinedButton from '@/legacy-components/buttons/InterlayDenimOrKintsugiMidnightOutlinedButton'; import ErrorFallback from '@/legacy-components/ErrorFallback'; import PrimaryColorSpan from '@/legacy-components/PrimaryColorSpan'; +import RequestWrapper from '@/legacy-components/RequestWrapper'; import { useSubstrateSecureState } from '@/lib/substrate'; -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'; diff --git a/src/legacy-components/RedeemUI/index.tsx b/src/legacy-components/RedeemUI/index.tsx index 229f1f3619..6e7fda6f02 100644 --- a/src/legacy-components/RedeemUI/index.tsx +++ b/src/legacy-components/RedeemUI/index.tsx @@ -158,6 +158,17 @@ const RedeemUI = ({ redeem, onClose }: Props): JSX.Element => { +

+ + {t('issue_page.request')} + + +
<>{renderModalStatusPanel(redeem)} diff --git a/src/pages/Bridge/RequestWrapper/index.tsx b/src/legacy-components/RequestWrapper/index.tsx similarity index 100% rename from src/pages/Bridge/RequestWrapper/index.tsx rename to src/legacy-components/RequestWrapper/index.tsx diff --git a/src/legacy-components/buttons/InterlayDefaultContainedButton/index.tsx b/src/legacy-components/buttons/InterlayDefaultContainedButton/index.tsx index e381ee79b3..099a44a7b4 100644 --- a/src/legacy-components/buttons/InterlayDefaultContainedButton/index.tsx +++ b/src/legacy-components/buttons/InterlayDefaultContainedButton/index.tsx @@ -28,7 +28,8 @@ const InterlayDefaultContainedButton = React.forwardRef( 'focus:ring-opacity-50', 'border', - 'border-transparent', + 'border-black', + 'border-opacity-25', 'font-medium', disabledOrPending @@ -36,7 +37,7 @@ const InterlayDefaultContainedButton = React.forwardRef( : clsx( TEXT_CLASSES, 'dark:!text-black', // Suppressing white text color in this specific edge case - 'bg-interlayPaleSky-100', + 'bg-white', 'hover:bg-interlayPaleSky-200' ), diff --git a/src/lib/form/index.tsx b/src/lib/form/index.tsx index af76026d2e..60fbf3b63b 100644 --- a/src/lib/form/index.tsx +++ b/src/lib/form/index.tsx @@ -1,9 +1,3 @@ -export type { - CreateVaultFormData, - CrossChainTransferFormData, - DepositLiquidityPoolFormData, - LoanFormData -} from './schemas'; export * from './schemas'; export type { FormErrors } from './use-form'; export { useForm } from './use-form'; diff --git a/src/lib/form/schemas/amm.ts b/src/lib/form/schemas/amm.ts deleted file mode 100644 index def382e26f..0000000000 --- a/src/lib/form/schemas/amm.ts +++ /dev/null @@ -1,51 +0,0 @@ -import yup, { FeesValidationParams, MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; - -type DepositLiquidityPoolFormData = Record; - -type DepositLiquidityPoolValidationParams = FeesValidationParams & { - tokens: Record; -}; - -// MEMO: schema is dynamic because is deals with one to many fields -const depositLiquidityPoolSchema = (params: DepositLiquidityPoolValidationParams): yup.ObjectSchema => { - const shape = Object.keys(params.tokens).reduce((acc, ticker, idx) => { - const tokenParams = params.tokens[ticker]; - const validation = yup.string().requiredAmount('deposit').maxAmount(tokenParams).minAmount(tokenParams, 'deposit'); - - if (idx === 0) { - return { ...acc, [ticker]: validation.fees(params) }; - } - - return { ...acc, [ticker]: validation }; - }, {}); - - return yup.object().shape(shape); -}; - -const WITHDRAW_LIQUIDITY_POOL_FIELD = 'withdraw'; - -type WithdrawLiquidityPoolFormData = { - [WITHDRAW_LIQUIDITY_POOL_FIELD]?: number; -}; - -type WithdrawLiquidityPoolValidationParams = FeesValidationParams & - MaxAmountValidationParams & - MinAmountValidationParams; - -const withdrawLiquidityPoolSchema = (params: WithdrawLiquidityPoolValidationParams): yup.ObjectSchema => - yup.object().shape({ - [WITHDRAW_LIQUIDITY_POOL_FIELD]: yup - .string() - .requiredAmount(WITHDRAW_LIQUIDITY_POOL_FIELD) - .maxAmount(params) - .minAmount(params, WITHDRAW_LIQUIDITY_POOL_FIELD) - .fees(params) - }); - -export { depositLiquidityPoolSchema, WITHDRAW_LIQUIDITY_POOL_FIELD, withdrawLiquidityPoolSchema }; -export type { - DepositLiquidityPoolFormData, - DepositLiquidityPoolValidationParams, - WithdrawLiquidityPoolFormData, - WithdrawLiquidityPoolValidationParams -}; diff --git a/src/lib/form/schemas/bridge.ts b/src/lib/form/schemas/bridge.ts new file mode 100644 index 0000000000..f53a778e83 --- /dev/null +++ b/src/lib/form/schemas/bridge.ts @@ -0,0 +1,105 @@ +import i18n from 'i18next'; + +import yup, { AddressType, MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; + +const BRIDGE_ISSUE_AMOUNT_FIELD = 'issue-amount'; +const BRIDGE_ISSUE_CUSTOM_VAULT_FIELD = 'issue-custom-vault'; +const BRIDGE_ISSUE_CUSTOM_VAULT_SWITCH = 'issue-custom-vault-switch'; +const BRIDGE_ISSUE_GRIEFING_COLLATERAL_TOKEN = 'issue-griefing-collateral-token'; +const BRIDGE_ISSUE_FEE_TOKEN = 'issue-fee-token'; + +type BridgeIssueFormData = { + [BRIDGE_ISSUE_AMOUNT_FIELD]?: string; + [BRIDGE_ISSUE_CUSTOM_VAULT_FIELD]?: string; + [BRIDGE_ISSUE_CUSTOM_VAULT_SWITCH]?: boolean; + [BRIDGE_ISSUE_GRIEFING_COLLATERAL_TOKEN]?: string; + [BRIDGE_ISSUE_FEE_TOKEN]?: string; +}; + +type BridgeIssueValidationParams = { + [BRIDGE_ISSUE_AMOUNT_FIELD]: MaxAmountValidationParams & MinAmountValidationParams; +}; + +const bridgeIssueSchema = (params: BridgeIssueValidationParams): yup.ObjectSchema => + yup.object().shape({ + [BRIDGE_ISSUE_AMOUNT_FIELD]: yup + .string() + .requiredAmount('issue') + .maxAmount( + params[BRIDGE_ISSUE_AMOUNT_FIELD], + 'issue', + i18n.t('forms.amount_must_be_at_most', { + action: 'issue', + amount: params[BRIDGE_ISSUE_AMOUNT_FIELD].maxAmount.toString() + }) + ) + .minAmount(params[BRIDGE_ISSUE_AMOUNT_FIELD], 'issue'), + [BRIDGE_ISSUE_CUSTOM_VAULT_FIELD]: yup.string().when([BRIDGE_ISSUE_CUSTOM_VAULT_SWITCH], { + is: (isManualVault: string) => isManualVault, + then: (schema) => schema.required(i18n.t('forms.please_select_your_field', { field: 'issue vault' })) + }), + [BRIDGE_ISSUE_GRIEFING_COLLATERAL_TOKEN]: yup.string().required(), + [BRIDGE_ISSUE_FEE_TOKEN]: yup.string().required() + }); + +const BRIDGE_REDEEM_AMOUNT_FIELD = 'redeem-amount'; +const BRIDGE_REDEEM_CUSTOM_VAULT_FIELD = 'redeem-custom-vault'; +const BRIDGE_REDEEM_CUSTOM_VAULT_SWITCH = 'redeem-custom-vault-switch'; +const BRIDGE_REDEEM_PREMIUM_VAULT_FIELD = 'redeem-premium-vault'; +const BRIDGE_REDEEM_ADDRESS = 'redeem-address'; +const BRIDGE_REDEEM_FEE_TOKEN = 'redeem-fee-token'; + +type BridgeRedeemFormData = { + [BRIDGE_REDEEM_AMOUNT_FIELD]?: string; + [BRIDGE_REDEEM_CUSTOM_VAULT_FIELD]?: string; + [BRIDGE_REDEEM_CUSTOM_VAULT_SWITCH]?: boolean; + [BRIDGE_REDEEM_PREMIUM_VAULT_FIELD]?: boolean; + [BRIDGE_REDEEM_ADDRESS]?: string; + [BRIDGE_REDEEM_FEE_TOKEN]?: string; +}; + +type BridgeRedeemValidationParams = { + [BRIDGE_REDEEM_AMOUNT_FIELD]: MaxAmountValidationParams & MinAmountValidationParams; +}; + +const bridgeRedeemSchema = (params: BridgeRedeemValidationParams): yup.ObjectSchema => + yup.object().shape({ + [BRIDGE_REDEEM_AMOUNT_FIELD]: yup + .string() + .requiredAmount('redeem') + .maxAmount( + params[BRIDGE_REDEEM_AMOUNT_FIELD], + 'redeem', + i18n.t('forms.amount_must_be_at_most', { + action: 'redeem', + amount: params[BRIDGE_REDEEM_AMOUNT_FIELD].maxAmount.toString() + }) + ) + .minAmount(params[BRIDGE_REDEEM_AMOUNT_FIELD], 'redeem'), + [BRIDGE_REDEEM_CUSTOM_VAULT_FIELD]: yup.string().when([BRIDGE_REDEEM_CUSTOM_VAULT_SWITCH], { + is: (isManualVault: string) => isManualVault, + then: (schema) => schema.required(i18n.t('forms.please_select_your_field', { field: 'redeem vault' })) + }), + [BRIDGE_REDEEM_ADDRESS]: yup + .string() + .required(i18n.t('forms.please_enter_your_field', { field: 'BTC address' })) + .address(AddressType.BTC), + [BRIDGE_REDEEM_FEE_TOKEN]: yup.string().required() + }); + +export { + BRIDGE_ISSUE_AMOUNT_FIELD, + BRIDGE_ISSUE_CUSTOM_VAULT_FIELD, + BRIDGE_ISSUE_CUSTOM_VAULT_SWITCH, + BRIDGE_ISSUE_FEE_TOKEN, + BRIDGE_ISSUE_GRIEFING_COLLATERAL_TOKEN, + BRIDGE_REDEEM_ADDRESS, + BRIDGE_REDEEM_AMOUNT_FIELD, + BRIDGE_REDEEM_CUSTOM_VAULT_FIELD, + BRIDGE_REDEEM_CUSTOM_VAULT_SWITCH, + BRIDGE_REDEEM_FEE_TOKEN, + BRIDGE_REDEEM_PREMIUM_VAULT_FIELD, + bridgeIssueSchema, + bridgeRedeemSchema +}; +export type { BridgeIssueFormData, BridgeRedeemFormData }; diff --git a/src/lib/form/schemas/index.ts b/src/lib/form/schemas/index.ts index fac035a489..3c89f2e120 100644 --- a/src/lib/form/schemas/index.ts +++ b/src/lib/form/schemas/index.ts @@ -1,29 +1,7 @@ -export type { - DepositLiquidityPoolFormData, - DepositLiquidityPoolValidationParams, - WithdrawLiquidityPoolFormData, - WithdrawLiquidityPoolValidationParams -} from './amm'; -export { depositLiquidityPoolSchema, WITHDRAW_LIQUIDITY_POOL_FIELD, withdrawLiquidityPoolSchema } from './amm'; -export type { LoanFormData, LoanValidationParams } from './loans'; -export { loanSchema } from './loans'; -export { StrategySchema } from './strategy'; -export type { SwapFormData, SwapValidationParams } from './swap'; -export { - SWAP_INPUT_AMOUNT_FIELD, - SWAP_INPUT_TOKEN_FIELD, - SWAP_OUTPUT_TOKEN_FIELD, - SwapErrorMessage, - swapSchema -} from './swap'; -export type { CrossChainTransferFormData, CrossChainTransferValidationParams } from './transfers'; -export { - CROSS_CHAIN_TRANSFER_AMOUNT_FIELD, - CROSS_CHAIN_TRANSFER_FROM_FIELD, - CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD, - CROSS_CHAIN_TRANSFER_TO_FIELD, - CROSS_CHAIN_TRANSFER_TOKEN_FIELD, - crossChainTransferSchema -} from './transfers'; -export type { CreateVaultFormData } from './vaults'; -export { CREATE_VAULT_DEPOSIT_FIELD, createVaultSchema } from './vaults'; +export * from './bridge'; +export * from './loans'; +export * from './pools'; +export * from './strategy'; +export * from './swap'; +export * from './transfers'; +export * from './vaults'; diff --git a/src/lib/form/schemas/loans.ts b/src/lib/form/schemas/loans.ts index 42dcefcc52..43c02e3172 100644 --- a/src/lib/form/schemas/loans.ts +++ b/src/lib/form/schemas/loans.ts @@ -1,16 +1,57 @@ import { LoanAction } from '@/types/loans'; -import yup, { FeesValidationParams, MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; +import yup, { MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; -type LoanFormData = Partial>; +const LOAN_AMOUNT_FIELD = 'loan-amount'; +const LOAN_FEE_TOKEN_FIELD = 'loan-fee-token'; -type LoanValidationParams = FeesValidationParams & MaxAmountValidationParams & MinAmountValidationParams; +type LoanFormData = { + [LOAN_AMOUNT_FIELD]?: string; + [LOAN_FEE_TOKEN_FIELD]?: string; +}; + +type LoanValidationParams = MaxAmountValidationParams & MinAmountValidationParams; const loanSchema = (loanAction: LoanAction, params: LoanValidationParams): yup.ObjectSchema => { return yup.object().shape({ - [loanAction]: yup.string().requiredAmount(loanAction).maxAmount(params).minAmount(params, loanAction).fees(params) + [LOAN_AMOUNT_FIELD]: yup + .string() + .requiredAmount(loanAction) + .maxAmount(params, loanAction) + .minAmount(params, loanAction), + [LOAN_FEE_TOKEN_FIELD]: yup.string().required() }); }; -export { loanSchema }; -export type { LoanFormData, LoanValidationParams }; +const LOAN_TOGGLE_COLLATERAL_FEE_TOKEN_FIELD = 'loan-toggle-collateral-fee-token'; + +type ToggleCollateralLoansFormData = { + [LOAN_TOGGLE_COLLATERAL_FEE_TOKEN_FIELD]?: string; +}; + +const toggleCollateralLoanSchema = (): yup.ObjectSchema => + yup.object().shape({ + [LOAN_TOGGLE_COLLATERAL_FEE_TOKEN_FIELD]: yup.string().required() + }); + +const LOAN_CLAIM_REWARDS_FEE_TOKEN_FIELD = 'loan-claim-rewards-fee-token'; + +type ClaimRewardsLoansFormData = { + [LOAN_CLAIM_REWARDS_FEE_TOKEN_FIELD]?: string; +}; + +const claimRewardsLoanSchema = (): yup.ObjectSchema => + yup.object().shape({ + [LOAN_CLAIM_REWARDS_FEE_TOKEN_FIELD]: yup.string().required() + }); + +export { + claimRewardsLoanSchema, + LOAN_AMOUNT_FIELD, + LOAN_CLAIM_REWARDS_FEE_TOKEN_FIELD, + LOAN_FEE_TOKEN_FIELD, + LOAN_TOGGLE_COLLATERAL_FEE_TOKEN_FIELD, + loanSchema, + toggleCollateralLoanSchema +}; +export type { ClaimRewardsLoansFormData, LoanFormData, LoanValidationParams, ToggleCollateralLoansFormData }; diff --git a/src/lib/form/schemas/pools.ts b/src/lib/form/schemas/pools.ts new file mode 100644 index 0000000000..43eea88722 --- /dev/null +++ b/src/lib/form/schemas/pools.ts @@ -0,0 +1,72 @@ +import yup, { MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; + +const POOL_DEPOSIT_FEE_TOKEN_FIELD = 'despodit-fee-token'; + +type DepositLiquidityPoolFormData = { + [field: string]: string | undefined; + [POOL_DEPOSIT_FEE_TOKEN_FIELD]?: string; +}; + +type DepositLiquidityPoolValidationParams = { + tokens: Record; +}; + +// MEMO: schema is dynamic because is deals with one to many fields +const depositLiquidityPoolSchema = (params: DepositLiquidityPoolValidationParams): yup.ObjectSchema => { + const shape = Object.keys(params.tokens).reduce((acc, ticker) => { + const tokenParams = params.tokens[ticker]; + const validation = yup.string().requiredAmount('deposit').maxAmount(tokenParams).minAmount(tokenParams, 'deposit'); + + return { ...acc, [ticker]: validation }; + }, {}); + + return yup.object().shape({ ...shape, [POOL_DEPOSIT_FEE_TOKEN_FIELD]: yup.string().required() }); +}; + +const POOL_WITHDRAW_AMOUNT_FIELD = 'withdraw-amount'; +const POOL_WITHDRAW_FEE_TOKEN_FIELD = 'withdraw-fee-token'; + +type WithdrawLiquidityPoolFormData = { + [POOL_WITHDRAW_AMOUNT_FIELD]?: string; + [POOL_WITHDRAW_FEE_TOKEN_FIELD]?: string; +}; + +type WithdrawLiquidityPoolValidationParams = MaxAmountValidationParams & MinAmountValidationParams; + +const withdrawLiquidityPoolSchema = (params: WithdrawLiquidityPoolValidationParams): yup.ObjectSchema => + yup.object().shape({ + [POOL_WITHDRAW_AMOUNT_FIELD]: yup + .string() + .requiredAmount('withdraw') + .maxAmount(params) + .minAmount(params, 'withdraw'), + [POOL_WITHDRAW_FEE_TOKEN_FIELD]: yup.string().required() + }); + +const POOL_CLAIM_REWARDS_FEE_TOKEN_FIELD = 'claim-rewards-fee-token'; + +type ClaimRewardsPoolFormData = { + [POOL_CLAIM_REWARDS_FEE_TOKEN_FIELD]?: string; +}; + +const claimRewardsPoolSchema = (): yup.ObjectSchema => + yup.object().shape({ + [POOL_CLAIM_REWARDS_FEE_TOKEN_FIELD]: yup.string().required() + }); + +export { + claimRewardsPoolSchema, + depositLiquidityPoolSchema, + POOL_CLAIM_REWARDS_FEE_TOKEN_FIELD, + POOL_DEPOSIT_FEE_TOKEN_FIELD, + POOL_WITHDRAW_AMOUNT_FIELD, + POOL_WITHDRAW_FEE_TOKEN_FIELD, + withdrawLiquidityPoolSchema +}; +export type { + ClaimRewardsPoolFormData, + DepositLiquidityPoolFormData, + DepositLiquidityPoolValidationParams, + WithdrawLiquidityPoolFormData, + WithdrawLiquidityPoolValidationParams +}; diff --git a/src/lib/form/schemas/swap.ts b/src/lib/form/schemas/swap.ts index 8b9a7855b2..e5c54e01de 100644 --- a/src/lib/form/schemas/swap.ts +++ b/src/lib/form/schemas/swap.ts @@ -1,4 +1,4 @@ -import yup, { FeesValidationParams, MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; +import yup, { MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; enum SwapErrorMessage { SELECT_TOKEN = 'SELECT_TOKEN', @@ -10,16 +10,16 @@ enum SwapErrorMessage { const SWAP_INPUT_AMOUNT_FIELD = 'input-amount'; const SWAP_INPUT_TOKEN_FIELD = 'input-token'; const SWAP_OUTPUT_TOKEN_FIELD = 'output-token'; +const SWAP_FEE_TOKEN_FIELD = 'fee-token'; type SwapFormData = { [SWAP_INPUT_AMOUNT_FIELD]?: string; [SWAP_INPUT_TOKEN_FIELD]?: string; [SWAP_OUTPUT_TOKEN_FIELD]?: string; + [SWAP_FEE_TOKEN_FIELD]?: string; }; -type SwapValidationParams = FeesValidationParams & - Partial & - Partial; +type SwapValidationParams = Partial & Partial; // Does not follow the normal pattern because this form has a // custom validation, specially when it comes to error messages @@ -39,9 +39,16 @@ const swapSchema = (params: { [SWAP_INPUT_AMOUNT_FIELD]: SwapValidationParams }) undefined, 'amm.insufficient_token_balance' ) - .fees(params[SWAP_INPUT_AMOUNT_FIELD], 'insufficient_funds_fees') - }) + }), + [SWAP_FEE_TOKEN_FIELD]: yup.string().required() }); -export { SWAP_INPUT_AMOUNT_FIELD, SWAP_INPUT_TOKEN_FIELD, SWAP_OUTPUT_TOKEN_FIELD, SwapErrorMessage, swapSchema }; +export { + SWAP_FEE_TOKEN_FIELD, + SWAP_INPUT_AMOUNT_FIELD, + SWAP_INPUT_TOKEN_FIELD, + SWAP_OUTPUT_TOKEN_FIELD, + SwapErrorMessage, + swapSchema +}; export type { SwapFormData, SwapValidationParams }; diff --git a/src/lib/form/schemas/transfers.ts b/src/lib/form/schemas/transfers.ts index a6fe5c5f47..6adf8adcb0 100644 --- a/src/lib/form/schemas/transfers.ts +++ b/src/lib/form/schemas/transfers.ts @@ -1,4 +1,5 @@ import { ChainName } from '@interlay/bridge'; +import i18n from 'i18next'; import { TFunction } from 'react-i18next'; import yup, { MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; @@ -43,12 +44,55 @@ const crossChainTransferSchema = (params: CrossChainTransferValidationParams, t: .required(t('forms.please_select_your_field', { field: 'transfer token' })) }); +const TRANSFER_RECIPIENT_FIELD = 'transfer-destination'; +const TRANSFER_TOKEN_FIELD = 'transfer-token'; +const TRANSFER_AMOUNT_FIELD = 'transfer-amount'; +const TRANSFER_FEE_TOKEN_FIELD = 'transfer-fee-token'; + +type TransferFormData = { + [TRANSFER_RECIPIENT_FIELD]?: string; + [TRANSFER_TOKEN_FIELD]?: string; + [TRANSFER_AMOUNT_FIELD]?: string; + [TRANSFER_FEE_TOKEN_FIELD]?: string; +}; + +type TransferValidationParams = { + [TRANSFER_AMOUNT_FIELD]: Partial & Partial; +}; + +const transferSchema = (params: TransferValidationParams): yup.ObjectSchema => + yup.object().shape({ + [TRANSFER_AMOUNT_FIELD]: yup + .string() + .requiredAmount('transfer') + .maxAmount(params[TRANSFER_AMOUNT_FIELD] as MaxAmountValidationParams) + .minAmount(params[TRANSFER_AMOUNT_FIELD] as MinAmountValidationParams, 'transfer'), + [TRANSFER_RECIPIENT_FIELD]: yup + .string() + .required(i18n.t('forms.please_enter_your_field', { field: 'recipient' })) + .address(), + [TRANSFER_TOKEN_FIELD]: yup + .string() + .required(i18n.t('forms.please_select_your_field', { field: 'transfer token' })), + [TRANSFER_FEE_TOKEN_FIELD]: yup.string().required() + }); + export { CROSS_CHAIN_TRANSFER_AMOUNT_FIELD, CROSS_CHAIN_TRANSFER_FROM_FIELD, CROSS_CHAIN_TRANSFER_TO_ACCOUNT_FIELD, CROSS_CHAIN_TRANSFER_TO_FIELD, CROSS_CHAIN_TRANSFER_TOKEN_FIELD, - crossChainTransferSchema + crossChainTransferSchema, + TRANSFER_AMOUNT_FIELD, + TRANSFER_FEE_TOKEN_FIELD, + TRANSFER_RECIPIENT_FIELD, + TRANSFER_TOKEN_FIELD, + transferSchema +}; +export type { + CrossChainTransferFormData, + CrossChainTransferValidationParams, + TransferFormData, + TransferValidationParams }; -export type { CrossChainTransferFormData, CrossChainTransferValidationParams }; diff --git a/src/lib/form/schemas/vaults.ts b/src/lib/form/schemas/vaults.ts index 0a348422c0..eda17492c2 100644 --- a/src/lib/form/schemas/vaults.ts +++ b/src/lib/form/schemas/vaults.ts @@ -1,22 +1,30 @@ -import yup, { FeesValidationParams, MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; +import i18n from 'i18next'; -const CREATE_VAULT_DEPOSIT_FIELD = 'deposit'; +import yup, { MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; -type CreateVaultFormData = { - [CREATE_VAULT_DEPOSIT_FIELD]?: number; +const VAULTS_DEPOSIT_COLLATERAL_AMOUNT_FIELD = 'vaults-deposit-collateral_amount'; +const VAULTS_DEPOSIT_COLLATERAL_FEE_TOKEN_FIELD = 'vaults-deposit-collateral-fee-token'; + +type VaultsDepositCollateralFormData = { + [VAULTS_DEPOSIT_COLLATERAL_AMOUNT_FIELD]?: string; + [VAULTS_DEPOSIT_COLLATERAL_FEE_TOKEN_FIELD]?: string; }; -type CreateVaultValidationParams = FeesValidationParams & MaxAmountValidationParams & MinAmountValidationParams; +type VaultsDepositCollateralValidationParams = MaxAmountValidationParams & MinAmountValidationParams; -const createVaultSchema = (params: CreateVaultValidationParams): yup.ObjectSchema => +const depositCollateralVaultsSchema = (params: VaultsDepositCollateralValidationParams): yup.ObjectSchema => yup.object().shape({ - [CREATE_VAULT_DEPOSIT_FIELD]: yup + [VAULTS_DEPOSIT_COLLATERAL_AMOUNT_FIELD]: yup .string() - .requiredAmount(CREATE_VAULT_DEPOSIT_FIELD) + .requiredAmount(i18n.t('deposit').toLowerCase()) .maxAmount(params) - .minAmount(params, CREATE_VAULT_DEPOSIT_FIELD) - .fees(params) + .minAmount(params, i18n.t('deposit').toLowerCase()), + [VAULTS_DEPOSIT_COLLATERAL_FEE_TOKEN_FIELD]: yup.string().required() }); -export { CREATE_VAULT_DEPOSIT_FIELD, createVaultSchema }; -export type { CreateVaultFormData }; +export { + depositCollateralVaultsSchema, + VAULTS_DEPOSIT_COLLATERAL_AMOUNT_FIELD, + VAULTS_DEPOSIT_COLLATERAL_FEE_TOKEN_FIELD +}; +export type { VaultsDepositCollateralFormData, VaultsDepositCollateralValidationParams }; diff --git a/src/lib/form/use-form.tsx b/src/lib/form/use-form.tsx index 7e62e97bd9..a85effedc3 100644 --- a/src/lib/form/use-form.tsx +++ b/src/lib/form/use-form.tsx @@ -1,66 +1,125 @@ -import { - FieldInputProps, - FormikConfig, - FormikErrors as FormErrors, - FormikValues, - useFormik, - validateYupSchema, - yupToFormErrors -} from 'formik'; -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; +import { chain } from '@react-aria/utils'; +import { FieldInputProps, FormikConfig, FormikErrors as FormErrors, FormikValues, useFormik } from 'formik'; +import { FocusEvent, Key, useCallback } from 'react'; +import { useDebounce } from 'react-use'; type GetFieldProps = ( nameOrOptions: any, - withErrorMessage?: boolean -) => FieldInputProps & { errorMessage?: string | string[] }; + hideErrorMessage?: boolean, + hideUntouchedError?: boolean +) => FieldInputProps & { + errorMessage?: string | string[]; + onSelectionChange: (key: Key) => void; +}; type UseFormArgs = FormikConfig & { - disableValidation?: boolean; - getFieldProps?: GetFieldProps; + hideErrorMessages?: boolean; + onComplete?: (form: Values) => void; }; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types const useForm = ({ - validationSchema, - disableValidation, + hideErrorMessages, + onComplete, ...args }: UseFormArgs) => { - const { t } = useTranslation(); - const { validateForm, values, getFieldProps: getFormikFieldProps, ...formik } = useFormik({ - ...args, - validate: (values) => { - if (disableValidation) return; - - try { - validateYupSchema(values, validationSchema, true, { t }); - } catch (err) { - return yupToFormErrors(err); + const { + validateForm, + values, + getFieldProps: getFormikFieldProps, + setFieldTouched, + setFieldValue, + ...formik + } = useFormik(args); + + // emits onComplete event based on debounced values, only if form is modified and valid + // meaning that it will only check for completeness in 250ms interval of no changes to the values + useDebounce( + () => { + if (!formik.isValid || !formik.dirty) return; + + onComplete?.(values); + }, + 250, + // do not run debounce if onComplete is not passed + onComplete ? [values] : [] + ); + + // Handles when field gets forced blur to focus on modal + // If so, we dont want to consider it as touched if it has not yet been touched on + const handleBlur = useCallback( + (e: FocusEvent, fieldName: string, isTouched: boolean) => { + if (!isTouched) { + const relatedTargetEl = e.relatedTarget as HTMLElement; + const targetEl = e.target as HTMLElement; + + if (!relatedTargetEl || !targetEl) return; + + const isModal = relatedTargetEl.getAttribute('role') === 'dialog'; + + if (!isModal) return; + + const modalId = relatedTargetEl.getAttribute('id'); + const buttonAriaControls = targetEl.getAttribute('aria-controls'); + + if (!modalId || !buttonAriaControls) return; + + const isSelect = buttonAriaControls === modalId; + + if (!isSelect) return; + + setFieldTouched(fieldName, false); } - } - }); + }, + [setFieldTouched] + ); const getFieldProps: GetFieldProps = useCallback( - (nameOrOptions, withErrorMessage = true) => { - if (withErrorMessage) { - const isOptions = nameOrOptions !== null && typeof nameOrOptions === 'object'; - const errorMessage = isOptions ? formik.errors[nameOrOptions.name] : formik.errors[nameOrOptions]; + (nameOrOptions: any, hideErrorMessage?: boolean, hideUntouchedError?: boolean) => { + const fieldProps = getFormikFieldProps(nameOrOptions); + + const isOptions = nameOrOptions !== null && typeof nameOrOptions === 'object'; + const fieldName = isOptions ? nameOrOptions.name : nameOrOptions; + + const customFieldProps = { + ...fieldProps, + onSelectionChange: (key: Key) => { + setFieldValue(fieldName, key, true); + } + }; + + // Asses if error message is going to be omitted, but validation still takes place (approach used in swap due to custom error messages) + const hideError = hideErrorMessage || hideErrorMessages; + + if (!hideError) { + const isTouched = formik.touched[fieldName]; + + // Option allows to only show error when input is touched. + // Input is touched when if focus and blur events are emitted + const errorMessage = hideUntouchedError + ? isTouched + ? formik.errors[fieldName] + : undefined + : formik.errors[fieldName]; return { - ...getFormikFieldProps(nameOrOptions), + ...customFieldProps, + onBlur: chain(fieldProps.onBlur, (e: FocusEvent) => handleBlur(e, fieldName, isTouched as boolean)), errorMessage: errorMessage as string | string[] | undefined }; } - return getFormikFieldProps(nameOrOptions); + return customFieldProps; }, - [formik.errors, getFormikFieldProps] + [getFormikFieldProps, hideErrorMessages, formik.touched, formik.errors, setFieldValue, handleBlur] ); return { values, validateForm, getFieldProps, + setFieldTouched, + setFieldValue, ...formik }; }; diff --git a/src/lib/form/validate.ts b/src/lib/form/validate.ts new file mode 100644 index 0000000000..5e1c82adeb --- /dev/null +++ b/src/lib/form/validate.ts @@ -0,0 +1,21 @@ +import { decodeAddress, encodeAddress } from '@polkadot/keyring'; +import { hexToU8a, isHex } from '@polkadot/util'; + +import { BTC_ADDRESS_REGEX } from '@/constants'; + +const btcAddressRegex = new RegExp(BTC_ADDRESS_REGEX); + +// TODO: use library instead +const isValidBTCAddress = (address: string): boolean => btcAddressRegex.test(address); + +const isValidRelayAddress = (address: string): boolean => { + try { + encodeAddress(isHex(address) ? hexToU8a(address) : decodeAddress(address)); + + return true; + } catch { + return false; + } +}; + +export { isValidBTCAddress, isValidRelayAddress }; diff --git a/src/lib/form/yup.custom.ts b/src/lib/form/yup.custom.ts index f2195f81aa..b24a1b9af0 100644 --- a/src/lib/form/yup.custom.ts +++ b/src/lib/form/yup.custom.ts @@ -2,20 +2,16 @@ import { CurrencyExt } from '@interlay/interbtc-api'; import { MonetaryAmount } from '@interlay/monetary-js'; import Big from 'big.js'; -import { TFunction } from 'react-i18next'; +import i18n from 'i18next'; import * as yup from 'yup'; import { AnyObject, Maybe } from 'yup/lib/types'; -type YupContext = { - t: TFunction; -}; +import { isValidBTCAddress, isValidRelayAddress } from './validate'; yup.addMethod(yup.string, 'requiredAmount', function (action: string, customMessage?: string) { return this.transform((value) => (isNaN(value) ? undefined : value)).test('requiredAmount', (value, ctx) => { if (value === undefined) { - const { t } = ctx.options.context as YupContext; - - const message = customMessage || t('forms.please_enter_the_amount_to', { field: action }); + const message = customMessage || i18n.t('forms.please_enter_the_amount_to', { field: action }); return ctx.createError({ message }); } @@ -34,12 +30,10 @@ yup.addMethod( 'fees', function ({ transactionFee, governanceBalance }: FeesValidationParams, customMessage?: string) { return this.test('fees', (_, ctx) => { - const { t } = ctx.options.context as YupContext; - if (governanceBalance.lt(transactionFee)) { const message = customMessage || - t('insufficient_funds_governance_token', { + i18n.t('insufficient_funds_governance_token', { governanceTokenSymbol: transactionFee.currency.ticker }); @@ -60,8 +54,6 @@ yup.addMethod( 'maxAmount', function ({ maxAmount }: MaxAmountValidationParams, action?: string, customMessage?: string) { return this.test('maxAmount', (value, ctx) => { - const { t } = ctx.options.context as YupContext; - if (value === undefined) return true; const amount = new Big(value); @@ -70,12 +62,13 @@ yup.addMethod( // same validation, just different data types that lead to different implementation if (isMonetaryAmount && amount.gt((maxAmount as MonetaryAmount).toBig())) { - const message = customMessage || t('forms.please_enter_no_higher_available_balance'); + const message = customMessage || i18n.t('forms.please_enter_no_higher_available_balance'); return ctx.createError({ message }); } if (amount.gt(maxAmount as Big)) { - const message = customMessage || t('forms.amount_must_be_at_most', { action, amount: maxAmount.toString() }); + const message = + customMessage || i18n.t('forms.amount_must_be_at_most', { action, amount: maxAmount.toString() }); return ctx.createError({ message }); } @@ -93,8 +86,6 @@ yup.addMethod( 'minAmount', function ({ minAmount }: MinAmountValidationParams, action: string, customMessage?: string) { return this.test('balance', (value, ctx) => { - const { t } = ctx.options.context as YupContext; - if (value === undefined) return true; const amount = new Big(value); @@ -102,7 +93,7 @@ yup.addMethod( if (amount.lt(minAmount.toBig())) { const message = customMessage || - t('forms.amount_must_be_at_least', { + i18n.t('forms.amount_must_be_at_least', { action, amount: minAmount.toString(), token: minAmount.currency.ticker @@ -115,6 +106,33 @@ yup.addMethod( } ); +enum AddressType { + RELAY_CHAIN, + BTC +} + +const addressValidationMap = { + [AddressType.RELAY_CHAIN]: isValidRelayAddress, + [AddressType.BTC]: isValidBTCAddress +}; + +yup.addMethod( + yup.string, + 'address', + function (addressType: AddressType = AddressType.RELAY_CHAIN, customMessage?: string) { + return this.test('address', (value, ctx) => { + const isValidAdress = addressValidationMap[addressType]; + + if (!value || !isValidAdress(value)) { + const message = customMessage || i18n.t('forms.please_enter_a_valid_address'); + return ctx.createError({ message }); + } + + return true; + }); + } +); + declare module 'yup' { interface StringSchema< TType extends Maybe = string | undefined, @@ -133,8 +151,10 @@ declare module 'yup' { action?: string, customMessage?: string ): StringSchema; + address(addressType?: AddressType, customMessage?: string): StringSchema; } } export default yup; -export type { FeesValidationParams, MaxAmountValidationParams, MinAmountValidationParams, YupContext }; +export { AddressType }; +export type { FeesValidationParams, MaxAmountValidationParams, MinAmountValidationParams }; diff --git a/src/pages/AMM/Pools/components/DepositForm/DepositDivider.tsx b/src/pages/AMM/Pools/components/DepositForm/DepositDivider.tsx deleted file mode 100644 index 89ff283a43..0000000000 --- a/src/pages/AMM/Pools/components/DepositForm/DepositDivider.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { PlusCircle } from '@/assets/icons'; - -import { StyledBackground, StyledCircle, StyledDivider, StyledWrapper } from './DepositForm.styles'; - -const DepositDivider = (): JSX.Element => ( - - - - - - - -); - -export { DepositDivider }; diff --git a/src/pages/AMM/Pools/components/DepositForm/DepositForm.styles.tsx b/src/pages/AMM/Pools/components/DepositForm/DepositForm.styles.tsx index 6baa738902..7f8d378c46 100644 --- a/src/pages/AMM/Pools/components/DepositForm/DepositForm.styles.tsx +++ b/src/pages/AMM/Pools/components/DepositForm/DepositForm.styles.tsx @@ -1,41 +1,7 @@ import styled from 'styled-components'; -import { Divider, Dl, DlGroup, theme } from '@/component-library'; - -const StyledDl = styled(Dl)` - background-color: ${theme.card.bg.secondary}; - padding: ${theme.spacing.spacing4}; - font-size: ${theme.text.xs}; - border-radius: ${theme.rounded.rg}; -`; - -const StyledWrapper = styled.div` - position: relative; -`; - -const StyledCircle = styled.div` - display: inline-flex; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - padding: ${theme.spacing.spacing2}; - background-color: var(--colors-token-input-end-adornment-bg); - border-radius: ${theme.rounded.full}; -`; - -const StyledBackground = styled.div` - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - padding: ${theme.spacing.spacing1} ${theme.spacing.spacing8}; - background-color: ${theme.colors.bgPrimary}; -`; - -const StyledDivider = styled(Divider)` - background-color: var(--colors-token-input-end-adornment-bg); -`; +import { DlGroup, theme, TokenInput } from '@/component-library'; +import { PlusDivider } from '@/components'; const StyledDlGroup = styled(DlGroup)` flex-direction: column; @@ -45,4 +11,13 @@ const StyledDlGroup = styled(DlGroup)` } `; -export { StyledBackground, StyledCircle, StyledDivider, StyledDl, StyledDlGroup, StyledWrapper }; +const StyledPlusDivider = styled(PlusDivider)` + margin-bottom: calc(${theme.spacing.spacing2} * -1); + z-index: 0; +`; + +const StyledTokenInput = styled(TokenInput)` + z-index: 1; +`; + +export { StyledDlGroup, StyledPlusDivider, StyledTokenInput }; diff --git a/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx b/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx index 6b7387aeee..6a6c676b3a 100644 --- a/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx +++ b/src/pages/AMM/Pools/components/DepositForm/DepositForm.tsx @@ -1,18 +1,17 @@ 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 { ChangeEventHandler, Fragment, RefObject, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { displayMonetaryAmountInUSDFormat, newSafeMonetaryAmount } from '@/common/utils/utils'; -import { Alert, Dd, DlGroup, Dt, Flex, TokenInput } from '@/component-library'; -import { AuthCTA } from '@/components'; -import { GOVERNANCE_TOKEN, TRANSACTION_FEE_AMOUNT } from '@/config/relay-chains'; +import { newSafeMonetaryAmount } from '@/common/utils/utils'; +import { Alert, Flex } from '@/component-library'; +import { AuthCTA, TransactionFeeDetails } from '@/components'; import { DepositLiquidityPoolFormData, depositLiquidityPoolSchema, DepositLiquidityPoolValidationParams, - isFormDisabled, + POOL_DEPOSIT_FEE_TOKEN_FIELD, useForm } from '@/lib/form'; import { SlippageManager } from '@/pages/AMM/shared/components'; @@ -21,11 +20,11 @@ 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 { isTransactionFormDisabled } from '@/utils/hooks/transaction/utils/form'; import useAccountId from '@/utils/hooks/use-account-id'; import { PoolName } from '../PoolName'; -import { DepositDivider } from './DepositDivider'; -import { StyledDl } from './DepositForm.styles'; +import { StyledPlusDivider, StyledTokenInput } from './DepositForm.styles'; import { DepositOutputAssets } from './DepositOutputAssets'; const isCustomAmountsMode = (form: ReturnType) => @@ -33,73 +32,102 @@ const isCustomAmountsMode = (form: ReturnType) => type DepositFormProps = { pool: LiquidityPool; - slippageModalRef: RefObject; + overlappingModalRef: RefObject; onSuccess?: () => void; onSigning?: () => void; }; -const DepositForm = ({ pool, slippageModalRef, onSuccess, onSigning }: DepositFormProps): JSX.Element => { +const DepositForm = ({ pool, overlappingModalRef, onSuccess, onSigning }: DepositFormProps): JSX.Element => { const { pooledCurrencies } = pool; - const defaultValues = pooledCurrencies.reduce((acc, amount) => ({ ...acc, [amount.currency.ticker]: '' }), {}); const [slippage, setSlippage] = useState(0.1); const accountId = useAccountId(); const { t } = useTranslation(); - const { getAvailableBalance, getBalance } = useGetBalances(); + const { getAvailableBalance } = useGetBalances(); const prices = useGetPrices(); - const governanceBalance = getBalance(GOVERNANCE_TOKEN.ticker)?.free || newMonetaryAmount(0, GOVERNANCE_TOKEN); - const transaction = useTransaction(Transaction.AMM_ADD_LIQUIDITY, { onSuccess, onSigning }); - const handleSubmit = async (data: DepositLiquidityPoolFormData) => { - if (!accountId) return; + const getTransactionArgs = useCallback( + async (values: DepositLiquidityPoolFormData) => { + if (!accountId) return; - try { const amounts = pooledCurrencies.map((amount) => - newSafeMonetaryAmount(data[amount.currency.ticker] || 0, amount.currency, true) + newSafeMonetaryAmount(values[amount.currency.ticker] || 0, amount.currency, true) ); - const deadline = await window.bridge.system.getFutureBlockNumber(AMM_DEADLINE_INTERVAL); + try { + const deadline = await window.bridge.system.getFutureBlockNumber(AMM_DEADLINE_INTERVAL); - return transaction.execute(amounts, pool, slippage, deadline, accountId); - } catch (error: any) { - transaction.reject(error); - } + return { amounts, pool, slippage, deadline, accountId }; + } catch (error: any) { + transaction.reject(error); + } + }, + [accountId, pool, pooledCurrencies, slippage, transaction] + ); + + const handleSubmit = async (data: DepositLiquidityPoolFormData) => { + const transactionData = await getTransactionArgs(data); + + if (!transactionData) return; + + const { accountId, amounts, deadline, pool } = transactionData; + + return transaction.execute(amounts, pool, slippage, deadline, accountId); }; - const tokens = pooledCurrencies.reduce( - (acc: DepositLiquidityPoolValidationParams['tokens'], pooled) => ({ - ...acc, - [pooled.currency.ticker]: { - minAmount: newMonetaryAmount(1, pooled.currency), - maxAmount: getAvailableBalance(pooled.currency.ticker) || newMonetaryAmount(0, pooled.currency) - } - }), - {} + const tokens = useMemo( + () => + pooledCurrencies.reduce( + (acc: DepositLiquidityPoolValidationParams['tokens'], pooled) => ({ + ...acc, + [pooled.currency.ticker]: { + minAmount: newMonetaryAmount(1, pooled.currency), + maxAmount: getAvailableBalance(pooled.currency.ticker) || newMonetaryAmount(0, pooled.currency) + } + }), + {} + ), + [getAvailableBalance, pooledCurrencies] + ); + + const defaultValues = pooledCurrencies.reduce((acc, amount) => ({ ...acc, [amount.currency.ticker]: '' }), {}); + + const initialValues = useMemo( + () => ({ ...defaultValues, [POOL_DEPOSIT_FEE_TOKEN_FIELD]: transaction.fee.defaultCurrency.ticker }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] ); const form = useForm({ - initialValues: defaultValues, - validationSchema: depositLiquidityPoolSchema({ transactionFee: TRANSACTION_FEE_AMOUNT, governanceBalance, tokens }), - onSubmit: handleSubmit - }); + initialValues, + validationSchema: depositLiquidityPoolSchema({ tokens }), + onSubmit: handleSubmit, + onComplete: async (values) => { + const transactionData = await getTransactionArgs(values); - const handleChange: ChangeEventHandler = (e) => { - if (isCustomAmountsMode(form)) return; + if (!transactionData) return; - if (!e.target.value || isNaN(Number(e.target.value))) { - return form.setValues(defaultValues); + const { accountId, amounts, deadline, pool } = transactionData; + + const feeTicker = values[POOL_DEPOSIT_FEE_TOKEN_FIELD]; + + return transaction.fee.setCurrency(feeTicker).estimate(amounts, pool, slippage, deadline, accountId); } + }); + const handleChange: ChangeEventHandler = (e) => { // If pool has no liquidity, the assets ratio is set by the user, // therefore the value inputted is directly used. - if (pool.isEmpty) { - return form.setValues({ [e.target.name]: e.target.value }); + if (isCustomAmountsMode(form) || pool.isEmpty) return; + + if (!e.target.value || isNaN(Number(e.target.value))) { + return form.setValues({ ...form.values, ...defaultValues }); } const inputCurrency = pooledCurrencies.find((currency) => currency.currency.ticker === e.target.name); @@ -115,22 +143,22 @@ const DepositForm = ({ pool, slippageModalRef, onSuccess, onSigning }: DepositFo return { ...acc, [val.currency.ticker]: val.toBig().toString() }; }, {}); - form.setValues(newValues); + form.setValues({ ...form.values, ...newValues }); }; const poolName = ( amount.currency.ticker)} /> ); - const isBtnDisabled = isFormDisabled(form); + const isBtnDisabled = isTransactionFormDisabled(form, transaction.fee); return (
- setSlippage(slippage)} /> + setSlippage(slippage)} /> {poolName} - + {pooledCurrencies.map((amount, index) => { const { currency: { ticker } @@ -141,8 +169,8 @@ const DepositForm = ({ pool, slippageModalRef, onSuccess, onSigning }: DepositFo const balance = getAvailableBalance(ticker); return ( - - + - {!isLastItem && } - + {!isLastItem && } + ); })} @@ -167,25 +195,15 @@ const DepositForm = ({ pool, slippageModalRef, onSuccess, onSigning }: DepositFo ) : ( )} - - - -
- {t('fees')} -
-
- {TRANSACTION_FEE_AMOUNT.toHuman()} {TRANSACTION_FEE_AMOUNT.currency.ticker} ( - {displayMonetaryAmountInUSDFormat( - TRANSACTION_FEE_AMOUNT, - getTokenPrice(prices, TRANSACTION_FEE_AMOUNT.currency.ticker)?.usd - )} - ) -
-
-
- - {t('amm.pools.add_liquidity')} - + + + + {t('amm.pools.add_liquidity')} + +
diff --git a/src/pages/AMM/Pools/components/PoolModal/PoolModal.tsx b/src/pages/AMM/Pools/components/PoolModal/PoolModal.tsx index b768873cff..65c4eaa63d 100644 --- a/src/pages/AMM/Pools/components/PoolModal/PoolModal.tsx +++ b/src/pages/AMM/Pools/components/PoolModal/PoolModal.tsx @@ -20,7 +20,7 @@ type PoolModalProps = Props & InheritAttrs; const PoolModal = ({ pool, onClose, ...props }: PoolModalProps): JSX.Element | null => { const { t } = useTranslation(); const { refetch } = useGetAccountPools(); - const ref = useRef(null); + const overlappingModalRef = useRef(null); if (!pool) { return null; @@ -32,19 +32,29 @@ const PoolModal = ({ pool, onClose, ...props }: PoolModalProps): JSX.Element | n onClose={onClose} align='top' // Pool modal should not close while user interacts with stacked modal (slippage modal) - shouldCloseOnInteractOutside={(el) => !ref.current?.contains(el)} + shouldCloseOnInteractOutside={(el) => !overlappingModalRef.current?.contains(el)} {...props} > - + - + diff --git a/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx b/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx index 961db4e3c0..883ba28318 100644 --- a/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx +++ b/src/pages/AMM/Pools/components/PoolsInsights/PoolsInsights.tsx @@ -1,14 +1,29 @@ import { LiquidityPool } from '@interlay/interbtc-api'; import Big from 'big.js'; +import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { formatUSD } from '@/common/utils/utils'; -import { Card, Dl, DlGroup } from '@/component-library'; -import { AuthCTA } from '@/components'; +import { Card, CTA, Dl, DlGroup, Flex, Modal, ModalBody, ModalFooter, ModalHeader } from '@/component-library'; +import { + AuthCTA, + TransactionDetails, + TransactionDetailsDd, + TransactionDetailsDt, + TransactionDetailsGroup, + TransactionFeeDetails +} from '@/components'; +import { + ClaimRewardsPoolFormData, + claimRewardsPoolSchema, + POOL_CLAIM_REWARDS_FEE_TOKEN_FIELD, + useForm +} from '@/lib/form'; import { calculateAccountLiquidityUSD, calculateTotalLiquidityUSD } from '@/pages/AMM/shared/utils'; 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 { isTransactionFormDisabled } from '@/utils/hooks/transaction/utils/form'; import { StyledDd, StyledDt } from './PoolsInsights.style'; import { calculateClaimableFarmingRewardUSD } from './utils'; @@ -22,6 +37,42 @@ type PoolsInsightsProps = { const PoolsInsights = ({ pools, accountPoolsData, refetch }: PoolsInsightsProps): JSX.Element => { const { t } = useTranslation(); const prices = useGetPrices(); + const [isOpen, setOpen] = useState(false); + const overlappingModalRef = useRef(null); + + const transaction = useTransaction(Transaction.AMM_CLAIM_REWARDS, { + onSuccess: refetch, + onSigning: () => setOpen(false) + }); + + const handleSubmit = () => { + if (!accountPoolsData) return; + + transaction.execute(accountPoolsData.claimableRewards); + }; + + const form = useForm({ + initialValues: { + [POOL_CLAIM_REWARDS_FEE_TOKEN_FIELD]: '' + }, + validationSchema: claimRewardsPoolSchema(), + onSubmit: handleSubmit, + onComplete: async (values) => { + if (!accountPoolsData) return; + + const feeTicker = values[POOL_CLAIM_REWARDS_FEE_TOKEN_FIELD]; + + return transaction.fee.setCurrency(feeTicker).estimate(accountPoolsData.claimableRewards); + } + }); + + // Doing this call on mount so that the form becomes dirty + useEffect(() => { + form.setFieldValue(POOL_CLAIM_REWARDS_FEE_TOKEN_FIELD, transaction.fee.defaultCurrency.ticker, true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleCloseModal = () => setOpen(false); const accountPositions = accountPoolsData?.positions; @@ -47,40 +98,74 @@ const PoolsInsights = ({ pools, accountPoolsData, refetch }: PoolsInsightsProps) const totalLiquidityUSD = formatUSD(totalLiquidity?.toNumber() || 0, { compact: true }); const totalClaimableRewardUSD = calculateClaimableFarmingRewardUSD(accountPoolsData?.claimableRewards, prices); + const totalClaimableRewardUSDLabel = formatUSD(totalClaimableRewardUSD, { compact: true }); - const transaction = useTransaction(Transaction.AMM_CLAIM_REWARDS, { - onSuccess: refetch - }); - - const handleClickClaimRewards = () => accountPoolsData && transaction.execute(accountPoolsData.claimableRewards); + const handleClickClaimRewards = () => setOpen(true); const hasClaimableRewards = totalClaimableRewardUSD > 0; + + const isBtnDisabled = isTransactionFormDisabled(form, transaction.fee); + return ( -
- - - {t('supply_balance')} - {supplyBalanceLabel} - - - - - {t('total_liquidity')} - {totalLiquidityUSD} - - - - - {t('rewards')} - {formatUSD(totalClaimableRewardUSD, { compact: true })} - - {hasClaimableRewards && ( - - Claim - - )} - -
+ <> +
+ + + {t('supply_balance')} + {supplyBalanceLabel} + + + + + {t('total_liquidity')} + {totalLiquidityUSD} + + + + + {t('rewards')} + {totalClaimableRewardUSDLabel} + + {hasClaimableRewards && ( + + Claim + + )} + +
+ !overlappingModalRef.current?.contains(el)} + > + Claim Rewards + + + + Amount + {totalClaimableRewardUSDLabel} + + + + +
+ + + + {t('claim_rewards')} + + +
+
+
+ ); }; diff --git a/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx b/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx index 10d7f0fa85..f513e28299 100644 --- a/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx +++ b/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.tsx @@ -1,17 +1,13 @@ import { LiquidityPool, newMonetaryAmount } from '@interlay/interbtc-api'; import Big from 'big.js'; -import { RefObject, useState } from 'react'; +import { RefObject, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { - convertMonetaryAmountToValueInUSD, - displayMonetaryAmountInUSDFormat, - newSafeMonetaryAmount -} from '@/common/utils/utils'; -import { Dd, DlGroup, Dt, Flex, TokenInput } from '@/component-library'; -import { AuthCTA, ReceivableAssets } from '@/components'; +import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/common/utils/utils'; +import { Flex, TokenInput } from '@/component-library'; +import { AuthCTA, ReceivableAssets, TransactionFeeDetails } from '@/components'; import { GOVERNANCE_TOKEN, TRANSACTION_FEE_AMOUNT } from '@/config/relay-chains'; -import { isFormDisabled, useForm, WITHDRAW_LIQUIDITY_POOL_FIELD } from '@/lib/form'; +import { POOL_WITHDRAW_AMOUNT_FIELD, POOL_WITHDRAW_FEE_TOKEN_FIELD, useForm } from '@/lib/form'; import { WithdrawLiquidityPoolFormData, withdrawLiquidityPoolSchema } from '@/lib/form/schemas'; import { SlippageManager } from '@/pages/AMM/shared/components'; import { AMM_DEADLINE_INTERVAL } from '@/utils/constants/api'; @@ -19,19 +15,19 @@ 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 { isTransactionFormDisabled } from '@/utils/hooks/transaction/utils/form'; import useAccountId from '@/utils/hooks/use-account-id'; import { PoolName } from '../PoolName'; -import { StyledDl } from './WithdrawForm.styles'; type WithdrawFormProps = { pool: LiquidityPool; - slippageModalRef: RefObject; + overlappingModalRef: RefObject; onSuccess?: () => void; onSigning?: () => void; }; -const WithdrawForm = ({ pool, slippageModalRef, onSuccess, onSigning }: WithdrawFormProps): JSX.Element => { +const WithdrawForm = ({ pool, overlappingModalRef, onSuccess, onSigning }: WithdrawFormProps): JSX.Element => { const [slippage, setSlippage] = useState(0.1); const accountId = useAccountId(); @@ -57,32 +53,60 @@ const WithdrawForm = ({ pool, slippageModalRef, onSuccess, onSigning }: Withdraw transactionFee: TRANSACTION_FEE_AMOUNT }; + const getTransactionArgs = useCallback( + async (values: WithdrawLiquidityPoolFormData) => { + if (!accountId) return; + + try { + const amount = newMonetaryAmount(values[POOL_WITHDRAW_AMOUNT_FIELD] || 0, lpToken, true); + const deadline = await window.bridge.system.getFutureBlockNumber(AMM_DEADLINE_INTERVAL); + + return { amount, pool, slippage, deadline, accountId }; + } catch (error: any) { + transaction.reject(error); + } + }, + [accountId, lpToken, pool, slippage, transaction] + ); + const handleSubmit = async (data: WithdrawLiquidityPoolFormData) => { - if (!accountId) return; + const transactionData = await getTransactionArgs(data); - try { - const amount = newMonetaryAmount(data[WITHDRAW_LIQUIDITY_POOL_FIELD] || 0, lpToken, true); - const deadline = await window.bridge.system.getFutureBlockNumber(AMM_DEADLINE_INTERVAL); + if (!transactionData) return; - return transaction.execute(amount, pool, slippage, deadline, accountId); - } catch (error: any) { - transaction.reject(error); - } + const { accountId, amount, deadline, pool } = transactionData; + + return transaction.execute(amount, pool, slippage, deadline, accountId); }; + const initialValues = useMemo( + () => ({ + [POOL_WITHDRAW_AMOUNT_FIELD]: '', + [POOL_WITHDRAW_FEE_TOKEN_FIELD]: transaction.fee.defaultCurrency.ticker + }), + [transaction.fee.defaultCurrency.ticker] + ); + const form = useForm({ - initialValues: { withdraw: undefined }, + initialValues: initialValues, validationSchema: withdrawLiquidityPoolSchema(schemaParams), - onSubmit: handleSubmit + onSubmit: handleSubmit, + onComplete: async (values) => { + const transactionData = await getTransactionArgs(values); + + if (!transactionData) return; + + const { accountId, amount, deadline, pool } = transactionData; + + const feeTicker = values[POOL_WITHDRAW_FEE_TOKEN_FIELD]; + + return transaction.fee.setCurrency(feeTicker).estimate(amount, pool, slippage, deadline, accountId); + } }); - const lpTokenMonetaryAmount = newSafeMonetaryAmount( - form.values[WITHDRAW_LIQUIDITY_POOL_FIELD] || 0, - pool.lpToken, - true - ); + const lpTokenMonetaryAmount = newSafeMonetaryAmount(form.values[POOL_WITHDRAW_AMOUNT_FIELD] || 0, pool.lpToken, true); - const isBtnDisabled = isFormDisabled(form); + const isBtnDisabled = isTransactionFormDisabled(form, transaction.fee); const tickers = pool.pooledCurrencies.map((currency) => currency.currency.ticker); const poolName = ; @@ -103,7 +127,7 @@ const WithdrawForm = ({ pool, slippageModalRef, onSuccess, onSigning }: Withdraw return (
- setSlippage(slippage)} /> + setSlippage(slippage)} /> {poolName} @@ -116,29 +140,19 @@ const WithdrawForm = ({ pool, slippageModalRef, onSuccess, onSigning }: Withdraw balance={balance?.toString() || 0} humanBalance={balance?.toHuman() || 0} valueUSD={pooledAmountsUSD} - errorMessage={form.errors[WITHDRAW_LIQUIDITY_POOL_FIELD]} - {...form.getFieldProps(WITHDRAW_LIQUIDITY_POOL_FIELD)} + {...form.getFieldProps(POOL_WITHDRAW_AMOUNT_FIELD)} /> - - -
- Fees -
-
- {TRANSACTION_FEE_AMOUNT.toHuman()} {TRANSACTION_FEE_AMOUNT.currency.ticker} ( - {displayMonetaryAmountInUSDFormat( - TRANSACTION_FEE_AMOUNT, - getTokenPrice(prices, TRANSACTION_FEE_AMOUNT.currency.ticker)?.usd - )} - ) -
-
-
- - {t('amm.pools.remove_liquidity')} - + + + + {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 a817c120ff..9db4bed0a6 100644 --- a/src/pages/AMM/Swap/components/SwapForm/SwapCTA.tsx +++ b/src/pages/AMM/Swap/components/SwapForm/SwapCTA.tsx @@ -3,22 +3,20 @@ import { TFunction, useTranslation } from 'react-i18next'; import { CTAProps } from '@/component-library'; import { AuthCTA } from '@/components'; -import { - FormErrors, - SWAP_INPUT_AMOUNT_FIELD, - SWAP_INPUT_TOKEN_FIELD, - SWAP_OUTPUT_TOKEN_FIELD, - SwapFormData -} from '@/lib/form'; +import { SWAP_INPUT_AMOUNT_FIELD, SWAP_INPUT_TOKEN_FIELD, SWAP_OUTPUT_TOKEN_FIELD, useForm } from '@/lib/form'; import { SwapPair } from '@/types/swap'; +import { Transaction } from '@/utils/hooks/transaction'; +import { UseFeeEstimateResult } from '@/utils/hooks/transaction/types/hook'; +import { isTransactionFormDisabled } from '@/utils/hooks/transaction/utils/form'; const getProps = ( pair: SwapPair, trade: Trade | null | undefined, - errors: FormErrors, + form: ReturnType, + fee: UseFeeEstimateResult, t: TFunction ): Pick => { - const tickersError = errors[SWAP_INPUT_TOKEN_FIELD] || errors[SWAP_OUTPUT_TOKEN_FIELD]; + const tickersError = (form.errors[SWAP_INPUT_TOKEN_FIELD] || form.errors[SWAP_OUTPUT_TOKEN_FIELD]) as string; if (tickersError) { return { @@ -27,7 +25,7 @@ const getProps = ( }; } - const inputError = errors[SWAP_INPUT_AMOUNT_FIELD]; + const inputError = form.errors[SWAP_INPUT_AMOUNT_FIELD] as string; if (inputError) { return { @@ -45,20 +43,22 @@ const getProps = ( } return { - children: t('amm.swap') + children: t('amm.swap'), + disabled: isTransactionFormDisabled(form, fee) }; }; type SwapCTAProps = { pair: SwapPair; trade: Trade | null | undefined; - errors: FormErrors; + form: ReturnType; + fee: UseFeeEstimateResult; }; -const SwapCTA = ({ pair, trade, errors }: SwapCTAProps): JSX.Element | null => { +const SwapCTA = ({ pair, trade, form, fee }: SwapCTAProps): JSX.Element | null => { const { t } = useTranslation(); - const otherProps = getProps(pair, trade, errors, t); + const otherProps = getProps(pair, trade, form, fee, t); return ; }; diff --git a/src/pages/AMM/Swap/components/SwapForm/SwapDivider.tsx b/src/pages/AMM/Swap/components/SwapForm/SwapDivider.tsx index fa33bb2cd5..eb274de502 100644 --- a/src/pages/AMM/Swap/components/SwapForm/SwapDivider.tsx +++ b/src/pages/AMM/Swap/components/SwapForm/SwapDivider.tsx @@ -5,8 +5,9 @@ import { PressEvent } from '@react-types/shared'; import { useRef } from 'react'; import { ArrowsUpDown } from '@/assets/icons'; +import { Divider } from '@/component-library'; -import { StyledBackground, StyledCircle, StyledDivider, StyledWrapper } from './SwapForm.style'; +import { StyledBackground, StyledCircle, StyledWrapper } from './SwapForm.style'; type SwapDividerProps = { onPress: (e: PressEvent) => void; @@ -19,7 +20,7 @@ const SwapDivider = ({ onPress }: SwapDividerProps): JSX.Element | null => { return ( - + diff --git a/src/pages/AMM/Swap/components/SwapForm/SwapForm.style.tsx b/src/pages/AMM/Swap/components/SwapForm/SwapForm.style.tsx index 5a662b756d..b321bb79f1 100644 --- a/src/pages/AMM/Swap/components/SwapForm/SwapForm.style.tsx +++ b/src/pages/AMM/Swap/components/SwapForm/SwapForm.style.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { Divider, theme } from '@/component-library'; +import { theme } from '@/component-library'; type StyledCircleProps = { $isFocusVisible: boolean; @@ -17,7 +17,7 @@ const StyledCircle = styled.button` top: 50%; transform: translate(-50%, -50%); padding: ${theme.spacing.spacing2}; - background-color: var(--colors-token-input-end-adornment-bg); + background-color: var(--colors-border); border-radius: ${theme.rounded.full}; outline: ${({ $isFocusVisible }) => !$isFocusVisible && 'none'}; transition: transform ${theme.transition.duration.duration150}ms ease-in; @@ -37,8 +37,4 @@ const StyledBackground = styled.div` background-color: ${theme.colors.bgPrimary}; `; -const StyledDivider = styled(Divider)` - background-color: var(--colors-token-input-end-adornment-bg); -`; - -export { StyledBackground, StyledCircle, StyledDivider, StyledWrapper }; +export { StyledBackground, StyledCircle, StyledWrapper }; diff --git a/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx b/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx index 25753b06ca..99d18a2385 100644 --- a/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx +++ b/src/pages/AMM/Swap/components/SwapForm/SwapForm.tsx @@ -1,16 +1,18 @@ import { CurrencyExt, LiquidityPool, newMonetaryAmount, Trade } from '@interlay/interbtc-api'; import { mergeProps } from '@react-aria/utils'; import Big from 'big.js'; -import { ChangeEventHandler, Key, useEffect, useMemo, useState } from 'react'; +import { ChangeEventHandler, Key, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { useDebounce, useInterval } from 'react-use'; import { StoreType } from '@/common/types/util.types'; -import { convertMonetaryAmountToValueInUSD, formatUSD, newSafeMonetaryAmount } from '@/common/utils/utils'; -import { Card, CardProps, Divider, Flex, H1, TokenInput, TokenSelectProps } from '@/component-library'; +import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/common/utils/utils'; +import { Card, CardProps, Divider, Flex, H1, TokenInput } from '@/component-library'; +import { TransactionFeeDetails } from '@/components'; import { GOVERNANCE_TOKEN, TRANSACTION_FEE_AMOUNT } from '@/config/relay-chains'; import { + SWAP_FEE_TOKEN_FIELD, SWAP_INPUT_AMOUNT_FIELD, SWAP_INPUT_TOKEN_FIELD, SWAP_OUTPUT_TOKEN_FIELD, @@ -28,6 +30,7 @@ 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 { useSelectCurrency } from '@/utils/hooks/use-select-currency'; import { PriceImpactModal } from '../PriceImpactModal'; import { SwapInfo } from '../SwapInfo'; @@ -110,7 +113,7 @@ const SwapForm = ({ const { bridgeLoaded } = useSelector((state: StoreType) => state.general); const { getCurrencyFromTicker } = useGetCurrencies(bridgeLoaded); const { data: balances, getBalance, getAvailableBalance } = useGetBalances(); - const { data: currencies } = useGetCurrencies(bridgeLoaded); + const selectCurrency = useSelectCurrency(); const transaction = useTransaction(Transaction.AMM_SWAP, { onSigning: () => { @@ -150,17 +153,35 @@ const SwapForm = ({ transactionFee: TRANSACTION_FEE_AMOUNT }; + const getTransactionArgs = useCallback( + async (trade: Trade | null | undefined) => { + if (!trade || !accountId) return; + + try { + const minimumAmountOut = trade.getMinimumOutputAmount(slippage); + const deadline = await window.bridge.system.getFutureBlockNumber(30 * 60); + + return { + trade, + minimumAmountOut, + accountId, + deadline + }; + } catch (error: any) { + transaction.reject(error); + } + }, + [accountId, slippage, transaction] + ); + const handleSwap = async () => { - if (!trade || !accountId) return; + const transactionData = await getTransactionArgs(trade); - try { - const minimumAmountOut = trade.getMinimumOutputAmount(slippage); - const deadline = await window.bridge.system.getFutureBlockNumber(30 * 60); + if (!transactionData) return; - return transaction.execute(trade, minimumAmountOut, accountId, deadline); - } catch (error: any) { - transaction.reject(error); - } + const { accountId, deadline, minimumAmountOut, trade: tradeData } = transactionData; + + transaction.execute(tradeData, minimumAmountOut, accountId, deadline); }; const handleSubmit = async (values: SwapFormData) => { @@ -180,7 +201,8 @@ const SwapForm = ({ () => ({ [SWAP_INPUT_AMOUNT_FIELD]: '', [SWAP_INPUT_TOKEN_FIELD]: pair.input?.ticker || '', - [SWAP_OUTPUT_TOKEN_FIELD]: pair.output?.ticker || '' + [SWAP_OUTPUT_TOKEN_FIELD]: pair.output?.ticker || '', + [SWAP_FEE_TOKEN_FIELD]: transaction.fee.defaultCurrency.ticker }), // eslint-disable-next-line react-hooks/exhaustive-deps [] @@ -212,18 +234,31 @@ const SwapForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [pair]); - const handleChangeInput: ChangeEventHandler = (e) => { - setInputAmount(e.target.value); - setTrade(undefined); - }; + const feeToken = form.values[SWAP_FEE_TOKEN_FIELD]; - const handlePairChange = (pair: SwapPair) => { - onChangePair(pair); - setTrade(undefined); - }; + useEffect(() => { + const estimateFee = async () => { + const transactionData = await getTransactionArgs(trade); + + if (!transactionData) return; + + const { accountId, deadline, minimumAmountOut, trade: tradeData } = transactionData; + + transaction.fee.setCurrency(feeToken).estimate(tradeData, minimumAmountOut, accountId, deadline); + }; + + if (!trade) return; + + estimateFee(); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [trade, feeToken]); + + const handleChangeInput: ChangeEventHandler = (e) => setInputAmount(e.target.value); + + const handlePairChange = (pair: SwapPair) => onChangePair(pair); const handleTickerChange = (ticker: string, name: string) => { - form.setFieldValue(name, ticker, true); const currency = getCurrencyFromTicker(ticker); const newPair = getPairChange(pair, currency, name); @@ -246,24 +281,7 @@ const SwapForm = ({ form.values[SWAP_INPUT_AMOUNT_FIELD] ); - const selectItems: TokenSelectProps['items'] = useMemo( - () => - currencies - ?.filter((currency) => pooledTickers.has(currency.ticker)) - .map((currency) => { - const balance = getAvailableBalance(currency.ticker); - const balanceUSD = balance - ? convertMonetaryAmountToValueInUSD(balance, getTokenPrice(prices, currency.ticker)?.usd) - : 0; - - return { - balance: balance?.toHuman() || 0, - balanceUSD: formatUSD(balanceUSD || 0, { compact: true }), - value: currency.ticker - }; - }) || [], - [currencies, getAvailableBalance, pooledTickers, prices] - ); + const selectItems = selectCurrency.items.filter((tokenData) => pooledTickers.has(tokenData.value)); const { poolImpact, marketPrice } = getPoolPriceImpact(trade, inputAmountUSD, outputAmountUSD); const priceImpact = (marketPrice || poolImpact).toNumber(); @@ -286,11 +304,11 @@ const SwapForm = ({ balance={inputBalance?.toString() || 0} humanBalance={inputBalance?.toHuman() || 0} valueUSD={inputAmountUSD} - selectProps={mergeProps(form.getFieldProps(SWAP_INPUT_TOKEN_FIELD, false), { + selectProps={mergeProps(form.getFieldProps(SWAP_INPUT_TOKEN_FIELD, true), { onSelectionChange: (ticker: Key) => handleTickerChange(ticker as string, SWAP_INPUT_TOKEN_FIELD), items: selectItems })} - {...mergeProps(form.getFieldProps(SWAP_INPUT_AMOUNT_FIELD, false), { onChange: handleChangeInput })} + {...mergeProps(form.getFieldProps(SWAP_INPUT_AMOUNT_FIELD, true), { onChange: handleChangeInput })} /> handleTickerChange(ticker as string, SWAP_OUTPUT_TOKEN_FIELD), items: selectItems })} />
- {trade && } - + + {trade && } + + + diff --git a/src/pages/AMM/Swap/components/SwapInfo/SwapInfo.style.tsx b/src/pages/AMM/Swap/components/SwapInfo/SwapInfo.style.tsx index 2fc50a87cd..65421f1639 100644 --- a/src/pages/AMM/Swap/components/SwapInfo/SwapInfo.style.tsx +++ b/src/pages/AMM/Swap/components/SwapInfo/SwapInfo.style.tsx @@ -1,6 +1,7 @@ import styled from 'styled-components'; import { theme } from '@/component-library'; +import { TransactionDetails } from '@/components'; const StyledCard = styled.div` background-color: ${theme.card.bg.secondary}; @@ -8,4 +9,8 @@ const StyledCard = styled.div` border-radius: ${theme.rounded.md}; `; -export { StyledCard }; +const StyledTransactionDetails = styled(TransactionDetails)` + padding: 0; +`; + +export { StyledCard, StyledTransactionDetails }; diff --git a/src/pages/AMM/Swap/components/SwapInfo/SwapInfo.tsx b/src/pages/AMM/Swap/components/SwapInfo/SwapInfo.tsx index 23e343b8ae..15943eaa32 100644 --- a/src/pages/AMM/Swap/components/SwapInfo/SwapInfo.tsx +++ b/src/pages/AMM/Swap/components/SwapInfo/SwapInfo.tsx @@ -1,12 +1,10 @@ import { Trade } from '@interlay/interbtc-api'; -import { displayMonetaryAmountInUSDFormat, formatPercentage } from '@/common/utils/utils'; -import { Accordion, AccordionItem, Dd, Dl, DlGroup, Dt } from '@/component-library'; -import { TRANSACTION_FEE_AMOUNT } from '@/config/relay-chains'; -import { getTokenPrice } from '@/utils/helpers/prices'; -import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { formatPercentage } from '@/common/utils/utils'; +import { Accordion, AccordionItem } from '@/component-library'; +import { TransactionDetailsDd, TransactionDetailsDt, TransactionDetailsGroup } from '@/components'; -import { StyledCard } from './SwapInfo.style'; +import { StyledTransactionDetails } from './SwapInfo.style'; type SwapInfoProps = { trade: Trade; @@ -14,8 +12,6 @@ type SwapInfoProps = { }; const SwapInfo = ({ trade, slippage }: SwapInfoProps): JSX.Element | null => { - const prices = useGetPrices(); - const { inputAmount, outputAmount, executionPrice, priceImpact } = trade; const title = `1 ${inputAmount.currency.ticker} = ${executionPrice.toHuman()} ${outputAmount.currency.ticker}`; @@ -23,50 +19,29 @@ const SwapInfo = ({ trade, slippage }: SwapInfoProps): JSX.Element | null => { const minimumReceived = outputAmount.sub(outputAmount.mul(slippage).div(100)); return ( - - + + -
- -
- Expected Output -
-
- {outputAmount.toHuman()} {outputAmount.currency.ticker} -
-
- -
- Minimum Received -
-
- {minimumReceived.toHuman()} {outputAmount.currency.ticker} -
-
- -
- Price Impact -
- {/* TODO: handle small percentages */} -
{formatPercentage(priceImpact.toNumber())}
-
- -
- Fees -
-
- {TRANSACTION_FEE_AMOUNT.toHuman()} {TRANSACTION_FEE_AMOUNT.currency.ticker} ( - {displayMonetaryAmountInUSDFormat( - TRANSACTION_FEE_AMOUNT, - getTokenPrice(prices, TRANSACTION_FEE_AMOUNT.currency.ticker)?.usd - )} - ) -
-
-
+ + Expected Output + + {outputAmount.toHuman()} {outputAmount.currency.ticker} + + + + Minimum Received + + {minimumReceived.toHuman()} {outputAmount.currency.ticker} + + + + Price Impact + {/* TODO: handle small percentages */} + {formatPercentage(priceImpact.toNumber())} +
-
+ ); }; diff --git a/src/pages/Actions/Actions/components/ManualIssueExecutionActionsTable/ManualIssueExecutionActionsTable.tsx b/src/pages/Actions/Actions/components/ManualIssueExecutionActionsTable/ManualIssueExecutionActionsTable.tsx index 8ab0ffc851..5b4c48c85b 100644 --- a/src/pages/Actions/Actions/components/ManualIssueExecutionActionsTable/ManualIssueExecutionActionsTable.tsx +++ b/src/pages/Actions/Actions/components/ManualIssueExecutionActionsTable/ManualIssueExecutionActionsTable.tsx @@ -80,7 +80,7 @@ const ManualIssueExecutionActionsTable = (props: ManualIssueExecutionActionsTabl [ManualIssueExecutionActionsTableKeys.Action]: ( { + return ; +}; + +export default withErrorBoundary(Bridge, { + FallbackComponent: ErrorFallback, + onReset: () => { + window.location.reload(); + } +}); diff --git a/src/pages/Bridge/BridgeOverview/BridgeOverview.styles.tsx b/src/pages/Bridge/BridgeOverview/BridgeOverview.styles.tsx new file mode 100644 index 0000000000..34d264c703 --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/BridgeOverview.styles.tsx @@ -0,0 +1,19 @@ +import styled from 'styled-components'; + +import { Card, Flex, theme } from '@/component-library'; + +const StyledWrapper = styled(Flex)` + max-width: 540px; + width: 100%; + margin: 0 auto; +`; + +const StyledCard = styled(Card)` + width: 100%; +`; + +const StyledFormWrapper = styled.div` + margin-top: ${theme.spacing.spacing8}; +`; + +export { StyledCard, StyledFormWrapper, StyledWrapper }; diff --git a/src/pages/Bridge/BridgeOverview/BridgeOverview.tsx b/src/pages/Bridge/BridgeOverview/BridgeOverview.tsx new file mode 100644 index 0000000000..d7b3ae546b --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/BridgeOverview.tsx @@ -0,0 +1,62 @@ +import { Flex, Tabs, TabsItem } from '@/component-library'; +import FullLoadingSpinner from '@/legacy-components/FullLoadingSpinner'; +import MainContainer from '@/parts/MainContainer'; +import { BridgeActions } from '@/types/bridge'; +import { useGetIssueData } from '@/utils/hooks/api/bridge/use-get-issue-data'; +import { useGetIssueRequestLimit } from '@/utils/hooks/api/bridge/use-get-issue-request-limits'; +import { useGetMaxBurnableTokens } from '@/utils/hooks/api/bridge/use-get-max-burnable-tokens'; +import { useGetRedeemData } from '@/utils/hooks/api/bridge/use-get-redeem-data'; +import { useTabPageLocation } from '@/utils/hooks/use-tab-page-location'; + +import { StyledCard, StyledFormWrapper, StyledWrapper } from './BridgeOverview.styles'; +import { IssueForm, LegacyBurnForm, LegacyTransactions, RedeemForm } from './components'; + +const BridgeOverview = (): JSX.Element => { + const { tabsProps } = useTabPageLocation(); + + const { defaultSelectedKey } = tabsProps; + + const { data: issueRequestLimit } = useGetIssueRequestLimit(); + const { data: maxBurnableTokensData } = useGetMaxBurnableTokens(); + const { data: issueData } = useGetIssueData(); + const { data: redeemData } = useGetRedeemData(); + + // Only show the loading bar if the tab needed data is still loading + const isIssueLoading = defaultSelectedKey === BridgeActions.ISSUE && issueData === undefined; + const isRedeemLoading = defaultSelectedKey === BridgeActions.REDEEM && redeemData === undefined; + + if (issueRequestLimit === undefined || isIssueLoading || isRedeemLoading) { + return ; + } + + return ( + + + + + + + + {issueData && } + + + + {redeemData && } + + {maxBurnableTokensData?.hasBurnableTokens && ( + + + + + + )} + + + + + + + ); +}; + +export default BridgeOverview; diff --git a/src/pages/Bridge/BridgeOverview/components/IssueForm/IssueForm.styles.tsx b/src/pages/Bridge/BridgeOverview/components/IssueForm/IssueForm.styles.tsx new file mode 100644 index 0000000000..5c1eae4f4c --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/IssueForm/IssueForm.styles.tsx @@ -0,0 +1,23 @@ +import styled from 'styled-components'; + +import { InformationCircle } from '@/assets/icons'; +import { Dl, Switch, theme } from '@/component-library'; + +const StyledDl = styled(Dl)` + background-color: ${theme.card.bg.secondary}; + padding: ${theme.spacing.spacing4}; + font-size: ${theme.text.xs}; + border-radius: ${theme.rounded.rg}; +`; + +const StyledInformationCircle = styled(InformationCircle)` + margin-left: ${theme.spacing.spacing2}; + vertical-align: text-top; +`; + +const StyledSwitch = styled(Switch)` + flex-direction: row-reverse; + justify-content: space-between; +`; + +export { StyledDl, StyledInformationCircle, StyledSwitch }; diff --git a/src/pages/Bridge/BridgeOverview/components/IssueForm/IssueForm.tsx b/src/pages/Bridge/BridgeOverview/components/IssueForm/IssueForm.tsx new file mode 100644 index 0000000000..ceaaa0852b --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/IssueForm/IssueForm.tsx @@ -0,0 +1,289 @@ +import { CurrencyExt, getIssueRequestsFromExtrinsicResult, isCurrencyEqual, Issue } from '@interlay/interbtc-api'; +import { IssueLimits } from '@interlay/interbtc-api/build/src/parachain/issue'; +import { BitcoinAmount, MonetaryAmount } from '@interlay/monetary-js'; +import { mergeProps } from '@react-aria/utils'; +import { ChangeEvent, Key, useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDebounce } from 'react-use'; + +import { convertMonetaryAmountToValueInUSD, getRandomArrayElement, safeBitcoinAmount } from '@/common/utils/utils'; +import { Flex, TokenInput } from '@/component-library'; +import { AuthCTA } from '@/components'; +import { GOVERNANCE_TOKEN, WRAPPED_TOKEN } from '@/config/relay-chains'; +import { + BRIDGE_ISSUE_AMOUNT_FIELD, + BRIDGE_ISSUE_CUSTOM_VAULT_FIELD, + BRIDGE_ISSUE_CUSTOM_VAULT_SWITCH, + BRIDGE_ISSUE_FEE_TOKEN, + BRIDGE_ISSUE_GRIEFING_COLLATERAL_TOKEN, + BridgeIssueFormData, + bridgeIssueSchema, + useForm +} from '@/lib/form'; +import { BridgeActions } from '@/types/bridge'; +import { getTokenPrice } from '@/utils/helpers/prices'; +import { IssueData, useGetIssueData } from '@/utils/hooks/api/bridge/use-get-issue-data'; +import { BridgeVaultData, useGetVaults } from '@/utils/hooks/api/bridge/use-get-vaults'; +import { useGetBalances } from '@/utils/hooks/api/tokens/use-get-balances'; +import { useGetCurrencies } from '@/utils/hooks/api/use-get-currencies'; +import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; +import { TransactionArgs } from '@/utils/hooks/transaction/types'; +import { isTransactionFormDisabled } from '@/utils/hooks/transaction/utils/form'; + +import { LegacyIssueModal } from '../LegacyIssueModal'; +import { RequestLimitsCard } from '../RequestLimitsCard'; +import { SelectVaultCard } from '../SelectVaultCard'; +import { TransactionDetails } from '../TransactionDetails'; + +type IssueFormProps = { requestLimits: IssueLimits } & IssueData; + +const IssueForm = ({ requestLimits, dustValue, issueFee }: IssueFormProps): JSX.Element => { + const { t } = useTranslation(); + const prices = useGetPrices(); + const { getBalance } = useGetBalances(); + const { getSecurityDeposit } = useGetIssueData(); + const { getCurrencyFromTicker, isLoading: isLoadingCurrencies } = useGetCurrencies(true); + + const [issueRequest, setIssueRequest] = useState(); + + const [amount, setAmount] = useState(); + const [debouncedAmount, setDebouncedAmount] = useState(); + + useDebounce(() => setDebouncedAmount(amount), 500, [amount]); + + const [selectedVault, setSelectedVault] = useState(); + + const { data: vaultsData, getAvailableVaults } = useGetVaults(BridgeActions.ISSUE); + + const debouncedMonetaryAmount = safeBitcoinAmount(debouncedAmount || 0); + const availableVaults = getAvailableVaults(debouncedMonetaryAmount); + const vaults = availableVaults?.length ? availableVaults : vaultsData?.list; + + const transaction = useTransaction(Transaction.ISSUE_REQUEST, { + showSuccessModal: false, + onSuccess: async (result) => { + try { + const [issueRequest] = await getIssueRequestsFromExtrinsicResult(window.bridge, result.data); + + setIssueRequest(issueRequest); + } catch (e: any) { + transaction.reject(e); + } + + setAmount(undefined); + setDebouncedAmount(undefined); + form.resetForm(); + } + }); + + const currentRequestLimit = selectedVault ? selectedVault.amount : requestLimits.singleVaultMaxIssuable; + + const transferAmountSchemaParams = { + maxAmount: currentRequestLimit, + minAmount: dustValue + }; + + const handleSubmit = async (values: BridgeIssueFormData) => { + const args = getTransactionArgs(values); + + if (!args) return; + + transaction.execute(...args); + }; + + const getTransactionArgs = useCallback( + (values: BridgeIssueFormData): TransactionArgs | undefined => { + const amount = values[BRIDGE_ISSUE_AMOUNT_FIELD]; + const griefingCollateralCurrencyTicker = values[BRIDGE_ISSUE_GRIEFING_COLLATERAL_TOKEN]; + if (!vaultsData || !amount || griefingCollateralCurrencyTicker === undefined || isLoadingCurrencies) return; + + const monetaryAmount = new BitcoinAmount(amount); + + const availableVaults = getAvailableVaults(monetaryAmount); + + if (!availableVaults) return; + + const vaultId = values[BRIDGE_ISSUE_CUSTOM_VAULT_FIELD]; + + let vault: BridgeVaultData | undefined; + + // If custom vault was select, try to find it in the data + if (vaultId) { + vault = availableVaults.find((item) => item.id === vaultId); + } + + // If no vault provided nor the custom vault wasn't found (unlikely), choose random vault + if (!vault) { + vault = getRandomArrayElement(availableVaults); + } + + const griefingCollateralCurrency = getCurrencyFromTicker(griefingCollateralCurrencyTicker); + return [ + monetaryAmount, + vault.vaultId.accountId, + vault.collateralCurrency, + false, + vaultsData.map, + griefingCollateralCurrency + ]; + }, + [getAvailableVaults, getCurrencyFromTicker, isLoadingCurrencies, vaultsData] + ); + + const form = useForm({ + initialValues: { + [BRIDGE_ISSUE_AMOUNT_FIELD]: '', + [BRIDGE_ISSUE_CUSTOM_VAULT_FIELD]: '', + [BRIDGE_ISSUE_CUSTOM_VAULT_SWITCH]: false, + [BRIDGE_ISSUE_GRIEFING_COLLATERAL_TOKEN]: GOVERNANCE_TOKEN.ticker, + [BRIDGE_ISSUE_FEE_TOKEN]: transaction.fee.defaultCurrency.ticker + }, + validateOnChange: true, + validationSchema: bridgeIssueSchema({ [BRIDGE_ISSUE_AMOUNT_FIELD]: transferAmountSchemaParams }), + onSubmit: handleSubmit, + hideErrorMessages: transaction.isLoading, + onComplete: (values) => { + const args = getTransactionArgs(values); + + if (!args) return; + + const feeTicker = values[BRIDGE_ISSUE_FEE_TOKEN]; + + transaction.fee.setCurrency(feeTicker).estimate(...args); + } + }); + + const handleToggleCustomVault = (e: ChangeEvent) => { + if (!e.target.checked) { + form.setFieldTouched(BRIDGE_ISSUE_CUSTOM_VAULT_FIELD, false, true); + form.setFieldValue(BRIDGE_ISSUE_CUSTOM_VAULT_FIELD, '', true); + + setSelectedVault(undefined); + } + }; + + const handleChangeIssueAmount = (e: ChangeEvent) => setAmount(e.target.value); + + const handleVaultSelectionChange = (key: Key) => { + if (!vaults) return; + + const vault = vaults.find((item) => item.id === key); + + setSelectedVault(vault); + }; + + const monetaryAmount = safeBitcoinAmount(amount || 0); + const monetaryAmountUSD = monetaryAmount + ? convertMonetaryAmountToValueInUSD(monetaryAmount, getTokenPrice(prices, monetaryAmount.currency.ticker)?.usd) || 0 + : 0; + + const bridgeFee = monetaryAmount.mul(issueFee.toBig()); + + const griefingCollateralTicker = form.values[BRIDGE_ISSUE_GRIEFING_COLLATERAL_TOKEN]; + + const [securityDeposit, setSecurityDeposit] = useState>(); + useEffect(() => { + const computeSecurityDeposit = async () => { + const btcAmount = safeBitcoinAmount(amount || 0); + const griefingCollateralTicker = form.values[BRIDGE_ISSUE_GRIEFING_COLLATERAL_TOKEN]; + const deposit = await getSecurityDeposit(btcAmount, griefingCollateralTicker); + setSecurityDeposit(deposit); + }; + + computeSecurityDeposit(); + }, [amount, form.values, setSecurityDeposit, getSecurityDeposit]); + + const totalAmount = monetaryAmount.gte(bridgeFee) ? monetaryAmount.sub(bridgeFee) : new BitcoinAmount(0); + const totalAmountUSD = totalAmount + ? convertMonetaryAmountToValueInUSD(totalAmount, getTokenPrice(prices, totalAmount.currency.ticker)?.usd) || 0 + : 0; + + const isSelectingVault = form.values[BRIDGE_ISSUE_CUSTOM_VAULT_SWITCH]; + + const isBtnDisabled = isTransactionFormDisabled(form, transaction.fee); + + const griefingCollateralCurrencyBalance = griefingCollateralTicker + ? getBalance(griefingCollateralTicker)?.free + : undefined; + + const hasEnoughGriefingCollateralBalance = useMemo(() => { + if ( + securityDeposit === undefined || + griefingCollateralCurrencyBalance === undefined || + !isCurrencyEqual(securityDeposit.currency, griefingCollateralCurrencyBalance.currency) + ) { + return false; + } + return griefingCollateralCurrencyBalance.gte(securityDeposit); + }, [securityDeposit, griefingCollateralCurrencyBalance]); + + return ( + <> + +
+ + + + + + + + + + {hasEnoughGriefingCollateralBalance + ? t('issue') + : t('insufficient_token_balance', { token: griefingCollateralTicker })} + + + +
+
+ {issueRequest && ( + setIssueRequest(undefined)} request={issueRequest} /> + )} + + ); +}; + +export { IssueForm }; +export type { IssueFormProps }; diff --git a/src/pages/Bridge/BridgeOverview/components/IssueForm/index.tsx b/src/pages/Bridge/BridgeOverview/components/IssueForm/index.tsx new file mode 100644 index 0000000000..0064135a8e --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/IssueForm/index.tsx @@ -0,0 +1,2 @@ +export type { IssueFormProps } from './IssueForm'; +export { IssueForm } from './IssueForm'; diff --git a/src/pages/Bridge/BurnForm/index.tsx b/src/pages/Bridge/BridgeOverview/components/LegacyBurnForm/LegacyBurnForm.tsx similarity index 97% rename from src/pages/Bridge/BurnForm/index.tsx rename to src/pages/Bridge/BridgeOverview/components/LegacyBurnForm/LegacyBurnForm.tsx index 4699f19aa7..d64c39daf9 100644 --- a/src/pages/Bridge/BurnForm/index.tsx +++ b/src/pages/Bridge/BridgeOverview/components/LegacyBurnForm/LegacyBurnForm.tsx @@ -2,7 +2,7 @@ import { CollateralCurrencyExt, CurrencyExt, newMonetaryAmount } from '@interlay import { Bitcoin, BitcoinAmount, Currency, ExchangeRate, MonetaryAmount } from '@interlay/monetary-js'; import clsx from 'clsx'; import * as React from 'react'; -import { useErrorHandler, withErrorBoundary } from 'react-error-boundary'; +import { useErrorHandler } from 'react-error-boundary'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; @@ -13,7 +13,6 @@ import { CoinIcon } from '@/component-library'; 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 FormTitle from '@/legacy-components/FormTitle'; import Hr2 from '@/legacy-components/hrs/Hr2'; import PriceInfo from '@/legacy-components/PriceInfo'; @@ -41,7 +40,7 @@ type BurnableCollateral = { burnRate: ExchangeRate; }; -const BurnForm = (): JSX.Element | null => { +const LegacyBurnForm = (): JSX.Element | null => { const { t } = useTranslation(); const prices = useGetPrices(); @@ -296,9 +295,4 @@ const BurnForm = (): JSX.Element | null => { return null; }; -export default withErrorBoundary(BurnForm, { - FallbackComponent: ErrorFallback, - onReset: () => { - window.location.reload(); - } -}); +export { LegacyBurnForm }; diff --git a/src/pages/Bridge/BridgeOverview/components/LegacyBurnForm/index.tsx b/src/pages/Bridge/BridgeOverview/components/LegacyBurnForm/index.tsx new file mode 100644 index 0000000000..ab69591fc2 --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/LegacyBurnForm/index.tsx @@ -0,0 +1 @@ +export { LegacyBurnForm } from './LegacyBurnForm'; diff --git a/src/pages/Bridge/BridgeOverview/components/LegacyIssueModal/LegacyIssueModal.tsx b/src/pages/Bridge/BridgeOverview/components/LegacyIssueModal/LegacyIssueModal.tsx new file mode 100644 index 0000000000..b9def63cd5 --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/LegacyIssueModal/LegacyIssueModal.tsx @@ -0,0 +1,50 @@ +import { Issue } from '@interlay/interbtc-api'; +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next'; + +import { Modal, ModalBody, ModalFooter } from '@/component-library'; +import InterlayDefaultContainedButton from '@/legacy-components/buttons/InterlayDefaultContainedButton'; +import BTCPaymentPendingStatusUI from '@/legacy-components/IssueUI/BTCPaymentPendingStatusUI'; +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'; + +const queryString = require('query-string'); + +interface CustomProps { + request: Issue; +} + +const LegacyIssueModal = ({ open, onClose, request }: CustomProps & Omit): JSX.Element => { + const { t } = useTranslation(); + + return ( + + +
+

+ {t('issue_page.deposit')} +

+ +
+
+ + + + {t('issue_page.i_have_made_the_payment')} + + {' '} + +
+ ); +}; + +export { LegacyIssueModal }; diff --git a/src/pages/Bridge/BridgeOverview/components/LegacyIssueModal/index.tsx b/src/pages/Bridge/BridgeOverview/components/LegacyIssueModal/index.tsx new file mode 100644 index 0000000000..30ac7029c7 --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/LegacyIssueModal/index.tsx @@ -0,0 +1 @@ +export { LegacyIssueModal } from './LegacyIssueModal'; diff --git a/src/pages/Bridge/RedeemForm/SubmittedRedeemRequestModal/index.tsx b/src/pages/Bridge/BridgeOverview/components/LegacyRedeemModal/LegacyRedeemModal.tsx similarity index 95% rename from src/pages/Bridge/RedeemForm/SubmittedRedeemRequestModal/index.tsx rename to src/pages/Bridge/BridgeOverview/components/LegacyRedeemModal/LegacyRedeemModal.tsx index 8419404b6d..d22d63372a 100644 --- a/src/pages/Bridge/RedeemForm/SubmittedRedeemRequestModal/index.tsx +++ b/src/pages/Bridge/BridgeOverview/components/LegacyRedeemModal/LegacyRedeemModal.tsx @@ -24,11 +24,7 @@ interface CustomProps { request: Redeem; } -const SubmittedRedeemRequestModal = ({ - open, - onClose, - request -}: CustomProps & Omit): JSX.Element => { +const LegacyRedeemModal = ({ open, onClose, request }: CustomProps & Omit): JSX.Element => { const { t } = useTranslation(); const prices = useGetPrices(); @@ -99,7 +95,7 @@ const SubmittedRedeemRequestModal = ({ ): JSX.Element => { - const { t } = useTranslation(); - return ( - {t('issue_page.request', { id: request.id })} diff --git a/src/pages/Transactions/IssueRequestsTable/index.tsx b/src/pages/Bridge/BridgeOverview/components/LegacyTransactions/IssueRequestsTable/index.tsx similarity index 81% rename from src/pages/Transactions/IssueRequestsTable/index.tsx rename to src/pages/Bridge/BridgeOverview/components/LegacyTransactions/IssueRequestsTable/index.tsx index fdb72c9aa5..03d7e84fe0 100644 --- a/src/pages/Transactions/IssueRequestsTable/index.tsx +++ b/src/pages/Bridge/BridgeOverview/components/LegacyTransactions/IssueRequestsTable/index.tsx @@ -225,64 +225,68 @@ const IssueRequestsTable = (): JSX.Element => { <> {t('issue_page.issue_requests')} - - - {/* TODO: should type properly */} - {headerGroups.map((headerGroup: any) => ( - // eslint-disable-next-line react/jsx-key - - {/* TODO: should type properly */} - {headerGroup.headers.map((column: any) => ( - // eslint-disable-next-line react/jsx-key - - {column.render('Header')} - - ))} - - ))} - - - {/* TODO: should type properly */} - {rows.map((row: any) => { - prepareRow(row); - - const { className: rowClassName, ...restRowProps } = row.getRowProps(); - - return ( + {issueRequests?.length ? ( + + + {/* TODO: should type properly */} + {headerGroups.map((headerGroup: any) => ( // eslint-disable-next-line react/jsx-key - + {/* TODO: should type properly */} - {row.cells.map((cell: any) => { - return ( - // eslint-disable-next-line react/jsx-key - - {cell.render('Cell')} - - ); - })} + {headerGroup.headers.map((column: any) => ( + // eslint-disable-next-line react/jsx-key + + {column.render('Header')} + + ))} - ); - })} - - + ))} + + + {/* TODO: should type properly */} + {rows.map((row: any) => { + prepareRow(row); + + const { className: rowClassName, ...restRowProps } = row.getRowProps(); + + return ( + // eslint-disable-next-line react/jsx-key + + {/* TODO: should type properly */} + {row.cells.map((cell: any) => { + return ( + // eslint-disable-next-line react/jsx-key + + {cell.render('Cell')} + + ); + })} + + ); + })} + + + ) : ( +

{t('empty_data')}

+ )} {pageCount > 0 && (
): JSX.Element | null => { - const { t } = useTranslation(); - return ( - {t('issue_page.request', { id: request.id })} diff --git a/src/pages/Transactions/RedeemRequestsTable/index.tsx b/src/pages/Bridge/BridgeOverview/components/LegacyTransactions/RedeemRequestsTable/index.tsx similarity index 84% rename from src/pages/Transactions/RedeemRequestsTable/index.tsx rename to src/pages/Bridge/BridgeOverview/components/LegacyTransactions/RedeemRequestsTable/index.tsx index e8da48b299..d67f055f8b 100644 --- a/src/pages/Transactions/RedeemRequestsTable/index.tsx +++ b/src/pages/Bridge/BridgeOverview/components/LegacyTransactions/RedeemRequestsTable/index.tsx @@ -292,64 +292,69 @@ const RedeemRequestsTable = (): JSX.Element => { <> {t('redeem_requests')} - - - {/* TODO: should type properly */} - {headerGroups.map((headerGroup: any) => ( - // eslint-disable-next-line react/jsx-key - - {/* TODO: should type properly */} - {headerGroup.headers.map((column: any) => ( - // eslint-disable-next-line react/jsx-key - - {column.render('Header')} - - ))} - - ))} - - - {/* TODO: should type properly */} - {rows.map((row: any) => { - prepareRow(row); - - const { className: rowClassName, ...restRowProps } = row.getRowProps(); - - return ( + {redeemRequests.length ? ( + + + {/* TODO: should type properly */} + {headerGroups.map((headerGroup: any) => ( // eslint-disable-next-line react/jsx-key - + {/* TODO: should type properly */} - {row.cells.map((cell: any) => { - return ( - // eslint-disable-next-line react/jsx-key - - {cell.render('Cell')} - - ); - })} + {headerGroup.headers.map((column: any) => ( + // eslint-disable-next-line react/jsx-key + + {column.render('Header')} + + ))} - ); - })} - - + ))} + + + {/* TODO: should type properly */} + {rows.map((row: any) => { + prepareRow(row); + + const { className: rowClassName, ...restRowProps } = row.getRowProps(); + + return ( + // eslint-disable-next-line react/jsx-key + + {/* TODO: should type properly */} + {row.cells.map((cell: any) => { + return ( + // eslint-disable-next-line react/jsx-key + + {cell.render('Cell')} + + ); + })} + + ); + })} + + + ) : ( +

{t('empty_data')}

+ )} + {pageCount > 0 && (
{ +const LegacyTransactions = (): JSX.Element => { const { selectedAccount } = useSubstrateSecureState(); const { t } = useTranslation(); return ( - + + + {selectedAccount && ( {t('view_all_transactions_on_subscan')} )} - - - + ); }; -export default Transactions; +export { LegacyTransactions }; diff --git a/src/pages/Bridge/BridgeOverview/components/RedeemForm/PremiumRedeemCard.tsx b/src/pages/Bridge/BridgeOverview/components/RedeemForm/PremiumRedeemCard.tsx new file mode 100644 index 0000000000..9dd28db4ab --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/RedeemForm/PremiumRedeemCard.tsx @@ -0,0 +1,33 @@ +import { useTranslation } from 'react-i18next'; + +import { Card, SwitchProps, Tooltip } from '@/component-library'; + +import { StyledInformationCircle, StyledSwitch } from './RedeemForm.styles'; + +type PremiumRedeemCardProps = { isPremiumRedeem?: boolean; switchProps: SwitchProps }; + +const PremiumRedeemCard = ({ isPremiumRedeem, switchProps }: PremiumRedeemCardProps): JSX.Element => { + const { t } = useTranslation(); + + return ( + + + + {t('bridge.premium_redeem')} + + + + + ); +}; + +export { PremiumRedeemCard }; +export type { PremiumRedeemCardProps }; diff --git a/src/pages/Bridge/BridgeOverview/components/RedeemForm/RedeemForm.styles.tsx b/src/pages/Bridge/BridgeOverview/components/RedeemForm/RedeemForm.styles.tsx new file mode 100644 index 0000000000..5c1eae4f4c --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/RedeemForm/RedeemForm.styles.tsx @@ -0,0 +1,23 @@ +import styled from 'styled-components'; + +import { InformationCircle } from '@/assets/icons'; +import { Dl, Switch, theme } from '@/component-library'; + +const StyledDl = styled(Dl)` + background-color: ${theme.card.bg.secondary}; + padding: ${theme.spacing.spacing4}; + font-size: ${theme.text.xs}; + border-radius: ${theme.rounded.rg}; +`; + +const StyledInformationCircle = styled(InformationCircle)` + margin-left: ${theme.spacing.spacing2}; + vertical-align: text-top; +`; + +const StyledSwitch = styled(Switch)` + flex-direction: row-reverse; + justify-content: space-between; +`; + +export { StyledDl, StyledInformationCircle, StyledSwitch }; diff --git a/src/pages/Bridge/BridgeOverview/components/RedeemForm/RedeemForm.tsx b/src/pages/Bridge/BridgeOverview/components/RedeemForm/RedeemForm.tsx new file mode 100644 index 0000000000..a94cb619e3 --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/RedeemForm/RedeemForm.tsx @@ -0,0 +1,335 @@ +import { getRedeemRequestsFromExtrinsicResult, newMonetaryAmount, Redeem } from '@interlay/interbtc-api'; +import { Currency, MonetaryAmount } from '@interlay/monetary-js'; +import { mergeProps } from '@react-aria/utils'; +import { ChangeEvent, Key, useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDebounce } from 'react-use'; + +import { + convertMonetaryAmountToValueInUSD, + getRandomArrayElement, + newSafeMonetaryAmount, + safeBitcoinAmount +} from '@/common/utils/utils'; +import { Flex, Input, TokenInput } from '@/component-library'; +import { AuthCTA } from '@/components'; +import { GOVERNANCE_TOKEN, WRAPPED_TOKEN } from '@/config/relay-chains'; +import { + BRIDGE_REDEEM_ADDRESS, + BRIDGE_REDEEM_AMOUNT_FIELD, + BRIDGE_REDEEM_CUSTOM_VAULT_FIELD, + BRIDGE_REDEEM_CUSTOM_VAULT_SWITCH, + BRIDGE_REDEEM_FEE_TOKEN, + BRIDGE_REDEEM_PREMIUM_VAULT_FIELD, + BridgeRedeemFormData, + bridgeRedeemSchema, + useForm +} from '@/lib/form'; +import { BridgeActions } from '@/types/bridge'; +import { getTokenPrice } from '@/utils/helpers/prices'; +import { RedeemData, useGetRedeemData } from '@/utils/hooks/api/bridge/use-get-redeem-data'; +import { BridgeVaultData, useGetVaults } from '@/utils/hooks/api/bridge/use-get-vaults'; +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 { TransactionArgs } from '@/utils/hooks/transaction/types'; +import { isTransactionFormDisabled } from '@/utils/hooks/transaction/utils/form'; + +import { LegacyRedeemModal } from '../LegacyRedeemModal'; +import { RequestLimitsCard } from '../RequestLimitsCard'; +import { SelectVaultCard } from '../SelectVaultCard'; +import { TransactionDetails } from '../TransactionDetails'; +import { PremiumRedeemCard } from './PremiumRedeemCard'; + +const getRequestLimit = ( + redeemLimit: MonetaryAmount, + selectedVault?: BridgeVaultData, + premiumRedeemLimit?: MonetaryAmount, + isPremiumRedeem?: boolean +) => { + if (selectedVault) { + return selectedVault.amount; + } + + if (isPremiumRedeem && premiumRedeemLimit) { + return premiumRedeemLimit; + } + + return redeemLimit; +}; + +type RedeemFormProps = RedeemData; + +const RedeemForm = ({ + currentInclusionFee, + dustValue, + feeRate, + redeemLimit, + premium +}: RedeemFormProps): JSX.Element => { + const { t } = useTranslation(); + const prices = useGetPrices(); + const { getBalance, getAvailableBalance } = useGetBalances(); + const { getCompensationAmount } = useGetRedeemData(); + + const [redeemRequest, setRedeemRequest] = useState(); + + const [isPremiumRedeem, setPremiumRedeem] = useState(false); + + const [amount, setAmount] = useState(); + const [debouncedAmount, setDebouncedAmount] = useState(); + + useDebounce(() => setDebouncedAmount(amount), 500, [amount]); + + const [selectedVault, setSelectedVault] = useState(); + + const { data: vaultsData, getAvailableVaults } = useGetVaults(BridgeActions.REDEEM); + + const debouncedMonetaryAmount = safeBitcoinAmount(debouncedAmount || 0); + const availableVaults = getAvailableVaults(debouncedMonetaryAmount); + const vaults = availableVaults?.length ? availableVaults : vaultsData?.list; + + const transaction = useTransaction(Transaction.REDEEM_REQUEST, { + onSuccess: async (result) => { + try { + const [redeemRequest] = await getRedeemRequestsFromExtrinsicResult(window.bridge, result.data); + + setRedeemRequest(redeemRequest); + } catch (e: any) { + transaction.reject(e); + } + + setAmount(undefined); + setDebouncedAmount(undefined); + form.resetForm(); + }, + showSuccessModal: false + }); + + const governanceBalance = getBalance(GOVERNANCE_TOKEN.ticker)?.free || newMonetaryAmount(0, GOVERNANCE_TOKEN); + + const assetBalance = getAvailableBalance(WRAPPED_TOKEN.ticker) || newMonetaryAmount(0, WRAPPED_TOKEN); + + const currentRequestLimit = getRequestLimit(redeemLimit, selectedVault, premium?.redeemLimit, isPremiumRedeem); + const redeemBalance = assetBalance.gt(currentRequestLimit) ? currentRequestLimit : assetBalance; + + const transferAmountSchemaParams = { + governanceBalance, + maxAmount: redeemBalance, + minAmount: dustValue + }; + + const getTransactionArgs = useCallback( + (values: BridgeRedeemFormData): TransactionArgs | undefined => { + const amount = values[BRIDGE_REDEEM_AMOUNT_FIELD]; + const btcAddress = values[BRIDGE_REDEEM_ADDRESS]; + + if (!vaultsData || !amount || !btcAddress) return; + + const monetaryAmount = newMonetaryAmount(amount, WRAPPED_TOKEN, true); + + const isPremiumRedeem = values[BRIDGE_REDEEM_PREMIUM_VAULT_FIELD]; + + const availableVaults = getAvailableVaults(monetaryAmount, isPremiumRedeem); + + if (!availableVaults) return; + + const vaultId = values[BRIDGE_REDEEM_CUSTOM_VAULT_FIELD]; + + let vault: BridgeVaultData | undefined; + + // If custom vault was select, try to find it in the data + if (vaultId) { + vault = availableVaults.find((item) => item.id === vaultId); + } + + // If no vault provided nor the custom vault wasn't found (unlikely), choose random vault + if (!vault) { + vault = getRandomArrayElement(availableVaults); + } + + return [monetaryAmount, btcAddress, vault.vaultId]; + }, + [vaultsData, getAvailableVaults] + ); + + const handleSubmit = async (values: BridgeRedeemFormData) => { + const args = getTransactionArgs(values); + + if (!args) return; + + transaction.execute(...args); + }; + + const form = useForm({ + initialValues: { + [BRIDGE_REDEEM_AMOUNT_FIELD]: '', + [BRIDGE_REDEEM_CUSTOM_VAULT_FIELD]: '', + [BRIDGE_REDEEM_CUSTOM_VAULT_SWITCH]: false, + [BRIDGE_REDEEM_PREMIUM_VAULT_FIELD]: false, + [BRIDGE_REDEEM_ADDRESS]: '', + [BRIDGE_REDEEM_FEE_TOKEN]: transaction.fee.defaultCurrency.ticker + }, + validationSchema: bridgeRedeemSchema({ [BRIDGE_REDEEM_AMOUNT_FIELD]: transferAmountSchemaParams }), + onSubmit: handleSubmit, + hideErrorMessages: transaction.isLoading, + onComplete: (values) => { + const args = getTransactionArgs(values); + + if (!args) return; + + const feeTicker = values[BRIDGE_REDEEM_FEE_TOKEN]; + + transaction.fee.setCurrency(feeTicker).estimate(...args); + } + }); + + const handleToggleCustomVault = (e: ChangeEvent) => { + const isChecked = e.target.checked; + + // make vault select field untouched + if (!isChecked) { + return form.setFieldTouched(BRIDGE_REDEEM_CUSTOM_VAULT_FIELD, false, true); + } + }; + + const handleTogglePremiumVault = (e: ChangeEvent) => { + const isChecked = e.target.checked; + + setPremiumRedeem(isChecked); + + const isSelectingVault = form.values[BRIDGE_REDEEM_CUSTOM_VAULT_SWITCH]; + + // Do not continue if premium is unchecked and is not manually selecting vault + if (!isChecked || !isSelectingVault) return; + + const premiumVaults = getAvailableVaults(monetaryAmount, true); + + const selectedVault = form.values[BRIDGE_REDEEM_CUSTOM_VAULT_FIELD]; + + if (!selectedVault) return; + + const isSelectedVaultValid = premiumVaults?.find((vault) => vault.id === selectedVault); + + if (isSelectedVaultValid) return; + + form.setFieldValue(BRIDGE_REDEEM_CUSTOM_VAULT_FIELD, '', true); + }; + + const handleChangeIssueAmount = (e: ChangeEvent) => setAmount(e.target.value); + + const handleVaultSelectionChange = (key: Key) => { + if (!vaults) return; + + const vault = vaults.find((item) => item.id === key); + + setSelectedVault(vault); + }; + + const monetaryAmount = newSafeMonetaryAmount(amount || 0, WRAPPED_TOKEN, true); + const amountUSD = monetaryAmount + ? convertMonetaryAmountToValueInUSD(monetaryAmount, getTokenPrice(prices, monetaryAmount.currency.ticker)?.usd) || 0 + : 0; + + const bridgeFee = monetaryAmount.mul(feeRate); + + const totalFees = bridgeFee.add(currentInclusionFee); + + const totalAmount = monetaryAmount.gte(totalFees) + ? monetaryAmount.sub(totalFees) + : newMonetaryAmount(0, WRAPPED_TOKEN); + const totalAmountUSD = totalAmount + ? convertMonetaryAmountToValueInUSD(totalAmount, getTokenPrice(prices, totalAmount.currency.ticker)?.usd) || 0 + : 0; + + const compensationAmount = + monetaryAmount.isZero() && isPremiumRedeem ? getCompensationAmount(monetaryAmount) : undefined; + const compensationAmountUSD = compensationAmount + ? convertMonetaryAmountToValueInUSD( + compensationAmount, + getTokenPrice(prices, compensationAmount.currency.ticker)?.usd + ) || 0 + : 0; + + const isSelectingVault = form.values[BRIDGE_REDEEM_CUSTOM_VAULT_SWITCH]; + + const isBtnDisabled = isTransactionFormDisabled(form, transaction.fee); + + const hasPremiumRedeemFeature = premium; + + return ( + <> + +
+ + + + + {hasPremiumRedeemFeature && ( + + )} + + + + + + + {t('redeem')} + + + +
+
+ {redeemRequest && ( + setRedeemRequest(undefined)} request={redeemRequest} /> + )} + + ); +}; + +export { RedeemForm }; +export type { RedeemFormProps }; diff --git a/src/pages/Bridge/BridgeOverview/components/RedeemForm/index.tsx b/src/pages/Bridge/BridgeOverview/components/RedeemForm/index.tsx new file mode 100644 index 0000000000..0e5a736b57 --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/RedeemForm/index.tsx @@ -0,0 +1,2 @@ +export type { RedeemFormProps } from './RedeemForm'; +export { RedeemForm } from './RedeemForm'; diff --git a/src/pages/Bridge/BridgeOverview/components/RequestLimitsCard/RequestLimitsCard.tsx b/src/pages/Bridge/BridgeOverview/components/RequestLimitsCard/RequestLimitsCard.tsx new file mode 100644 index 0000000000..eae288b9e8 --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/RequestLimitsCard/RequestLimitsCard.tsx @@ -0,0 +1,46 @@ +import { Currency, MonetaryAmount } from '@interlay/monetary-js'; +import { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Card, Dd, Dl, DlGroup, Dt, Flex, P } from '@/component-library'; + +type RequestLimitsCardProps = { + title: ReactNode; + singleRequestLimit: MonetaryAmount; + maxRequestLimit?: MonetaryAmount; +}; + +const RequestLimitsCard = ({ title, singleRequestLimit, maxRequestLimit }: RequestLimitsCardProps): JSX.Element => { + const { t } = useTranslation(); + + return ( + +

{title}

+ +
+ +
+ {t('bridge.in_single_request')} +
+
+ {singleRequestLimit.toHuman()} {singleRequestLimit.currency.ticker} +
+
+ {maxRequestLimit && ( + +
+ {t('bridge.in_total')} +
+
+ {maxRequestLimit.toHuman()} {maxRequestLimit.currency.ticker} +
+
+ )} +
+
+
+ ); +}; + +export { RequestLimitsCard }; +export type { RequestLimitsCardProps }; diff --git a/src/pages/Bridge/BridgeOverview/components/RequestLimitsCard/index.tsx b/src/pages/Bridge/BridgeOverview/components/RequestLimitsCard/index.tsx new file mode 100644 index 0000000000..fb322f8032 --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/RequestLimitsCard/index.tsx @@ -0,0 +1,2 @@ +export type { RequestLimitsCardProps } from './RequestLimitsCard'; +export { RequestLimitsCard } from './RequestLimitsCard'; diff --git a/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/SelectVaultCard.tsx b/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/SelectVaultCard.tsx new file mode 100644 index 0000000000..068bf1ba27 --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/SelectVaultCard.tsx @@ -0,0 +1,43 @@ +import { useTranslation } from 'react-i18next'; + +import { Card, SwitchProps } from '@/component-library'; +import { BridgeVaultData } from '@/utils/hooks/api/bridge/use-get-vaults'; + +import { VaultSelect, VaultSelectProps } from './VaultSelect'; +import { StyledSwitch } from './VaultSelect.style'; + +type SelectVaultCardProps = { + isSelectingVault?: boolean; + vaults: BridgeVaultData[] | undefined; + switchProps: SwitchProps; + selectProps: VaultSelectProps; +}; + +const SelectVaultCard = ({ vaults, isSelectingVault, switchProps, selectProps }: SelectVaultCardProps): JSX.Element => { + const { t } = useTranslation(); + + return ( + + + {t('bridge.manually_select_vault')} + + {isSelectingVault && vaults && ( + + )} + + ); +}; + +SelectVaultCard.displayName = 'SelectVaultCard'; + +export { SelectVaultCard }; +export type { SelectVaultCardProps }; diff --git a/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/VaultListItem.tsx b/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/VaultListItem.tsx new file mode 100644 index 0000000000..0fd4507971 --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/VaultListItem.tsx @@ -0,0 +1,48 @@ +import Identicon from '@polkadot/react-identicon'; + +import { CoinIcon, Flex } from '@/component-library'; +import { useSelectModalContext } from '@/component-library/Select/SelectModalContext'; +import { BridgeVaultData } from '@/utils/hooks/api/bridge/use-get-vaults'; + +import { + StyledListLabelWrapper, + StyledListWrapper, + StyledVaultAddress, + StyledVaultIcon, + StyledVaultName +} from './VaultSelect.style'; + +type VaultListItemProps = { data: BridgeVaultData }; + +const VaultListItem = ({ data }: VaultListItemProps): JSX.Element => { + const { id, vaultId, collateralCurrency, amount } = data; + + const isSelected = useSelectModalContext().selectedItem?.key === id; + + const accountAddress = vaultId.accountId.toString(); + + return ( + + + + + + + + + {collateralCurrency.ticker} Vault + + + {amount.toHuman()} BTC + + + + {accountAddress} + + + + ); +}; + +export { VaultListItem }; +export type { VaultListItemProps }; diff --git a/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/VaultSelect.style.tsx b/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/VaultSelect.style.tsx new file mode 100644 index 0000000000..fdcb2a8a76 --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/VaultSelect.style.tsx @@ -0,0 +1,63 @@ +import styled from 'styled-components'; + +import { Flex, Span, Switch, theme } from '@/component-library'; + +type StyledListItemSelectedLabelProps = { + $isSelected: boolean; +}; + +const StyledChain = styled.span` + font-size: ${theme.text.s}; + color: ${theme.colors.textPrimary}; + overflow: hidden; + text-overflow: ellipsis; +`; + +const StyledListItemLabel = styled(Span)` + color: ${({ $isSelected }) => + $isSelected ? theme.tokenInput.list.item.selected.text : theme.tokenInput.list.item.default.text}; + text-overflow: ellipsis; + overflow: hidden; +`; + +const StyledVaultName = styled(Span)` + color: ${({ $isSelected }) => + $isSelected ? theme.tokenInput.list.item.selected.text : theme.tokenInput.list.item.default.text}; +`; + +const StyledListWrapper = styled(Flex)` + overflow: hidden; +`; + +const StyledListLabelWrapper = styled(Flex)` + overflow: hidden; +`; + +const StyledVaultAddress = styled(Span)` + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +`; + +const StyledVaultIcon = styled(Flex)` + & :last-child { + margin-left: -25%; + z-index: 1; + } +`; + +const StyledSwitch = styled(Switch)` + flex-direction: row-reverse; + justify-content: space-between; +`; + +export { + StyledChain, + StyledListItemLabel, + StyledListLabelWrapper, + StyledListWrapper, + StyledSwitch, + StyledVaultAddress, + StyledVaultIcon, + StyledVaultName +}; diff --git a/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/VaultSelect.tsx b/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/VaultSelect.tsx new file mode 100644 index 0000000000..7f71e9c14f --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/VaultSelect.tsx @@ -0,0 +1,27 @@ +import { useTranslation } from 'react-i18next'; + +import { Item, Select, SelectProps } from '@/component-library'; +import { BridgeVaultData } from '@/utils/hooks/api/bridge/use-get-vaults'; + +import { VaultListItem } from './VaultListItem'; + +type VaultSelectProps = Omit, 'children' | 'type'>; + +const VaultSelect = (props: VaultSelectProps): JSX.Element => { + const { t } = useTranslation(); + + return ( + {...props} type='modal' modalTitle={t('bridge.select_vault')} size='large'> + {(data: BridgeVaultData) => ( + + + + )} + + ); +}; + +VaultSelect.displayName = 'VaultSelect'; + +export { VaultSelect }; +export type { VaultSelectProps }; diff --git a/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/index.tsx b/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/index.tsx new file mode 100644 index 0000000000..dba800be13 --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/SelectVaultCard/index.tsx @@ -0,0 +1,2 @@ +export type { SelectVaultCardProps } from './SelectVaultCard'; +export { SelectVaultCard } from './SelectVaultCard'; diff --git a/src/pages/Bridge/BridgeOverview/components/TransactionDetails/TransactionDetails.style.tsx b/src/pages/Bridge/BridgeOverview/components/TransactionDetails/TransactionDetails.style.tsx new file mode 100644 index 0000000000..a806944d1c --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/TransactionDetails/TransactionDetails.style.tsx @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +import { theme } from '@/component-library/theme'; +import { PlusDivider } from '@/components'; + +const StyledPlusDivider = styled(PlusDivider)` + margin-bottom: calc(${theme.spacing.spacing2} * -1); + z-index: 0; +`; + +export { StyledPlusDivider }; diff --git a/src/pages/Bridge/BridgeOverview/components/TransactionDetails/TransactionDetails.tsx b/src/pages/Bridge/BridgeOverview/components/TransactionDetails/TransactionDetails.tsx new file mode 100644 index 0000000000..196bd23a1e --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/TransactionDetails/TransactionDetails.tsx @@ -0,0 +1,123 @@ +import { Currency, MonetaryAmount } from '@interlay/monetary-js'; +import { useTranslation } from 'react-i18next'; + +import { displayMonetaryAmountInUSDFormat } from '@/common/utils/utils'; +import { Flex, TokenInput } from '@/component-library'; +import { + TransactionDetails as BaseTransactionDetails, + TransactionDetailsDd, + TransactionDetailsDt, + TransactionDetailsGroup, + TransactionFeeDetails, + TransactionFeeDetailsProps, + TransactionSelectToken, + TransactionSelectTokenProps +} from '@/components'; +import { getTokenPrice } from '@/utils/helpers/prices'; +import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { SelectCurrencyFilter, useSelectCurrency } from '@/utils/hooks/use-select-currency'; + +import { StyledPlusDivider } from './TransactionDetails.style'; + +type TransactionDetailsProps = { + totalAmount: MonetaryAmount; + totalAmountUSD: number; + totalTicker: string; + compensationAmount?: MonetaryAmount; + compensationAmountUSD?: number; + bridgeFee: MonetaryAmount; + securityDeposit?: MonetaryAmount; + bitcoinNetworkFee?: MonetaryAmount; + feeDetailsProps?: TransactionFeeDetailsProps; + securityDepositSelectProps?: TransactionSelectTokenProps; +}; + +const TransactionDetails = ({ + totalAmount, + totalAmountUSD, + totalTicker, + compensationAmount, + compensationAmountUSD, + bridgeFee, + securityDeposit, + bitcoinNetworkFee, + feeDetailsProps, + securityDepositSelectProps +}: TransactionDetailsProps): JSX.Element => { + const prices = useGetPrices(); + const { t } = useTranslation(); + + const { items: griefingCollateralCurrencies } = useSelectCurrency( + SelectCurrencyFilter.ISSUE_GRIEFING_COLLATERAL_CURRENCY + ); + + return ( + + + + {compensationAmount && ( + <> + + + + )} + + + + + {t('bridge.bridge_fee')} + + + {bridgeFee.toHuman()} {bridgeFee.currency.ticker} ( + {displayMonetaryAmountInUSDFormat(bridgeFee, getTokenPrice(prices, bridgeFee.currency.ticker)?.usd)}) + + + {securityDeposit && ( + <> + {securityDepositSelectProps && ( + + )} + + + {t('bridge.security_deposit')} + + + {securityDeposit.toHuman()} {securityDeposit.currency.ticker} ( + {displayMonetaryAmountInUSDFormat( + securityDeposit, + getTokenPrice(prices, securityDeposit.currency.ticker)?.usd + )} + ) + + + + )} + + {bitcoinNetworkFee && ( + + )} + {feeDetailsProps && } + + ); +}; + +export { TransactionDetails }; +export type { TransactionDetailsProps }; diff --git a/src/pages/Bridge/BridgeOverview/components/TransactionDetails/index.tsx b/src/pages/Bridge/BridgeOverview/components/TransactionDetails/index.tsx new file mode 100644 index 0000000000..d8856d5ba5 --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/TransactionDetails/index.tsx @@ -0,0 +1,2 @@ +export type { TransactionDetailsProps } from './TransactionDetails'; +export { TransactionDetails } from './TransactionDetails'; diff --git a/src/pages/Bridge/BridgeOverview/components/index.tsx b/src/pages/Bridge/BridgeOverview/components/index.tsx new file mode 100644 index 0000000000..20c449724d --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/components/index.tsx @@ -0,0 +1,7 @@ +import { IssueForm, IssueFormProps } from './IssueForm'; +import { LegacyBurnForm } from './LegacyBurnForm'; +import { LegacyTransactions } from './LegacyTransactions'; +import { RedeemForm, RedeemFormProps } from './RedeemForm'; + +export { IssueForm, LegacyBurnForm, LegacyTransactions, RedeemForm }; +export type { IssueFormProps, RedeemFormProps }; diff --git a/src/pages/Bridge/BridgeOverview/index.tsx b/src/pages/Bridge/BridgeOverview/index.tsx new file mode 100644 index 0000000000..a1de6d6a3f --- /dev/null +++ b/src/pages/Bridge/BridgeOverview/index.tsx @@ -0,0 +1,3 @@ +import BridgeOverview from './BridgeOverview'; + +export default BridgeOverview; diff --git a/src/pages/Bridge/IssueForm/index.tsx b/src/pages/Bridge/IssueForm/index.tsx deleted file mode 100644 index fbffaaae14..0000000000 --- a/src/pages/Bridge/IssueForm/index.tsx +++ /dev/null @@ -1,551 +0,0 @@ -import { - currencyIdToMonetaryCurrency, - getIssueRequestsFromExtrinsicResult, - GovernanceCurrency, - InterbtcPrimitivesVaultId, - Issue -} from '@interlay/interbtc-api'; -import { IssueLimits } from '@interlay/interbtc-api/build/src/parachain/issue'; -import { Bitcoin, BitcoinAmount, ExchangeRate } from '@interlay/monetary-js'; -import Big from 'big.js'; -import clsx from 'clsx'; -import * as React from 'react'; -import { useErrorHandler, withErrorBoundary } from 'react-error-boundary'; -import { useForm } from 'react-hook-form'; -import { Trans, useTranslation } from 'react-i18next'; -import { useQuery } from 'react-query'; -import { useDispatch, useSelector } from 'react-redux'; - -import { ReactComponent as BitcoinLogoIcon } from '@/assets/img/bitcoin-logo.svg'; -import { ParachainStatus, StoreType } from '@/common/types/util.types'; -import { VaultApiType } from '@/common/types/vault.types'; -import { - displayMonetaryAmount, - displayMonetaryAmountInUSDFormat, - getRandomVaultIdWithCapacity -} from '@/common/utils/utils'; -import { AuthCTA } from '@/components'; -import { INTERLAY_VAULT_DOCS_LINK } from '@/config/links'; -import { - BLOCKS_BEHIND_LIMIT, - DEFAULT_ISSUE_BRIDGE_FEE_RATE, - DEFAULT_ISSUE_DUST_AMOUNT, - DEFAULT_ISSUE_GRIEFING_COLLATERAL_RATE -} from '@/config/parachain'; -import { - GOVERNANCE_TOKEN, - GOVERNANCE_TOKEN_SYMBOL, - GovernanceTokenLogoIcon, - TRANSACTION_FEE_AMOUNT, - WRAPPED_TOKEN_SYMBOL, - WrappedTokenLogoIcon -} from '@/config/relay-chains'; -import AvailableBalanceUI from '@/legacy-components/AvailableBalanceUI'; -import ErrorFallback from '@/legacy-components/ErrorFallback'; -import FormTitle from '@/legacy-components/FormTitle'; -import Hr2 from '@/legacy-components/hrs/Hr2'; -import PriceInfo from '@/legacy-components/PriceInfo'; -import PrimaryColorEllipsisLoader from '@/legacy-components/PrimaryColorEllipsisLoader'; -import TokenField from '@/legacy-components/TokenField'; -import InformationTooltip from '@/legacy-components/tooltips/InformationTooltip'; -import InterlayLink from '@/legacy-components/UI/InterlayLink'; -import { useSubstrateSecureState } from '@/lib/substrate'; -import ParachainStatusInfo from '@/pages/Bridge/ParachainStatusInfo'; -import genericFetcher, { GENERIC_FETCHER } from '@/services/fetchers/generic-fetcher'; -import { ForeignAssetIdLiteral } from '@/types/currency'; -import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; -import STATUSES from '@/utils/constants/statuses'; -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'; - -const BTC_AMOUNT = 'btc-amount'; -const VAULT_SELECTION = 'vault-selection'; - -const getTokenFieldHelperText = (message?: string) => { - switch (message) { - case 'no_issuable_token_available': - return ( - - Oh, snap! All iBTC minting capacity has been snatched up. Please come back a bit later, or{' '} - - consider running a Vault - - ! - - ); - default: - return message; - } -}; - -type IssueFormData = { - [BTC_AMOUNT]: string; - [VAULT_SELECTION]: string; -}; - -const IssueForm = (): JSX.Element | null => { - const dispatch = useDispatch(); - const { t } = useTranslation(); - const prices = useGetPrices(); - - const handleError = useErrorHandler(); - - const { selectedAccount } = useSubstrateSecureState(); - const { bridgeLoaded, bitcoinHeight, btcRelayHeight, parachainStatus } = useSelector( - (state: StoreType) => state.general - ); - const { isLoading: isBalancesLoading, data: balances } = useGetBalances(); - - const { - register, - handleSubmit, - watch, - formState: { errors }, - trigger, - setError, - clearErrors - } = useForm({ - mode: 'onChange' // 'onBlur' - }); - const btcAmount = watch(BTC_AMOUNT) || '0'; - - const [status, setStatus] = React.useState(STATUSES.IDLE); - // Additional info: bridge fee, security deposit, amount BTC - // Current fee model specification taken from: https://interlay.gitlab.io/polkabtc-spec/spec/fee.html - const [issueFeeRate, setIssueFeeRate] = React.useState(new Big(DEFAULT_ISSUE_BRIDGE_FEE_RATE)); - const [depositRate, setDepositRate] = React.useState(new Big(DEFAULT_ISSUE_GRIEFING_COLLATERAL_RATE)); - const [btcToGovernanceTokenRate, setBTCToGovernanceTokenRate] = React.useState( - new ExchangeRate(Bitcoin, GOVERNANCE_TOKEN, new Big(0)) - ); - const [dustValue, setDustValue] = React.useState(new BitcoinAmount(DEFAULT_ISSUE_DUST_AMOUNT)); - const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE); - const [submittedRequest, setSubmittedRequest] = React.useState(); - const [selectVaultManually, setSelectVaultManually] = React.useState(false); - const [selectedVault, setSelectedVault] = React.useState(); - - const { - isIdle: requestLimitsIdle, - isLoading: requestLimitsLoading, - data: requestLimits, - error: requestLimitsError, - refetch: requestLimitsRefetch - } = useQuery([GENERIC_FETCHER, 'issue', 'getRequestLimits'], genericFetcher(), { - enabled: !!bridgeLoaded - }); - useErrorHandler(requestLimitsError); - - const transaction = useTransaction(Transaction.ISSUE_REQUEST, { showSuccessModal: false }); - - React.useEffect(() => { - if (!bridgeLoaded) return; - if (!dispatch) return; - if (!handleError) return; - - (async () => { - try { - setStatus(STATUSES.PENDING); - const [ - feeRateResult, - depositRateResult, - dustValueResult, - btcToGovernanceTokenResult - ] = await Promise.allSettled([ - // Loading this data is not strictly required as long as the constantly set values did - // not change. However, you will not see the correct value for the security deposit. - window.bridge.fee.getIssueFee(), - window.bridge.fee.getIssueGriefingCollateralRate(), - window.bridge.issue.getDustValue(), - getExchangeRate(GOVERNANCE_TOKEN) - ]); - setStatus(STATUSES.RESOLVED); - - if (feeRateResult.status === 'rejected') { - throw new Error(feeRateResult.reason); - } - - if (depositRateResult.status === 'rejected') { - throw new Error(depositRateResult.reason); - } - - if (dustValueResult.status === 'rejected') { - throw new Error(dustValueResult.reason); - } - - if (btcToGovernanceTokenResult.status === 'rejected') { - setError(BTC_AMOUNT, { - type: 'validate', - message: t('error_oracle_offline', { action: 'issue', wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL }) - }); - } - - if (btcToGovernanceTokenResult.status === 'fulfilled') { - setBTCToGovernanceTokenRate(btcToGovernanceTokenResult.value); - } - - setIssueFeeRate(feeRateResult.value); - setDepositRate(depositRateResult.value); - setDustValue(dustValueResult.value); - } catch (error) { - setStatus(STATUSES.REJECTED); - handleError(error); - } - })(); - }, [bridgeLoaded, dispatch, handleError, setError, t]); - - React.useEffect(() => { - // Deselect checkbox when required btcAmount exceeds capacity - if (requestLimits) { - const monetaryBtcAmount = new BitcoinAmount(btcAmount); - if (monetaryBtcAmount.gt(requestLimits.singleVaultMaxIssuable)) { - setSelectVaultManually(false); - } - } - }, [btcAmount, requestLimits]); - - React.useEffect(() => { - // Vault selection validation - const monetaryBtcAmount = new BitcoinAmount(btcAmount); - - if (selectVaultManually && selectedVault === undefined) { - setError(VAULT_SELECTION, { type: 'validate', message: t('issue_page.vault_must_be_selected') }); - } else if (selectVaultManually && selectedVault?.[1].lt(monetaryBtcAmount)) { - setError(VAULT_SELECTION, { type: 'validate', message: t('issue_page.selected_vault_has_no_enough_capacity') }); - } else { - clearErrors(VAULT_SELECTION); - } - }, [selectVaultManually, selectedVault, setError, clearErrors, t, btcAmount]); - - const hasIssuableToken = !requestLimits?.singleVaultMaxIssuable.isZero(); - - React.useEffect(() => { - if (!hasIssuableToken) { - setError(BTC_AMOUNT, { - type: 'validate', - message: 'no_issuable_token_available' - }); - } - }, [hasIssuableToken, setError]); - - if ( - status === STATUSES.IDLE || - status === STATUSES.PENDING || - requestLimitsIdle || - requestLimitsLoading || - isBalancesLoading - ) { - return ; - } - - if (requestLimits === undefined) { - throw new Error('Something went wrong!'); - } - - if (status === STATUSES.RESOLVED) { - const validateForm = (value: string): string | undefined => { - const governanceTokenBalance = balances?.[GOVERNANCE_TOKEN.ticker]; - - if (governanceTokenBalance === undefined) return; - - const numericValue = Number(value || '0'); - const btcAmount = new BitcoinAmount(numericValue); - - const securityDeposit = btcToGovernanceTokenRate.toCounter(btcAmount).mul(depositRate); - const minRequiredGovernanceTokenAmount = TRANSACTION_FEE_AMOUNT.add(securityDeposit); - if (governanceTokenBalance.transferable.lte(minRequiredGovernanceTokenAmount)) { - return t('issue_page.insufficient_funds', { - governanceTokenSymbol: GOVERNANCE_TOKEN_SYMBOL - }); - } - - if (btcAmount.lt(dustValue)) { - return `${t('issue_page.validation_min_value')}${displayMonetaryAmount(dustValue)} BTC).`; - } - - if (btcAmount.gt(requestLimits.singleVaultMaxIssuable)) { - return t('issue_page.maximum_in_single_request_error', { - maxAmount: displayMonetaryAmount(requestLimits.singleVaultMaxIssuable), - wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL - }); - } - - if (bitcoinHeight - btcRelayHeight > BLOCKS_BEHIND_LIMIT) { - return t('issue_page.error_more_than_6_blocks_behind', { - wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL - }); - } - - if (isOracleOffline) { - return t('error_oracle_offline', { action: 'issue', wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL }); - } - - return undefined; - }; - - const handleSubmittedRequestModalOpen = (newSubmittedRequest: Issue) => { - setSubmittedRequest(newSubmittedRequest); - }; - const handleSubmittedRequestModalClose = () => { - setSubmittedRequest(undefined); - }; - - const handleSelectVaultCheckboxChange = () => { - if (!isSelectVaultCheckboxDisabled) { - setSelectVaultManually((prev) => !prev); - } - }; - - const onSubmit = async (data: IssueFormData) => { - 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); - } - - 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); - - const bridgeFee = monetaryBtcAmount.mul(issueFeeRate); - const bridgeFeeInBTC = bridgeFee.toHuman(8); - const bridgeFeeInUSD = displayMonetaryAmountInUSDFormat( - bridgeFee, - getTokenPrice(prices, ForeignAssetIdLiteral.BTC)?.usd - ); - - const securityDeposit = btcToGovernanceTokenRate.toCounter(monetaryBtcAmount).mul(depositRate); - const securityDepositInGovernanceToken = displayMonetaryAmount(securityDeposit); - const securityDepositInUSD = displayMonetaryAmountInUSDFormat( - securityDeposit, - getTokenPrice(prices, GOVERNANCE_TOKEN_SYMBOL)?.usd - ); - - const txFeeInGovernanceToken = displayMonetaryAmount(TRANSACTION_FEE_AMOUNT); - const txFeeInUSD = displayMonetaryAmountInUSDFormat( - TRANSACTION_FEE_AMOUNT, - getTokenPrice(prices, GOVERNANCE_TOKEN_SYMBOL)?.usd - ); - - const total = monetaryBtcAmount.sub(bridgeFee); - const totalInBTC = total.toHuman(8); - const totalInUSD = displayMonetaryAmountInUSDFormat(total, getTokenPrice(prices, ForeignAssetIdLiteral.BTC)?.usd); - - const accountSet = !!selectedAccount; - - const isSelectVaultCheckboxDisabled = monetaryBtcAmount.gt(requestLimits.singleVaultMaxIssuable); - - // `btcToGovernanceTokenRate` has 0 value only if oracle call fails - const isOracleOffline = btcToGovernanceTokenRate.toBig().eq(0); - - // TODO: `parachainStatus` and `address` should be checked at upper levels - const isSubmitBtnDisabled = accountSet ? parachainStatus !== ParachainStatus.Running || !selectedAccount : false; - - return ( - <> -
- - {t('issue_page.mint_polka_by_wrapping', { - wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL - })} - -
- - - validateForm(value) - })} - approxUSD={`≈ ${displayMonetaryAmountInUSDFormat( - monetaryBtcAmount || BitcoinAmount.zero(), - getTokenPrice(prices, ForeignAssetIdLiteral.BTC)?.usd - )}`} - error={!!errors[BTC_AMOUNT]} - helperText={getTokenFieldHelperText(errors[BTC_AMOUNT]?.message)} - helperTextClassName={clsx({ 'h-12': !hasIssuableToken })} - /> -
- - - - {t('you_will_receive')} - - } - unitIcon={} - dataTestId='total-receiving-amount' - value={totalInBTC} - unitName={WRAPPED_TOKEN_SYMBOL} - approxUSD={totalInUSD} - /> - - - {t('bridge_fee')} - - } - unitIcon={} - dataTestId='issue-bridge-fee' - value={bridgeFeeInBTC} - unitName='BTC' - approxUSD={bridgeFeeInUSD} - tooltip={ - - } - /> - - {t('issue_page.security_deposit')} - - } - unitIcon={} - dataTestId='security-deposit' - value={securityDepositInGovernanceToken} - unitName={GOVERNANCE_TOKEN_SYMBOL} - approxUSD={securityDepositInUSD} - tooltip={ - - } - /> - - {t('issue_page.transaction_fee')} - - } - unitIcon={} - dataTestId='transaction-fee' - value={txFeeInGovernanceToken} - unitName={GOVERNANCE_TOKEN_SYMBOL} - approxUSD={txFeeInUSD} - tooltip={ - - } - /> - - - {t('confirm')} - - - {submittedRequest && ( - - )} - - ); - } - - return null; -}; - -export default withErrorBoundary(IssueForm, { - FallbackComponent: ErrorFallback, - onReset: () => { - window.location.reload(); - } -}); diff --git a/src/pages/Bridge/ManualVaultSelectUI/index.tsx b/src/pages/Bridge/ManualVaultSelectUI/index.tsx deleted file mode 100644 index cf7246be17..0000000000 --- a/src/pages/Bridge/ManualVaultSelectUI/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { BitcoinAmount } from '@interlay/monetary-js'; -import clsx from 'clsx'; -import { FieldError } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; - -import { VaultApiType } from '@/common/types/vault.types'; -import Checkbox, { CheckboxLabelSide } from '@/legacy-components/Checkbox'; -import VaultsSelector from '@/legacy-components/VaultsSelector'; -import { TreasuryAction } from '@/types/general'; - -interface Props { - disabled: boolean; - checked: boolean; - treasuryAction: TreasuryAction; - requiredCapacity: BitcoinAmount; - error?: FieldError; - onSelectionCallback: (vault: VaultApiType | undefined) => void; - onCheckboxChange: () => void; -} - -const ManualVaultSelectUI = ({ - disabled, - checked, - treasuryAction, - requiredCapacity, - error, - onSelectionCallback, - onCheckboxChange -}: Props): JSX.Element => { - const { t } = useTranslation(); - - return ( -
- - -
- ); -}; - -export default ManualVaultSelectUI; diff --git a/src/pages/Bridge/ParachainStatusInfo/index.tsx b/src/pages/Bridge/ParachainStatusInfo/index.tsx deleted file mode 100644 index 764b5652a2..0000000000 --- a/src/pages/Bridge/ParachainStatusInfo/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import clsx from 'clsx'; -import { useTranslation } from 'react-i18next'; - -import { ParachainStatus } from '@/common/types/util.types'; -import { WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains'; -import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; -import { getColorShade } from '@/utils/helpers/colors'; - -interface Props { - status: ParachainStatus; - className?: string; -} - -const ParachainStatusInfo = ({ status, className }: Props): JSX.Element | null => { - const { t } = useTranslation(); - - switch (status) { - case ParachainStatus.Loading: - return ( -

- {t('interbtc_bridge_loading', { - wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL - })} -

- ); - case ParachainStatus.Running: - return null; - // Shutdown and Error cases - default: - return ( -
-

{t('issue_redeem_disabled')}

-

- {t('interbtc_bridge_recovery_mode', { - wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL - })} -

-
- ); - } -}; - -export default ParachainStatusInfo; diff --git a/src/pages/Bridge/RedeemForm/index.tsx b/src/pages/Bridge/RedeemForm/index.tsx deleted file mode 100644 index f934ae0a99..0000000000 --- a/src/pages/Bridge/RedeemForm/index.tsx +++ /dev/null @@ -1,554 +0,0 @@ -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'; -import * as React from 'react'; -import { useErrorHandler, withErrorBoundary } from 'react-error-boundary'; -import { useForm } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; -import { useDispatch, useSelector } from 'react-redux'; - -import { ReactComponent as BitcoinLogoIcon } from '@/assets/img/bitcoin-logo.svg'; -import { togglePremiumRedeemAction } from '@/common/actions/redeem.actions'; -import { ParachainStatus, StoreType } from '@/common/types/util.types'; -import { VaultApiType } from '@/common/types/vault.types'; -import { - displayMonetaryAmount, - displayMonetaryAmountInUSDFormat, - getRandomVaultIdWithCapacity -} from '@/common/utils/utils'; -import { AuthCTA } from '@/components'; -import { BLOCKS_BEHIND_LIMIT, DEFAULT_REDEEM_BRIDGE_FEE_RATE, DEFAULT_REDEEM_DUST_AMOUNT } from '@/config/parachain'; -import { - RELAY_CHAIN_NATIVE_TOKEN, - RELAY_CHAIN_NATIVE_TOKEN_SYMBOL, - RelayChainNativeTokenLogoIcon, - WRAPPED_TOKEN, - WRAPPED_TOKEN_SYMBOL -} from '@/config/relay-chains'; -import { BALANCE_MAX_INTEGER_LENGTH, BTC_ADDRESS_REGEX } from '@/constants'; -import AvailableBalanceUI from '@/legacy-components/AvailableBalanceUI'; -import ErrorFallback from '@/legacy-components/ErrorFallback'; -import FormTitle from '@/legacy-components/FormTitle'; -import Hr2 from '@/legacy-components/hrs/Hr2'; -import PriceInfo from '@/legacy-components/PriceInfo'; -import PrimaryColorEllipsisLoader from '@/legacy-components/PrimaryColorEllipsisLoader'; -import TextField from '@/legacy-components/TextField'; -import Toggle from '@/legacy-components/Toggle'; -import TokenField from '@/legacy-components/TokenField'; -import InformationTooltip from '@/legacy-components/tooltips/InformationTooltip'; -import ParachainStatusInfo from '@/pages/Bridge/ParachainStatusInfo'; -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 { 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'; - -const WRAPPED_TOKEN_AMOUNT = 'wrapped-token-amount'; -const BTC_ADDRESS = 'btc-address'; -const VAULT_SELECTION = 'vault-selection'; - -const BTC_ADDRESS_LABEL = 'BTC Address'; - -type RedeemFormData = { - [WRAPPED_TOKEN_AMOUNT]: string; - [BTC_ADDRESS]: string; - [VAULT_SELECTION]: string; -}; - -const RedeemForm = (): JSX.Element | null => { - const dispatch = useDispatch(); - const { t } = useTranslation(); - const prices = useGetPrices(); - - const handleError = useErrorHandler(); - - const usdPrice = getTokenPrice(prices, ForeignAssetIdLiteral.BTC)?.usd; - const { bridgeLoaded, bitcoinHeight, btcRelayHeight, parachainStatus } = useSelector( - (state: StoreType) => state.general - ); - const premiumRedeemSelected = useSelector((state: StoreType) => state.redeem.premiumRedeem); - const { data: balances } = useGetBalances(); - - const { - register, - handleSubmit, - formState: { errors }, - watch, - setError, - clearErrors - } = useForm({ - mode: 'onChange' - }); - - const wrappedTokenAmount = watch(WRAPPED_TOKEN_AMOUNT) || '0'; - - const monetaryWrappedTokenAmount = React.useMemo(() => { - return new BitcoinAmount(wrappedTokenAmount); - }, [wrappedTokenAmount]); - - const [dustValue, setDustValue] = React.useState(new BitcoinAmount(DEFAULT_REDEEM_DUST_AMOUNT)); - const [status, setStatus] = React.useState(STATUSES.IDLE); - const [redeemFeeRate, setRedeemFeeRate] = React.useState(new Big(DEFAULT_REDEEM_BRIDGE_FEE_RATE)); - const [btcToRelayChainNativeTokenRate, setBtcToRelayChainNativeTokenRate] = React.useState( - new ExchangeRate(Bitcoin, RELAY_CHAIN_NATIVE_TOKEN, new Big(0)) - ); - const [hasPremiumRedeemVaults, setHasPremiumRedeemVaults] = React.useState(false); - const [maxRedeemableCapacity, setMaxRedeemableCapacity] = React.useState(BitcoinAmount.zero()); - const [premiumRedeemFee, setPremiumRedeemFee] = React.useState(new Big(0)); - const [currentInclusionFee, setCurrentInclusionFee] = React.useState(BitcoinAmount.zero()); - const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE); - const [submittedRequest, setSubmittedRequest] = React.useState(); - - const [selectVaultManually, setSelectVaultManually] = React.useState(false); - - const [selectedVault, setSelectedVault] = React.useState(); - - const transaction = useTransaction(Transaction.REDEEM_REQUEST, { showSuccessModal: false }); - - React.useEffect(() => { - if (!monetaryWrappedTokenAmount) return; - if (!maxRedeemableCapacity) return; - - if (monetaryWrappedTokenAmount.gt(maxRedeemableCapacity)) { - setSelectVaultManually(false); - } - }, [monetaryWrappedTokenAmount, maxRedeemableCapacity]); - - React.useEffect(() => { - if (!monetaryWrappedTokenAmount) return; - if (!setError) return; - if (!clearErrors) return; - - if (selectVaultManually && selectedVault === undefined) { - setError(VAULT_SELECTION, { type: 'validate', message: t('issue_page.vault_must_be_selected') }); - } else if (selectVaultManually && selectedVault?.[1].lt(monetaryWrappedTokenAmount)) { - setError(VAULT_SELECTION, { type: 'validate', message: t('issue_page.selected_vault_has_no_enough_capacity') }); - } else { - clearErrors(VAULT_SELECTION); - } - }, [selectVaultManually, selectedVault, setError, clearErrors, t, monetaryWrappedTokenAmount]); - - const bridgeFee = monetaryWrappedTokenAmount.mul(redeemFeeRate); - - React.useEffect(() => { - if (!bridgeLoaded) return; - if (!handleError) return; - - (async () => { - try { - setStatus(STATUSES.PENDING); - const [ - dustValueResult, - premiumRedeemVaultsResult, - premiumRedeemFeeRateResult, - btcToRelayChainNativeTokenRateResult, - feeRateResult, - currentInclusionFeeResult, - vaultsWithRedeemableTokensResult - ] = await Promise.allSettled([ - window.bridge.redeem.getDustValue(), - window.bridge.vaults.getPremiumRedeemVaults(), - window.bridge.redeem.getPremiumRedeemFeeRate(), - getExchangeRate(RELAY_CHAIN_NATIVE_TOKEN), - window.bridge.redeem.getFeeRate(), - window.bridge.redeem.getCurrentInclusionFee(), - window.bridge.vaults.getVaultsWithRedeemableTokens() - ]); - - if (dustValueResult.status === 'rejected') { - throw new Error(dustValueResult.reason); - } - if (premiumRedeemFeeRateResult.status === 'rejected') { - throw new Error(premiumRedeemFeeRateResult.reason); - } - if (feeRateResult.status === 'rejected') { - throw new Error(feeRateResult.reason); - } - if (currentInclusionFeeResult.status === 'rejected') { - throw new Error(currentInclusionFeeResult.reason); - } - if (btcToRelayChainNativeTokenRateResult.status === 'rejected') { - setError(WRAPPED_TOKEN_AMOUNT, { - type: 'validate', - message: t('error_oracle_offline', { action: 'redeem', wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL }) - }); - } - - if (premiumRedeemVaultsResult.status === 'fulfilled' && premiumRedeemVaultsResult.value.size > 0) { - // Premium redeem vaults are refetched on submission so we only need to set - // true/false rather than keep them in state. No need to set false as this is - // set as a default on render. - setHasPremiumRedeemVaults(true); - } - if ( - vaultsWithRedeemableTokensResult.status === 'fulfilled' && - vaultsWithRedeemableTokensResult.value.size > 0 - ) { - // Find the vault with the largest capacity - const theMaxRedeemableCapacity = vaultsWithRedeemableTokensResult.value.values().next().value; - setMaxRedeemableCapacity(theMaxRedeemableCapacity); - } - if (btcToRelayChainNativeTokenRateResult.status === 'fulfilled') { - setBtcToRelayChainNativeTokenRate(btcToRelayChainNativeTokenRateResult.value); - } - - setDustValue(dustValueResult.value); - setPremiumRedeemFee(new Big(premiumRedeemFeeRateResult.value)); - setRedeemFeeRate(feeRateResult.value); - setCurrentInclusionFee(currentInclusionFeeResult.value); - setStatus(STATUSES.RESOLVED); - } catch (error) { - setStatus(STATUSES.REJECTED); - handleError(error); - } - })(); - }, [bridgeLoaded, handleError, setError, t]); - - if (status === STATUSES.IDLE || status === STATUSES.PENDING) { - return ; - } - - if (status === STATUSES.RESOLVED) { - const handleSubmittedRequestModalOpen = (newSubmittedRequest: Redeem) => { - setSubmittedRequest(newSubmittedRequest); - }; - const handleSubmittedRequestModalClose = () => { - setSubmittedRequest(undefined); - }; - - const handleSelectVaultCheckboxChange = () => { - if (!isSelectVaultCheckboxDisabled) { - setSelectVaultManually((prev) => !prev); - } - }; - - const onSubmit = async (data: RedeemFormData) => { - try { - setSubmitStatus(STATUSES.PENDING); - const monetaryWrappedTokenAmount = new BitcoinAmount(data[WRAPPED_TOKEN_AMOUNT]); - - // Differentiate between premium and regular redeem - let vaultId: InterbtcPrimitivesVaultId; - if (premiumRedeemSelected) { - const premiumRedeemVaults = await window.bridge.vaults.getPremiumRedeemVaults(); - // Select a vault from the premium redeem vault list - for (const [id, redeemableTokenAmount] of premiumRedeemVaults) { - if (redeemableTokenAmount.gte(monetaryWrappedTokenAmount)) { - vaultId = id; - break; - } - } - if (vaultId === undefined) { - let maxAmount = BitcoinAmount.zero(); - for (const redeemableTokenAmount of premiumRedeemVaults.values()) { - if (maxAmount.lt(redeemableTokenAmount)) { - maxAmount = redeemableTokenAmount; - } - } - setError(WRAPPED_TOKEN_AMOUNT, { - type: 'manual', - message: t('redeem_page.error_max_premium_redeem', { - maxPremiumRedeem: displayMonetaryAmount(maxAmount), - wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL - }) - }); - - return; - } - } else { - const updatedVaults = await window.bridge.vaults.getVaultsWithRedeemableTokens(); - const updatedMaxCapacity = updatedVaults.values().next().value; - - if (monetaryWrappedTokenAmount.gte(updatedMaxCapacity)) { - setError(WRAPPED_TOKEN_AMOUNT, { - type: 'manual', - message: t('redeem_page.request_exceeds_capacity', { - maxRedeemableAmount: `${displayMonetaryAmount(maxRedeemableCapacity)} BTC` - }) - }); - - setSubmitStatus(STATUSES.RESOLVED); - - return; - } - - if (selectVaultManually) { - if (!selectedVault) { - throw new Error('Specific vault is not selected!'); - } - vaultId = selectedVault[0]; - } else { - vaultId = getRandomVaultIdWithCapacity(Array.from(updatedVaults || new Map()), monetaryWrappedTokenAmount); - } - } - - // FIXME: workaround to make premium redeem still possible - const relevantVaults = new Map(); - // FIXME: a bit of a dirty workaround with the capacity - relevantVaults.set(vaultId, monetaryWrappedTokenAmount.mul(2)); - - const result = await transaction.executeAsync(monetaryWrappedTokenAmount, data[BTC_ADDRESS], vaultId); - - const redeemRequests = await getRedeemRequestsFromExtrinsicResult(window.bridge, result.data); - - // TODO: handle redeem aggregator - const redeemRequest = redeemRequests[0]; - handleSubmittedRequestModalOpen(redeemRequest); - setSubmitStatus(STATUSES.RESOLVED); - } catch (error) { - setSubmitStatus(STATUSES.REJECTED); - } - }; - - const validateForm = (value: string): string | undefined => { - const monetaryValue = new BitcoinAmount(value); - - const wrappedTokenBalance = balances?.[WRAPPED_TOKEN.ticker].free || newMonetaryAmount(0, WRAPPED_TOKEN); - if (monetaryValue.gt(wrappedTokenBalance)) { - return `${t('redeem_page.current_balance')}${displayMonetaryAmount(wrappedTokenBalance)}`; - } - - if (monetaryValue.gt(maxRedeemableCapacity)) { - return `${t('redeem_page.request_exceeds_capacity', { - maxRedeemableAmount: `${maxRedeemableCapacity.toHuman(8)} ${ForeignAssetIdLiteral.BTC}`, - btcIdLiteral: `${ForeignAssetIdLiteral.BTC}` - })}`; - } - - const bridgeFee = monetaryValue.mul(redeemFeeRate); - const minValue = dustValue.add(currentInclusionFee).add(bridgeFee); - if (monetaryValue.lte(minValue)) { - return `${t('redeem_page.amount_greater_dust_inclusion')}${displayMonetaryAmount(minValue)} BTC).`; - } - - if (bitcoinHeight - btcRelayHeight > BLOCKS_BEHIND_LIMIT) { - return t('redeem_page.error_more_than_6_blocks_behind', { - wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL - }); - } - - const wrappedTokenAmountInteger = value.toString().split('.')[0]; - if (wrappedTokenAmountInteger.length > BALANCE_MAX_INTEGER_LENGTH) { - return 'Input value is too high!'; - } - - if (isOracleOffline) { - return t('error_oracle_offline', { action: 'redeem', wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL }); - } - - return undefined; - }; - - const handlePremiumRedeemToggle = () => { - // TODO: should not use redux - dispatch(togglePremiumRedeemAction(!premiumRedeemSelected)); - }; - - const bridgeFeeInBTC = bridgeFee.toHuman(8); - const bridgeFeeInUSD = displayMonetaryAmountInUSDFormat( - bridgeFee, - getTokenPrice(prices, ForeignAssetIdLiteral.BTC)?.usd - ); - - const total = monetaryWrappedTokenAmount.gt(bridgeFee.add(currentInclusionFee)) - ? monetaryWrappedTokenAmount.sub(bridgeFee).sub(currentInclusionFee) - : BitcoinAmount.zero(); - const totalInBTC = total.toHuman(8); - const totalInUSD = displayMonetaryAmountInUSDFormat(total, getTokenPrice(prices, ForeignAssetIdLiteral.BTC)?.usd); - - const totalRelayChainNativeToken = monetaryWrappedTokenAmount.gt(BitcoinAmount.zero()) - ? btcToRelayChainNativeTokenRate.toCounter(monetaryWrappedTokenAmount).mul(premiumRedeemFee) - : newMonetaryAmount(0, RELAY_CHAIN_NATIVE_TOKEN); - const totalRelayChainNativeTokenInUSD = displayMonetaryAmountInUSDFormat( - totalRelayChainNativeToken, - getTokenPrice(prices, RELAY_CHAIN_NATIVE_TOKEN_SYMBOL)?.usd - ); - - const bitcoinNetworkFeeInBTC = currentInclusionFee.toHuman(8); - const bitcoinNetworkFeeInUSD = displayMonetaryAmountInUSDFormat( - currentInclusionFee, - getTokenPrice(prices, ForeignAssetIdLiteral.BTC)?.usd - ); - - // `btcToDotRate` has 0 value only if oracle call fails - const isOracleOffline = btcToRelayChainNativeTokenRate.toBig().eq(0); - - const isSelectVaultCheckboxDisabled = monetaryWrappedTokenAmount.gt(maxRedeemableCapacity); - - return ( - <> -
- - {t('redeem_page.you_will_receive', { - wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL - })} - -
- - validateForm(value) - })} - approxUSD={`≈ ${displayMonetaryAmountInUSDFormat(monetaryWrappedTokenAmount, usdPrice)}`} - error={!!errors[WRAPPED_TOKEN_AMOUNT]} - helperText={errors[WRAPPED_TOKEN_AMOUNT]?.message} - /> -
- - {!premiumRedeemSelected && ( - - )} - - {hasPremiumRedeemVaults && ( -
-
- {t('redeem_page.premium_redeem')} - -
- -
- )} - - {t('you_will_receive')} - - } - unitIcon={} - dataTestId='total-receiving-amount' - value={totalInBTC} - unitName='BTC' - approxUSD={totalInUSD} - /> - - - {t('bridge_fee')} - - } - unitIcon={} - dataTestId='redeem-bridge-fee' - value={bridgeFeeInBTC} - unitName='BTC' - approxUSD={bridgeFeeInUSD} - /> - - {t('bitcoin_network_fee')} - - } - unitIcon={} - dataTestId='redeem-bitcoin-network-fee' - value={bitcoinNetworkFeeInBTC} - unitName='BTC' - approxUSD={bitcoinNetworkFeeInUSD} - /> - {premiumRedeemSelected && ( - {t('redeem_page.earned_premium')}} - unitIcon={} - value={displayMonetaryAmount(totalRelayChainNativeToken)} - unitName={RELAY_CHAIN_NATIVE_TOKEN_SYMBOL} - approxUSD={totalRelayChainNativeTokenInUSD} - /> - )} - - {t('confirm')} - - - {submittedRequest && ( - - )} - - ); - } - - return null; -}; - -export { BTC_ADDRESS_LABEL }; - -export default withErrorBoundary(RedeemForm, { - FallbackComponent: ErrorFallback, - onReset: () => { - window.location.reload(); - } -}); diff --git a/src/pages/Bridge/index.tsx b/src/pages/Bridge/index.tsx index 50a3bc78e1..4624eb9420 100644 --- a/src/pages/Bridge/index.tsx +++ b/src/pages/Bridge/index.tsx @@ -1,150 +1,3 @@ -import { BitcoinAmount } from '@interlay/monetary-js'; -import clsx from 'clsx'; -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; - -import { StoreType } from '@/common/types/util.types'; -import Hr1 from '@/legacy-components/hrs/Hr1'; -import Panel from '@/legacy-components/Panel'; -import InterlayTabGroup, { - InterlayTab, - InterlayTabList, - InterlayTabPanel, - InterlayTabPanels -} from '@/legacy-components/UI/InterlayTabGroup'; -import MainContainer from '@/parts/MainContainer'; -import { QUERY_PARAMETERS } from '@/utils/constants/links'; -import TAB_IDS from '@/utils/constants/tab-ids'; -import { useGetCollateralCurrencies } from '@/utils/hooks/api/use-get-collateral-currencies'; -import useQueryParams from '@/utils/hooks/use-query-params'; -import useUpdateQueryParameters, { QueryParameters } from '@/utils/hooks/use-update-query-parameters'; - -import BurnForm from './BurnForm'; -import IssueForm from './IssueForm'; -import RedeemForm from './RedeemForm'; - -const TAB_ITEMS_WITHOUT_BURN = [ - { - id: TAB_IDS.issue, - label: 'issue' - }, - { - id: TAB_IDS.redeem, - label: 'redeem' - } -]; - -const TAB_ITEMS_WITH_BURN = [ - ...TAB_ITEMS_WITHOUT_BURN, - { - id: TAB_IDS.burn, - label: 'burn' - } -]; - -const Bridge = (): JSX.Element | null => { - const { t } = useTranslation(); - const { bridgeLoaded } = useSelector((state: StoreType) => state.general); - - const queryParams = useQueryParams(); - const selectedTabId = queryParams.get(QUERY_PARAMETERS.TAB); - const updateQueryParameters = useUpdateQueryParameters(); - - const [burnable, setBurnable] = React.useState(false); - - const { data: collateralCurrencies } = useGetCollateralCurrencies(bridgeLoaded); - - const updateQueryParametersRef = React.useRef<(newQueryParameters: QueryParameters) => void>(); - // MEMO: inspired by https://epicreact.dev/the-latest-ref-pattern-in-react/ - React.useLayoutEffect(() => { - updateQueryParametersRef.current = updateQueryParameters; - }); - - React.useEffect(() => { - if (!updateQueryParametersRef.current) return; - - const tabIdValues = Object.values(TAB_IDS); - switch (true) { - case selectedTabId === null: - case selectedTabId === TAB_IDS.burn && !burnable: - case selectedTabId && !tabIdValues.includes(selectedTabId): - updateQueryParametersRef.current({ - [QUERY_PARAMETERS.TAB]: TAB_IDS.issue - }); - } - }, [selectedTabId, burnable]); - - React.useEffect(() => { - if (!bridgeLoaded) return; - if (!collateralCurrencies) return; - - (async () => { - try { - const burnableTokens = await Promise.all( - collateralCurrencies.map(async (collateral) => { - const maxBurnable = await window.bridge.redeem.getMaxBurnableTokens(collateral); - - return maxBurnable.gt(BitcoinAmount.zero()); - }) - ); - - setBurnable(burnableTokens.includes(true)); - } catch (error) { - // TODO: should add error handling - console.log('[Bridge] error => ', error); - } - })(); - }, [bridgeLoaded, collateralCurrencies]); - - if (selectedTabId === null) { - return null; - } - - const TAB_ITEMS = burnable ? TAB_ITEMS_WITH_BURN : TAB_ITEMS_WITHOUT_BURN; - - const selectedTabIndex = TAB_ITEMS.findIndex((tabItem) => tabItem.id === selectedTabId); - - const handleTabSelect = (index: number) => { - updateQueryParameters({ - [QUERY_PARAMETERS.TAB]: TAB_ITEMS[index].id - }); - }; - - return ( - - - { - handleTabSelect(index); - }} - > - - {TAB_ITEMS.map((tabItem) => ( - - {t(tabItem.label)} - - ))} - - - - - - - - - - {burnable && ( - - - - )} - - - - - ); -}; +import Bridge from './Bridge'; export default Bridge; diff --git a/src/pages/Loans/LoansOverview/components/CollateralModal/CollateralModal.tsx b/src/pages/Loans/LoansOverview/components/CollateralModal/CollateralModal.tsx index 9c46a763c8..934b8db2d7 100644 --- a/src/pages/Loans/LoansOverview/components/CollateralModal/CollateralModal.tsx +++ b/src/pages/Loans/LoansOverview/components/CollateralModal/CollateralModal.tsx @@ -1,15 +1,22 @@ import { CollateralPosition, LoanAsset } from '@interlay/interbtc-api'; +import { useEffect, useRef } from 'react'; import { TFunction, useTranslation } from 'react-i18next'; -import { Flex, Modal, ModalBody, ModalFooter, ModalHeader, ModalProps, Status } from '@/component-library'; -import { AuthCTA } from '@/components'; +import { CTA, Flex, Modal, ModalBody, ModalFooter, ModalHeader, ModalProps, Status } from '@/component-library'; +import { AuthCTA, TransactionFeeDetails } from '@/components'; +import { + LOAN_TOGGLE_COLLATERAL_FEE_TOKEN_FIELD, + toggleCollateralLoanSchema, + ToggleCollateralLoansFormData, + useForm +} from '@/lib/form'; 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 { isTransactionFormDisabled } from '@/utils/hooks/transaction/utils/form'; import { useGetLTV } from '../../hooks/use-get-ltv'; import { BorrowLimit } from '../BorrowLimit'; -import { LoanActionInfo } from '../LoanActionInfo'; import { StyledDescription } from './CollateralModal.style'; type CollateralModalVariant = 'enable' | 'disable' | 'disable-error'; @@ -45,8 +52,8 @@ const getModalVariant = (isCollateralActive: boolean, ltvStatus?: Status): Colla }; type Props = { - asset?: LoanAsset; - position?: CollateralPosition; + asset: LoanAsset; + position: CollateralPosition; }; type InheritAttrs = Omit; @@ -59,28 +66,20 @@ const CollateralModal = ({ asset, position, onClose, ...props }: CollateralModal const { getLTV } = useGetLTV(); const prices = useGetPrices(); + const overlappingModalRef = useRef(null); + const transaction = useTransaction({ onSigning: onClose, onSuccess: refetch }); - if (!asset || !position) { - return null; - } - const { isCollateral: isCollateralActive, amount: lendPositionAmount } = position; const loanAction = isCollateralActive ? 'withdraw' : 'lend'; const currentLTV = getLTV({ type: loanAction, amount: lendPositionAmount }); const variant = getModalVariant(isCollateralActive, currentLTV?.status); - const content = getContentMap(t, variant, asset); - - const handleClickBtn = () => { - if (variant === 'disable-error') { - return onClose?.(); - } - + const handleSubmit = () => { if (variant === 'enable') { return transaction.execute(Transaction.LOANS_ENABLE_COLLATERAL, asset.currency); } else { @@ -88,20 +87,70 @@ const CollateralModal = ({ asset, position, onClose, ...props }: CollateralModal } }; + const form = useForm({ + initialValues: { + [LOAN_TOGGLE_COLLATERAL_FEE_TOKEN_FIELD]: '' + }, + validationSchema: toggleCollateralLoanSchema(), + onSubmit: handleSubmit, + onComplete: async (values) => { + const feeTicker = values[LOAN_TOGGLE_COLLATERAL_FEE_TOKEN_FIELD]; + + if (variant === 'enable') { + return transaction.fee.setCurrency(feeTicker).estimate(Transaction.LOANS_ENABLE_COLLATERAL, asset.currency); + } else { + return transaction.fee.setCurrency(feeTicker).estimate(Transaction.LOANS_DISABLE_COLLATERAL, asset.currency); + } + } + }); + + // Doing this call on mount so that the form becomes dirty + // TODO: find better approach + useEffect(() => { + if (variant === 'disable-error') return; + + form.setFieldValue(LOAN_TOGGLE_COLLATERAL_FEE_TOKEN_FIELD, transaction.fee.defaultCurrency.ticker, true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const content = getContentMap(t, variant, asset); + + const isBtnDisabled = isTransactionFormDisabled(form, transaction.fee); + return ( - + !overlappingModalRef.current?.contains(el)} + {...props} + > {content.title} {content.description} - {variant !== 'disable-error' && } - - {content.buttonLabel} - + {variant === 'disable-error' ? ( + + {content.buttonLabel} + + ) : ( +
+ + + + {content.buttonLabel} + + +
+ )}
); diff --git a/src/pages/Loans/LoansOverview/components/LoanActionInfo/LoanActionInfo.style.tsx b/src/pages/Loans/LoansOverview/components/LoanActionInfo/LoanActionInfo.style.tsx deleted file mode 100644 index f4a56eb18a..0000000000 --- a/src/pages/Loans/LoansOverview/components/LoanActionInfo/LoanActionInfo.style.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import styled from 'styled-components'; - -import { Dl, theme } from '@/component-library'; - -const StyledDl = styled(Dl)` - background-color: ${theme.card.bg.secondary}; - padding: ${theme.spacing.spacing4}; - font-size: ${theme.text.xs}; - border-radius: ${theme.rounded.rg}; -`; - -export { StyledDl }; diff --git a/src/pages/Loans/LoansOverview/components/LoanActionInfo/LoanActionInfo.tsx b/src/pages/Loans/LoansOverview/components/LoanActionInfo/LoanActionInfo.tsx deleted file mode 100644 index 9fde09bd7e..0000000000 --- a/src/pages/Loans/LoansOverview/components/LoanActionInfo/LoanActionInfo.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { LoanAsset } from '@interlay/interbtc-api'; - -import { displayMonetaryAmount, displayMonetaryAmountInUSDFormat } from '@/common/utils/utils'; -import { Dd, DlGroup, Dt } from '@/component-library'; -import { TRANSACTION_FEE_AMOUNT } from '@/config/relay-chains'; -import { LoanAction } from '@/types/loans'; -import { getTokenPrice } from '@/utils/helpers/prices'; -import { Prices } from '@/utils/hooks/api/use-get-prices'; - -import { StyledDl } from './LoanActionInfo.style'; -import { LoanGroup } from './LoanGroup'; - -type LoanActionInfoProps = { - variant?: LoanAction; - asset?: LoanAsset; - prices?: Prices; -}; - -const LoanActionInfo = ({ variant, asset, prices }: LoanActionInfoProps): JSX.Element => ( - - {(variant === 'borrow' || variant === 'lend') && } - -
Fees
-
- {displayMonetaryAmount(TRANSACTION_FEE_AMOUNT)} {TRANSACTION_FEE_AMOUNT.currency.ticker} ( - {displayMonetaryAmountInUSDFormat( - TRANSACTION_FEE_AMOUNT, - getTokenPrice(prices, TRANSACTION_FEE_AMOUNT.currency.ticker)?.usd - )} - ) -
-
-
-); -export { LoanActionInfo }; -export type { LoanActionInfoProps }; diff --git a/src/pages/Loans/LoansOverview/components/LoanActionInfo/LoanGroup.tsx b/src/pages/Loans/LoansOverview/components/LoanActionInfo/LoanGroup.tsx deleted file mode 100644 index dbd6c7db93..0000000000 --- a/src/pages/Loans/LoansOverview/components/LoanActionInfo/LoanGroup.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { LoanAsset } from '@interlay/interbtc-api'; - -import { Dd, DlGroup, Dt } from '@/component-library'; -import { LoanAction } from '@/types/loans'; -import { getApyLabel } from '@/utils/helpers/loans'; -import { Prices } from '@/utils/hooks/api/use-get-prices'; - -import { RewardsGroup } from './RewardsGroup'; - -type LoanGroupProps = { - variant?: Extract; - asset?: LoanAsset; - prices?: Prices; -}; - -const LoanGroup = ({ variant, asset, prices }: LoanGroupProps): JSX.Element | null => { - const isBorrow = variant === 'borrow'; - const apy = isBorrow ? asset?.borrowApy : asset?.lendApy; - - if (!apy || !asset) { - return null; - } - - const rewards = isBorrow ? asset?.borrowReward : asset?.lendReward; - - return ( - <> - -
- {isBorrow ? 'Borrow' : 'Lend'} APY {asset.currency.ticker} -
-
{getApyLabel(apy)}
-
- {!!rewards && ( - - )} - - ); -}; - -export { LoanGroup }; -export type { LoanGroupProps }; diff --git a/src/pages/Loans/LoansOverview/components/LoanActionInfo/RewardsGroup.tsx b/src/pages/Loans/LoansOverview/components/LoanActionInfo/RewardsGroup.tsx deleted file mode 100644 index 6dd8c5372b..0000000000 --- a/src/pages/Loans/LoansOverview/components/LoanActionInfo/RewardsGroup.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { CurrencyExt } from '@interlay/interbtc-api'; -import { MonetaryAmount } from '@interlay/monetary-js'; -import Big from 'big.js'; - -import { Dd, DlGroup, Dt } from '@/component-library'; -import { getApyLabel, getSubsidyRewardApy } from '@/utils/helpers/loans'; -import { Prices } from '@/utils/hooks/api/use-get-prices'; - -type RewardsGroupProps = { - apy: Big; - rewards: MonetaryAmount; - assetCurrency: CurrencyExt; - isBorrow: boolean; - prices?: Prices; -}; - -const RewardsGroup = ({ isBorrow, apy, assetCurrency, rewards, prices }: RewardsGroupProps): JSX.Element | null => { - const subsidyRewardApy = getSubsidyRewardApy(assetCurrency, rewards, prices); - - if (!subsidyRewardApy) { - return null; - } - - const totalApy = isBorrow ? apy.sub(subsidyRewardApy) : apy.add(subsidyRewardApy); - - return ( - <> - -
Rewards APR {rewards.currency.ticker}
-
{getApyLabel(subsidyRewardApy)}
-
- -
Total APY
-
{getApyLabel(totalApy)}
-
- - ); -}; -export { RewardsGroup }; -export type { RewardsGroupProps }; diff --git a/src/pages/Loans/LoansOverview/components/LoanActionInfo/index.tsx b/src/pages/Loans/LoansOverview/components/LoanActionInfo/index.tsx deleted file mode 100644 index 8aef992fc2..0000000000 --- a/src/pages/Loans/LoansOverview/components/LoanActionInfo/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export type { LoanActionInfoProps } from './LoanActionInfo'; -export { LoanActionInfo } from './LoanActionInfo'; diff --git a/src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.styles.tsx b/src/pages/Loans/LoansOverview/components/LoanDetails/LoanDetails.style.tsx similarity index 100% rename from src/pages/AMM/Pools/components/WithdrawForm/WithdrawForm.styles.tsx rename to src/pages/Loans/LoansOverview/components/LoanDetails/LoanDetails.style.tsx diff --git a/src/pages/Loans/LoansOverview/components/LoanDetails/LoanDetails.tsx b/src/pages/Loans/LoansOverview/components/LoanDetails/LoanDetails.tsx new file mode 100644 index 0000000000..dc7ae9a09f --- /dev/null +++ b/src/pages/Loans/LoansOverview/components/LoanDetails/LoanDetails.tsx @@ -0,0 +1,47 @@ +import { LoanAsset } from '@interlay/interbtc-api'; + +import { TransactionDetails, TransactionDetailsDd, TransactionDetailsDt, TransactionDetailsGroup } from '@/components'; +import { LoanAction } from '@/types/loans'; +import { Prices } from '@/utils/hooks/api/use-get-prices'; + +import { getApyLabel } from '../../utils/apy'; +import { RewardsDetails } from './RewardsDetails'; + +type LoanDetailsProps = { + variant?: LoanAction; + asset?: LoanAsset; + prices?: Prices; +}; + +const LoanDetails = ({ variant, asset, prices }: LoanDetailsProps): JSX.Element | null => { + const isBorrow = variant === 'borrow'; + const apy = isBorrow ? asset?.borrowApy : asset?.lendApy; + + if (!apy || !asset) { + return null; + } + + const rewards = isBorrow ? asset?.borrowReward : asset?.lendReward; + + return ( + + + + {isBorrow ? 'Borrow' : 'Lend'} APY {asset.currency.ticker} + + {getApyLabel(apy)} + + {!!rewards && ( + + )} + + ); +}; +export { LoanDetails }; +export type { LoanDetailsProps }; diff --git a/src/pages/Loans/LoansOverview/components/LoanDetails/RewardsDetails.tsx b/src/pages/Loans/LoansOverview/components/LoanDetails/RewardsDetails.tsx new file mode 100644 index 0000000000..fcd5cc2462 --- /dev/null +++ b/src/pages/Loans/LoansOverview/components/LoanDetails/RewardsDetails.tsx @@ -0,0 +1,40 @@ +import { CurrencyExt } from '@interlay/interbtc-api'; +import { MonetaryAmount } from '@interlay/monetary-js'; +import Big from 'big.js'; + +import { TransactionDetailsDd, TransactionDetailsDt, TransactionDetailsGroup } from '@/components'; +import { getApyLabel, getSubsidyRewardApy } from '@/utils/helpers/loans'; +import { Prices } from '@/utils/hooks/api/use-get-prices'; + +type RewardsDetailsProps = { + apy: Big; + rewards: MonetaryAmount; + assetCurrency: CurrencyExt; + isBorrow: boolean; + prices?: Prices; +}; + +const RewardsDetails = ({ isBorrow, apy, assetCurrency, rewards, prices }: RewardsDetailsProps): JSX.Element | null => { + const subsidyRewardApy = getSubsidyRewardApy(assetCurrency, rewards, prices); + + if (!subsidyRewardApy) { + return null; + } + + const totalApy = isBorrow ? apy.sub(subsidyRewardApy) : apy.add(subsidyRewardApy); + + return ( + <> + + Rewards APR {rewards.currency.ticker} + {getApyLabel(subsidyRewardApy)} + + + Total APY + {getApyLabel(totalApy)} + + + ); +}; +export { RewardsDetails }; +export type { RewardsDetailsProps }; diff --git a/src/pages/Loans/LoansOverview/components/LoanDetails/index.tsx b/src/pages/Loans/LoansOverview/components/LoanDetails/index.tsx new file mode 100644 index 0000000000..0e313aad82 --- /dev/null +++ b/src/pages/Loans/LoansOverview/components/LoanDetails/index.tsx @@ -0,0 +1,2 @@ +export type { LoanDetailsProps } from './LoanDetails'; +export { LoanDetails } from './LoanDetails'; diff --git a/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx b/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx index 390be8cba5..3b7c53160e 100644 --- a/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx +++ b/src/pages/Loans/LoansOverview/components/LoanForm/LoanForm.tsx @@ -1,23 +1,31 @@ import { BorrowPosition, CollateralPosition, CurrencyExt, LoanAsset, newMonetaryAmount } from '@interlay/interbtc-api'; import { MonetaryAmount } from '@interlay/monetary-js'; import { mergeProps } from '@react-aria/utils'; -import { ChangeEventHandler, useState } from 'react'; +import { ChangeEventHandler, RefObject, useCallback, useState } from 'react'; import { TFunction, useTranslation } from 'react-i18next'; import { useDebounce } from 'react-use'; import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/common/utils/utils'; import { Flex, TokenInput } from '@/component-library'; -import { AuthCTA } from '@/components'; -import { isFormDisabled, LoanFormData, loanSchema, LoanValidationParams, useForm } from '@/lib/form'; +import { AuthCTA, TransactionFeeDetails } from '@/components'; +import { + LOAN_AMOUNT_FIELD, + LOAN_FEE_TOKEN_FIELD, + LoanFormData, + loanSchema, + LoanValidationParams, + useForm +} from '@/lib/form'; import { LoanAction } from '@/types/loans'; import { useGetAccountPositions } from '@/utils/hooks/api/loans/use-get-account-positions'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; import { Transaction, useTransaction } from '@/utils/hooks/transaction'; +import { isTransactionFormDisabled } from '@/utils/hooks/transaction/utils/form'; import { useLoanFormData } from '../../hooks/use-loan-form-data'; import { isLendAsset } from '../../utils/is-loan-asset'; import { BorrowLimit } from '../BorrowLimit'; -import { LoanActionInfo } from '../LoanActionInfo'; +import { LoanDetails } from '../LoanDetails'; import { StyledFormWrapper } from './LoanForm.style'; // The borrow limit component is only displayed when @@ -72,10 +80,11 @@ type LoanFormProps = { asset: LoanAsset; variant: LoanAction; position?: BorrowPosition | CollateralPosition; + overlappingModalRef: RefObject; onChangeLoan?: () => void; }; -const LoanForm = ({ asset, variant, position, onChangeLoan }: LoanFormProps): JSX.Element => { +const LoanForm = ({ asset, variant, position, overlappingModalRef, onChangeLoan }: LoanFormProps): JSX.Element => { const [inputAmount, setInputAmount] = useState(); const [isMaxAmount, setMaxAmount] = useState(false); @@ -85,7 +94,7 @@ const LoanForm = ({ asset, variant, position, onChangeLoan }: LoanFormProps): JS data: { hasCollateral } } = useGetAccountPositions(); const prices = useGetPrices(); - const { governanceBalance, assetAmount, assetPrice, transactionFee } = useLoanFormData(variant, asset, position); + const { assetAmount, assetPrice } = useLoanFormData(variant, asset, position); const { content } = getData(t, variant); @@ -117,9 +126,22 @@ const LoanForm = ({ asset, variant, position, onChangeLoan }: LoanFormProps): JS const transaction = useTransaction({ onSigning: onChangeLoan, onSuccess: refetch }); + const getTransactionArgs = useCallback( + (values: LoanFormData) => { + const amount = values[LOAN_AMOUNT_FIELD] || 0; + const monetaryAmount = newMonetaryAmount(amount, asset.currency, true); + + return { monetaryAmount }; + }, + [asset.currency] + ); + const handleSubmit = (data: LoanFormData) => { - const amount = data[variant] || 0; - const monetaryAmount = newMonetaryAmount(amount, asset.currency, true); + const transactionData = getTransactionArgs(data); + + if (!transactionData) return; + + const { monetaryAmount } = transactionData; switch (variant) { case 'lend': @@ -134,7 +156,7 @@ const LoanForm = ({ asset, variant, position, onChangeLoan }: LoanFormProps): JS return transaction.execute(Transaction.LOANS_BORROW, monetaryAmount.currency, monetaryAmount); case 'repay': if (isMaxAmount) { - return transaction.execute(Transaction.LOANS_REPAY_ALL, monetaryAmount.currency); + return transaction.execute(Transaction.LOANS_REPAY_ALL, monetaryAmount.currency, assetAmount.available); } else { return transaction.execute(Transaction.LOANS_REPAY, monetaryAmount.currency, monetaryAmount); } @@ -142,21 +164,65 @@ const LoanForm = ({ asset, variant, position, onChangeLoan }: LoanFormProps): JS }; const schemaParams: LoanValidationParams = { - governanceBalance, - transactionFee, minAmount: assetAmount.min, maxAmount: assetAmount.available }; const form = useForm({ - initialValues: { [variant]: '' }, + initialValues: { [LOAN_AMOUNT_FIELD]: '', [LOAN_FEE_TOKEN_FIELD]: transaction.fee.defaultCurrency.ticker }, validationSchema: loanSchema(variant, schemaParams), - onSubmit: handleSubmit + onSubmit: handleSubmit, + onComplete: (values) => { + const transactionData = getTransactionArgs(values); + + if (!transactionData) return; + + const { monetaryAmount } = transactionData; + + const feeTicker = values[LOAN_FEE_TOKEN_FIELD]; + + switch (variant) { + case 'lend': + return transaction.fee + .setCurrency(feeTicker) + .estimate(Transaction.LOANS_LEND, monetaryAmount.currency, monetaryAmount); + case 'withdraw': { + if (isMaxAmount) { + return transaction.fee + .setCurrency(feeTicker) + .estimate(Transaction.LOANS_WITHDRAW_ALL, monetaryAmount.currency); + } else { + return transaction.fee + .setCurrency(feeTicker) + .estimate(Transaction.LOANS_WITHDRAW, monetaryAmount.currency, monetaryAmount); + } + } + case 'borrow': + return transaction.fee + .setCurrency(feeTicker) + .estimate(Transaction.LOANS_BORROW, monetaryAmount.currency, monetaryAmount); + + case 'repay': { + if (isMaxAmount) { + return ( + transaction.fee + .setCurrency(feeTicker) + // passing the limit calculated, so it can be used in the validation in transaction hook + .estimate(Transaction.LOANS_REPAY_ALL, monetaryAmount.currency, assetAmount.available) + ); + } else { + return transaction.fee + .setCurrency(feeTicker) + .estimate(Transaction.LOANS_REPAY, monetaryAmount.currency, monetaryAmount); + } + } + } + } }); - const monetaryAmount = newSafeMonetaryAmount(form.values[variant] || 0, asset.currency, true); + const monetaryAmount = newSafeMonetaryAmount(form.values[LOAN_AMOUNT_FIELD] || 0, asset.currency, true); - const isBtnDisabled = isFormDisabled(form); + const isBtnDisabled = isTransactionFormDisabled(form, transaction.fee); const handleClickBalance = () => { if (!hasMultiActionVariant) return; @@ -175,7 +241,7 @@ const LoanForm = ({ asset, variant, position, onChangeLoan }: LoanFormProps): JS return (
- + {showBorrowLimit && ( - + {(variant === 'lend' || variant === 'borrow') && ( + + )} + {content.title} diff --git a/src/pages/Loans/LoansOverview/components/LoanModal/LoanModal.tsx b/src/pages/Loans/LoansOverview/components/LoanModal/LoanModal.tsx index 43d6ed4254..4066483f1a 100644 --- a/src/pages/Loans/LoansOverview/components/LoanModal/LoanModal.tsx +++ b/src/pages/Loans/LoansOverview/components/LoanModal/LoanModal.tsx @@ -1,4 +1,5 @@ import { BorrowPosition, CollateralPosition, LoanAsset } from '@interlay/interbtc-api'; +import { useRef } from 'react'; import { TFunction, useTranslation } from 'react-i18next'; import { Modal, ModalBody, ModalProps, TabsItem } from '@/component-library'; @@ -42,6 +43,7 @@ type LoanModalProps = Props & InheritAttrs; const LoanModal = ({ variant = 'lend', asset, position, onClose, ...props }: LoanModalProps): JSX.Element | null => { const { t } = useTranslation(); + const overlappingModalRef = useRef(null); if (!asset) { return null; @@ -50,13 +52,25 @@ const LoanModal = ({ variant = 'lend', asset, position, onClose, ...props }: Loa const { tabs } = getData(t, variant); return ( - + !overlappingModalRef.current?.contains(el)} + {...props} + > {tabs.map((tab) => ( - + ))} diff --git a/src/pages/Loans/LoansOverview/components/LoansInsights/LoansInsights.tsx b/src/pages/Loans/LoansOverview/components/LoansInsights/LoansInsights.tsx index 2ef53674f4..b5a9d0cf35 100644 --- a/src/pages/Loans/LoansOverview/components/LoansInsights/LoansInsights.tsx +++ b/src/pages/Loans/LoansOverview/components/LoansInsights/LoansInsights.tsx @@ -1,9 +1,26 @@ +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + import { formatNumber, formatPercentage, formatUSD } from '@/common/utils/utils'; -import { Card, Dl, DlGroup } from '@/component-library'; -import { AuthCTA } from '@/components'; +import { Card, Dl, DlGroup, Flex, Modal, ModalBody, ModalFooter, ModalHeader } from '@/component-library'; +import { + AuthCTA, + TransactionDetails, + TransactionDetailsDd, + TransactionDetailsDt, + TransactionDetailsGroup, + TransactionFeeDetails +} from '@/components'; +import { + claimRewardsLoanSchema, + ClaimRewardsLoansFormData, + LOAN_CLAIM_REWARDS_FEE_TOKEN_FIELD, + useForm +} from '@/lib/form'; 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 { isTransactionFormDisabled } from '@/utils/hooks/transaction/utils/form'; import { StyledDd, StyledDt } from './LoansInsights.style'; @@ -12,13 +29,37 @@ type LoansInsightsProps = { }; const LoansInsights = ({ statistics }: LoansInsightsProps): JSX.Element => { + const { t } = useTranslation(); + const { data: subsidyRewards, refetch } = useGetAccountSubsidyRewards(); + const [isOpen, setOpen] = useState(false); + const overlappingModalRef = useRef(null); + const transaction = useTransaction(Transaction.LOANS_CLAIM_REWARDS, { - onSuccess: refetch + onSuccess: refetch, + onSigning: () => setOpen(false) + }); + + const form = useForm({ + initialValues: { + [LOAN_CLAIM_REWARDS_FEE_TOKEN_FIELD]: '' + }, + validationSchema: claimRewardsLoanSchema(), + onSubmit: () => transaction.execute(), + onComplete: async (values) => { + const feeTicker = values[LOAN_CLAIM_REWARDS_FEE_TOKEN_FIELD]; + + return transaction.fee.setCurrency(feeTicker).estimate(); + } }); - const handleClickClaimRewards = () => transaction.execute(); + // Doing this call on mount so that the form becomes dirty + // TODO: improve approach + useEffect(() => { + form.setFieldValue(LOAN_CLAIM_REWARDS_FEE_TOKEN_FIELD, transaction.fee.defaultCurrency.ticker, true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const { supplyAmountUSD, netAPY } = statistics || {}; @@ -34,6 +75,8 @@ const LoansInsights = ({ statistics }: LoansInsightsProps): JSX.Element => { const subsidyRewardsAmountLabel = `${subsidyRewardsAmount} ${subsidyRewards?.total.currency.ticker || ''}`; const hasSubsidyRewards = !!subsidyRewards && !subsidyRewards?.total.isZero(); + const isModalBtnDisabled = isTransactionFormDisabled(form, transaction.fee); + return ( <>
@@ -62,12 +105,45 @@ const LoansInsights = ({ statistics }: LoansInsightsProps): JSX.Element => { {subsidyRewardsAmountLabel} {hasSubsidyRewards && ( - + setOpen(true)} loading={transaction.isLoading}> Claim )}
+ {hasSubsidyRewards && ( + setOpen(false)} + shouldCloseOnInteractOutside={(el) => !overlappingModalRef.current?.contains(el)} + > + Claim Rewards + + + + Amount + {subsidyRewardsAmountLabel} + + + + + + + + + {t('claim_rewards')} + + + + + + )} ); }; diff --git a/src/pages/Loans/LoansOverview/components/LoansTables/LendTables.tsx b/src/pages/Loans/LoansOverview/components/LoansTables/LendTables.tsx index 09dbdb5d7f..bafb8df232 100644 --- a/src/pages/Loans/LoansOverview/components/LoansTables/LendTables.tsx +++ b/src/pages/Loans/LoansOverview/components/LoansTables/LendTables.tsx @@ -60,12 +60,14 @@ const LendTables = ({ assets, positions, disabledAssets, hasPositions }: LendTab position={selectedAsset.position} onClose={handleClose} /> - + {selectedAsset.data && selectedAsset.position && ( + + )} ); }; diff --git a/src/pages/Loans/LoansOverview/hooks/use-loan-form-data.tsx b/src/pages/Loans/LoansOverview/hooks/use-loan-form-data.tsx index 8db2cf8e30..5a0a850277 100644 --- a/src/pages/Loans/LoansOverview/hooks/use-loan-form-data.tsx +++ b/src/pages/Loans/LoansOverview/hooks/use-loan-form-data.tsx @@ -8,7 +8,6 @@ import { } from '@interlay/interbtc-api'; import { MonetaryAmount } from '@interlay/monetary-js'; -import { GOVERNANCE_TOKEN, TRANSACTION_FEE_AMOUNT } from '@/config/relay-chains'; import { BorrowAction, LendAction, LoanAction } from '@/types/loans'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetAccountLendingStatistics } from '@/utils/hooks/api/loans/use-get-account-lending-statistics'; @@ -48,8 +47,6 @@ const getMaxCalculatedAmount = ({ }; type UseLoanFormData = { - governanceBalance: MonetaryAmount; - transactionFee: MonetaryAmount; assetPrice: number; assetAmount: { available: MonetaryAmount; @@ -58,20 +55,17 @@ type UseLoanFormData = { }; }; -// TODO: reduce GOVERNANCE for fees from max amount const useLoanFormData = ( loanAction: BorrowAction | LendAction, asset: LoanAsset, position?: CollateralPosition | BorrowPosition ): UseLoanFormData => { - const { getBalance, getAvailableBalance } = useGetBalances(); + const { getAvailableBalance } = useGetBalances(); const prices = useGetPrices(); const { data: statistics } = useGetAccountLendingStatistics(); const zeroAssetAmount = newMonetaryAmount(0, asset.currency); - const governanceBalance = getBalance(GOVERNANCE_TOKEN.ticker)?.free || newMonetaryAmount(0, GOVERNANCE_TOKEN); - const transactionFee = TRANSACTION_FEE_AMOUNT; const assetBalance = getAvailableBalance(asset.currency.ticker) || zeroAssetAmount; const assetPrice = getTokenPrice(prices, asset.currency.ticker)?.usd || 0; @@ -94,8 +88,6 @@ const useLoanFormData = ( : maxAmountData; return { - governanceBalance, - transactionFee, assetPrice, assetAmount: { available, diff --git a/src/pages/Transfer/CrossChainTransferForm/components/index.tsx b/src/pages/Transfer/CrossChainTransferForm/components/index.tsx deleted file mode 100644 index 6cb7e0e8e5..0000000000 --- a/src/pages/Transfer/CrossChainTransferForm/components/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { ChainSelect, ChainSelectProps } from './ChainSelect'; - -export { ChainSelect }; -export type { ChainSelectProps }; diff --git a/src/pages/Transfer/CrossChainTransferForm/index.tsx b/src/pages/Transfer/CrossChainTransferForm/index.tsx deleted file mode 100644 index b2417c4fb7..0000000000 --- a/src/pages/Transfer/CrossChainTransferForm/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import CrossChainTransferForm from './CrossChainTransferForm'; - -export default CrossChainTransferForm; diff --git a/src/pages/Transfer/TokenAmountField/index.tsx b/src/pages/Transfer/TokenAmountField/index.tsx deleted file mode 100644 index b47ff6ae4f..0000000000 --- a/src/pages/Transfer/TokenAmountField/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import clsx from 'clsx'; -import * as React from 'react'; - -import NumberInput, { Props as NumberInputProps } from '@/legacy-components/NumberInput'; -import { TextFieldContainer, TextFieldHelperText, TextFieldLabel } from '@/legacy-components/TextField'; -import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; - -interface CustomProps { - error?: boolean; - helperText?: JSX.Element | string; - required?: boolean; - approxUSD?: string; -} - -type Ref = HTMLInputElement; -const TokenAmountField = React.forwardRef( - ({ id, error, helperText, required, approxUSD, ...rest }, ref): JSX.Element => { - return ( -
- - - - - {approxUSD} - - - - {helperText} - -
- ); - } -); - -TokenAmountField.displayName = 'TokenAmountField'; - -export default TokenAmountField; diff --git a/src/pages/Transfer/Transfer.style.tsx b/src/pages/Transfer/Transfer.style.tsx deleted file mode 100644 index 4ec3066518..0000000000 --- a/src/pages/Transfer/Transfer.style.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import styled from 'styled-components'; - -import { theme } from '@/component-library'; - -const StyledWrapper = styled.div` - margin-top: ${theme.spacing.spacing6}; -`; - -export { StyledWrapper }; diff --git a/src/pages/Transfer/Transfer.tsx b/src/pages/Transfer/Transfer.tsx new file mode 100644 index 0000000000..4db6b8d1ba --- /dev/null +++ b/src/pages/Transfer/Transfer.tsx @@ -0,0 +1,16 @@ +import { withErrorBoundary } from 'react-error-boundary'; + +import ErrorFallback from '@/legacy-components/ErrorFallback'; + +import TransferForms from './TransferForms'; + +const Transfer = (): JSX.Element => { + return ; +}; + +export default withErrorBoundary(Transfer, { + FallbackComponent: ErrorFallback, + onReset: () => { + window.location.reload(); + } +}); diff --git a/src/pages/Transfer/TransferForm/index.tsx b/src/pages/Transfer/TransferForm/index.tsx deleted file mode 100644 index 2bc9ed3f19..0000000000 --- a/src/pages/Transfer/TransferForm/index.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { newMonetaryAmount } from '@interlay/interbtc-api'; -import clsx from 'clsx'; -import * as React from 'react'; -import { withErrorBoundary } from 'react-error-boundary'; -import { useForm } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; - -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 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 isValidPolkadotAddress from '@/utils/helpers/is-valid-polkadot-address'; -import { Transaction, useTransaction } from '@/utils/hooks/transaction'; - -import TokenAmountField from '../TokenAmountField'; - -const TRANSFER_AMOUNT = 'transfer-amount'; -const RECIPIENT_ADDRESS = 'recipient-address'; - -type TransferFormData = { - [TRANSFER_AMOUNT]: string; - [RECIPIENT_ADDRESS]: string; -}; - -const TransferForm = (): JSX.Element => { - const { t } = useTranslation(); - - const { parachainStatus } = useSelector((state: StoreType) => state.general); - - const { - register, - handleSubmit, - setValue, - formState: { errors }, - reset - } = useForm({ - mode: 'onChange' - }); - - const [activeToken, setActiveToken] = React.useState(undefined); - - 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; - - transaction.execute(data[RECIPIENT_ADDRESS], newMonetaryAmount(data[TRANSFER_AMOUNT], activeToken.token, true)); - }; - - const validateTransferAmount = React.useCallback( - (value: string): string | undefined => { - if (!activeToken) return; - - const balance = newMonetaryAmount(activeToken.transferableBalance, activeToken.token, true); - const transferAmount = newMonetaryAmount(value, activeToken.token, true); - - return balance.lt(transferAmount) ? t('insufficient_funds') : undefined; - }, - [activeToken, t] - ); - - const validateAddress = React.useCallback( - (address: string): string | undefined => { - return isValidPolkadotAddress(address) ? undefined : t('validation.invalid_polkadot_address'); - }, - [t] - ); - - const handleTokenChange = (token: any) => { - setActiveToken(token); - }; - - const handleClickBalance = () => setValue(TRANSFER_AMOUNT, activeToken?.transferableBalance || ''); - - return ( - <> -
- {t('transfer_page.transfer_currency')} -
-

- Transferable balance: - - {activeToken?.transferableBalance - ? formatNumber(Number(activeToken.transferableBalance), { - minimumFractionDigits: 0, - maximumFractionDigits: 5 - }) - : 0} - -

-
- {/* TODO: use forwardRef to pull in select value as form data */} - - validateTransferAmount(value) - })} - error={!!errors[TRANSFER_AMOUNT]} - helperText={errors[TRANSFER_AMOUNT]?.message} - /> -
-
-
- validateAddress(value) - })} - error={!!errors[RECIPIENT_ADDRESS]} - helperText={errors[RECIPIENT_ADDRESS]?.message} - /> -
- - {t('transfer')} - -
- - ); -}; - -export default withErrorBoundary(TransferForm, { - FallbackComponent: ErrorFallback, - onReset: () => { - window.location.reload(); - } -}); diff --git a/src/pages/Transfer/TransferForms/TransferForms.styles.tsx b/src/pages/Transfer/TransferForms/TransferForms.styles.tsx new file mode 100644 index 0000000000..34d264c703 --- /dev/null +++ b/src/pages/Transfer/TransferForms/TransferForms.styles.tsx @@ -0,0 +1,19 @@ +import styled from 'styled-components'; + +import { Card, Flex, theme } from '@/component-library'; + +const StyledWrapper = styled(Flex)` + max-width: 540px; + width: 100%; + margin: 0 auto; +`; + +const StyledCard = styled(Card)` + width: 100%; +`; + +const StyledFormWrapper = styled.div` + margin-top: ${theme.spacing.spacing8}; +`; + +export { StyledCard, StyledFormWrapper, StyledWrapper }; diff --git a/src/pages/Transfer/TransferForms/TransferForms.tsx b/src/pages/Transfer/TransferForms/TransferForms.tsx new file mode 100644 index 0000000000..7d7cd2b233 --- /dev/null +++ b/src/pages/Transfer/TransferForms/TransferForms.tsx @@ -0,0 +1,30 @@ +import { Flex, Tabs, TabsItem } from '@/component-library'; +import MainContainer from '@/parts/MainContainer'; + +import { CrossChainTransferForm, TransferForm } from './components'; +import { StyledCard, StyledFormWrapper, StyledWrapper } from './TransferForms.styles'; + +const TransferForms = (): JSX.Element => ( + + + + + + + + + + + + + + + + + + + + +); + +export default TransferForms; diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/ChainIcon.style.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/ChainIcon.style.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/ChainIcon.style.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/ChainIcon.style.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/ChainIcon.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/ChainIcon.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/ChainIcon.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/ChainIcon.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Acala.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/icons/Acala.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Acala.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/icons/Acala.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Astar.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/icons/Astar.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Astar.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/icons/Astar.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Bifrost.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/icons/Bifrost.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Bifrost.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/icons/Bifrost.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Heiko.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/icons/Heiko.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Heiko.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/icons/Heiko.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Hydra.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/icons/Hydra.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Hydra.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/icons/Hydra.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Interlay.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/icons/Interlay.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Interlay.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/icons/Interlay.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Karura.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/icons/Karura.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Karura.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/icons/Karura.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Kintsugi.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/icons/Kintsugi.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Kintsugi.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/icons/Kintsugi.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Kusama.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/icons/Kusama.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Kusama.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/icons/Kusama.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Parallel.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/icons/Parallel.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Parallel.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/icons/Parallel.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Polkadot.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/icons/Polkadot.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Polkadot.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/icons/Polkadot.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Statemine.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/icons/Statemine.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Statemine.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/icons/Statemine.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Statemint.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/icons/Statemint.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/Statemint.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/icons/Statemint.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/index.ts b/src/pages/Transfer/TransferForms/components/ChainIcon/icons/index.ts similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/index.ts rename to src/pages/Transfer/TransferForms/components/ChainIcon/icons/index.ts diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/index.tsx b/src/pages/Transfer/TransferForms/components/ChainIcon/index.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/index.tsx rename to src/pages/Transfer/TransferForms/components/ChainIcon/index.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/ChainSelect.style.tsx b/src/pages/Transfer/TransferForms/components/ChainSelect/ChainSelect.style.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/ChainSelect.style.tsx rename to src/pages/Transfer/TransferForms/components/ChainSelect/ChainSelect.style.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/ChainSelect.tsx b/src/pages/Transfer/TransferForms/components/ChainSelect/ChainSelect.tsx similarity index 93% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/ChainSelect.tsx rename to src/pages/Transfer/TransferForms/components/ChainSelect/ChainSelect.tsx index 1506d894e6..7359fb0188 100644 --- a/src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/ChainSelect.tsx +++ b/src/pages/Transfer/TransferForms/components/ChainSelect/ChainSelect.tsx @@ -6,7 +6,7 @@ import { ChainData } from '@/types/chains'; import { ChainIcon } from '../ChainIcon'; import { StyledChain, StyledListChainWrapper, StyledListItemLabel } from './ChainSelect.style'; -type ChainSelectProps = Omit, 'children' | 'type'>; +type ChainSelectProps = Omit, 'children' | 'type'>; const ListItem = ({ data }: { data: ChainData }) => { const isSelected = useSelectModalContext().selectedItem?.key === data.id; @@ -31,7 +31,7 @@ const Value = ({ data }: { data: ChainData }) => ( const ChainSelect = ({ ...props }: ChainSelectProps): JSX.Element => { return ( - + {...props} type='modal' renderValue={(item) => } diff --git a/src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/index.tsx b/src/pages/Transfer/TransferForms/components/ChainSelect/index.tsx similarity index 100% rename from src/pages/Transfer/CrossChainTransferForm/components/ChainSelect/index.tsx rename to src/pages/Transfer/TransferForms/components/ChainSelect/index.tsx diff --git a/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.styles.tsx b/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/CrossChainTransferForm.styles.tsx similarity index 95% rename from src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.styles.tsx rename to src/pages/Transfer/TransferForms/components/CrossChainTransferForm/CrossChainTransferForm.styles.tsx index 1c1ee33b0b..4868a48df7 100644 --- a/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.styles.tsx +++ b/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/CrossChainTransferForm.styles.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { ArrowRightCircle } from '@/assets/icons'; import { Dl, Flex, theme } from '@/component-library'; -import { ChainSelect } from './components'; +import { ChainSelect } from '../ChainSelect'; const StyledDl = styled(Dl)` background-color: ${theme.card.bg.secondary}; diff --git a/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx b/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/CrossChainTransferForm.tsx similarity index 91% rename from src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx rename to src/pages/Transfer/TransferForms/components/CrossChainTransferForm/CrossChainTransferForm.tsx index fbd54a3cd8..eea939163f 100644 --- a/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx +++ b/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/CrossChainTransferForm.tsx @@ -29,7 +29,7 @@ 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'; +import { ChainSelect } from '../ChainSelect'; import { ChainSelectSection, StyledArrowRightCircle, @@ -104,20 +104,16 @@ const CrossChainTransferForm = (): JSX.Element => { validationSchema: crossChainTransferSchema(schema, t) }); - const handleOriginatingChainChange = (chain: ChainName, name: string) => { - form.setFieldValue(name, chain); - + const handleOriginatingChainChange = (chain: ChainName) => { const destinationChains = getDestinationChains(chain); setDestinationChains(destinationChains); form.setFieldValue(CROSS_CHAIN_TRANSFER_TO_FIELD, destinationChains[0].id); }; - const handleDestinationChainChange = async (chain: ChainName, name: string) => { + const handleDestinationChainChange = async (chain: ChainName) => { if (!accountId) return; - form.setFieldValue(name, chain); - setTokenData(chain); }; @@ -169,8 +165,7 @@ const CrossChainTransferForm = (): JSX.Element => { ) : 0; - const isCTADisabled = isFormDisabled(form) || form.values[CROSS_CHAIN_TRANSFER_AMOUNT_FIELD] === ''; - const amountShouldValidate = form.values[CROSS_CHAIN_TRANSFER_AMOUNT_FIELD] !== ''; + const isCTADisabled = isFormDisabled(form); useEffect(() => { if (!originatingChains?.length) return; @@ -219,19 +214,17 @@ const CrossChainTransferForm = (): JSX.Element => { - handleOriginatingChainChange(chain as ChainName, CROSS_CHAIN_TRANSFER_FROM_FIELD) - } - {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_FROM_FIELD, false))} + {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_FROM_FIELD, false), { + onSelectionChange: (key: Key) => handleOriginatingChainChange(key as ChainName) + })} /> - handleDestinationChainChange(chain as ChainName, CROSS_CHAIN_TRANSFER_TO_FIELD) - } - {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_TO_FIELD, false))} + {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_TO_FIELD, false), { + onSelectionChange: (key: Key) => handleDestinationChainChange(key as ChainName) + })} />
@@ -246,7 +239,7 @@ const CrossChainTransferForm = (): JSX.Element => { handleTickerChange(ticker as string, CROSS_CHAIN_TRANSFER_TOKEN_FIELD), items: transferableTokens })} - {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_AMOUNT_FIELD, amountShouldValidate))} + {...mergeProps(form.getFieldProps(CROSS_CHAIN_TRANSFER_AMOUNT_FIELD, false, true))} />
{ ); }; -export default CrossChainTransferForm; +export { CrossChainTransferForm }; diff --git a/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/index.tsx b/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/index.tsx new file mode 100644 index 0000000000..40d3630569 --- /dev/null +++ b/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/index.tsx @@ -0,0 +1 @@ +export { CrossChainTransferForm } from './CrossChainTransferForm'; diff --git a/src/pages/Transfer/TransferForms/components/TransferForm/TransferForm.tsx b/src/pages/Transfer/TransferForms/components/TransferForm/TransferForm.tsx new file mode 100644 index 0000000000..a45b540242 --- /dev/null +++ b/src/pages/Transfer/TransferForms/components/TransferForm/TransferForm.tsx @@ -0,0 +1,162 @@ +import { CurrencyExt, newMonetaryAmount } from '@interlay/interbtc-api'; +import { mergeProps } from '@react-aria/utils'; +import { Key, useCallback, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { StoreType } from '@/common/types/util.types'; +import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/common/utils/utils'; +import { Flex, Input, TokenInput } from '@/component-library'; +import { AuthCTA, TransactionFeeDetails } from '@/components'; +import { GOVERNANCE_TOKEN } from '@/config/relay-chains'; +import { useForm } from '@/lib/form'; +import { + TRANSFER_AMOUNT_FIELD, + TRANSFER_FEE_TOKEN_FIELD, + TRANSFER_RECIPIENT_FIELD, + TRANSFER_TOKEN_FIELD, + TransferFormData, + transferSchema, + TransferValidationParams +} from '@/lib/form/schemas'; +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 { useGetPrices } from '@/utils/hooks/api/use-get-prices'; +import { Transaction, useTransaction } from '@/utils/hooks/transaction'; +import { isTransactionFormDisabled } from '@/utils/hooks/transaction/utils/form'; +import { useSelectCurrency } from '@/utils/hooks/use-select-currency'; + +const TransferForm = (): JSX.Element => { + const { bridgeLoaded } = useSelector((state: StoreType) => state.general); + + const prices = useGetPrices(); + const { getCurrencyFromTicker } = useGetCurrencies(bridgeLoaded); + const { getBalance } = useGetBalances(); + const { items: selectItems } = useSelectCurrency(); + + const [transferToken, setTransferToken] = useState(GOVERNANCE_TOKEN); + + const transaction = useTransaction(Transaction.TOKENS_TRANSFER, { + onSuccess: () => { + form.resetForm(); + } + }); + + const transferTokenBalance = transferToken && getBalance(transferToken.ticker)?.transferable; + + const minAmount = transferToken && newMonetaryAmount(1, transferToken); + + const transferSchemaParams: TransferValidationParams = { + [TRANSFER_AMOUNT_FIELD]: { + maxAmount: transferTokenBalance, + minAmount + } + }; + + const getTransactionArgs = useCallback( + (values: TransferFormData) => { + const destination = values[TRANSFER_RECIPIENT_FIELD]; + const feeTicker = values[TRANSFER_FEE_TOKEN_FIELD]; + + if (!destination) return; + + const amount = newMonetaryAmount(values[TRANSFER_AMOUNT_FIELD] || 0, transferToken, true); + + return { destination, amount, feeTicker }; + }, + [transferToken] + ); + + const handleSubmit = async (values: TransferFormData) => { + const transactionData = getTransactionArgs(values); + + if (!transactionData) return; + + const { amount, destination } = transactionData; + + transaction.execute(destination, amount); + }; + + const initialValues = useMemo( + () => ({ + [TRANSFER_RECIPIENT_FIELD]: '', + [TRANSFER_AMOUNT_FIELD]: '', + [TRANSFER_TOKEN_FIELD]: transferToken.ticker || '', + [TRANSFER_FEE_TOKEN_FIELD]: transaction.fee.defaultCurrency.ticker + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const form = useForm({ + initialValues, + validationSchema: transferSchema(transferSchemaParams), + onSubmit: handleSubmit, + hideErrorMessages: transaction.isLoading, + onComplete: (values) => { + const transactionData = getTransactionArgs(values); + + if (!transactionData) return; + + const { amount, destination, feeTicker } = transactionData; + + transaction.fee.setCurrency(feeTicker).estimate(destination, amount); + } + }); + + const handleTickerChange = (ticker: string, name: string) => { + form.setFieldValue(name, ticker, true); + const currency = getCurrencyFromTicker(ticker); + setTransferToken(currency); + }; + + const transferMonetaryAmount = newSafeMonetaryAmount(form.values[TRANSFER_AMOUNT_FIELD] || 0, transferToken, true); + const transferAmountUSD = transferMonetaryAmount + ? convertMonetaryAmountToValueInUSD( + transferMonetaryAmount, + getTokenPrice(prices, transferMonetaryAmount.currency.ticker)?.usd + ) || 0 + : 0; + + const isBtnDisabled = isTransactionFormDisabled(form, transaction.fee); + + return ( + +
+ + + handleTickerChange(ticker as string, TRANSFER_TOKEN_FIELD), + items: selectItems + })} + {...mergeProps(form.getFieldProps(TRANSFER_AMOUNT_FIELD, false, true))} + /> + + + + + + Transfer + + + +
+
+ ); +}; + +export { TransferForm }; diff --git a/src/pages/Transfer/TransferForms/components/TransferForm/index.tsx b/src/pages/Transfer/TransferForms/components/TransferForm/index.tsx new file mode 100644 index 0000000000..3fd228e19d --- /dev/null +++ b/src/pages/Transfer/TransferForms/components/TransferForm/index.tsx @@ -0,0 +1 @@ +export { TransferForm } from './TransferForm'; diff --git a/src/pages/Transfer/TransferForms/components/index.tsx b/src/pages/Transfer/TransferForms/components/index.tsx new file mode 100644 index 0000000000..3f964e425c --- /dev/null +++ b/src/pages/Transfer/TransferForms/components/index.tsx @@ -0,0 +1,4 @@ +import { CrossChainTransferForm } from './CrossChainTransferForm'; +import { TransferForm } from './TransferForm'; + +export { CrossChainTransferForm, TransferForm }; diff --git a/src/pages/Transfer/TransferForms/index.tsx b/src/pages/Transfer/TransferForms/index.tsx new file mode 100644 index 0000000000..a78f8eed42 --- /dev/null +++ b/src/pages/Transfer/TransferForms/index.tsx @@ -0,0 +1,3 @@ +import TransferOverview from './TransferForms'; + +export default TransferOverview; diff --git a/src/pages/Transfer/index.tsx b/src/pages/Transfer/index.tsx index 2dbbc7ac0b..bfd5635d2a 100644 --- a/src/pages/Transfer/index.tsx +++ b/src/pages/Transfer/index.tsx @@ -1,34 +1,3 @@ -import clsx from 'clsx'; - -import { Flex, Tabs, TabsItem } from '@/component-library'; -import Panel from '@/legacy-components/Panel'; -import MainContainer from '@/parts/MainContainer'; - -import CrossChainTransferForm from './CrossChainTransferForm'; -import { StyledWrapper } from './Transfer.style'; -import TransferForm from './TransferForm'; - -const Transfer = (): JSX.Element | null => { - return ( - - - - - - - - - - - - - - - - - - - ); -}; +import Transfer from './Transfer'; export default Transfer; diff --git a/src/pages/Vaults/Vault/RequestIssueModal/index.tsx b/src/pages/Vaults/Vault/RequestIssueModal/index.tsx index 4eec9acdd1..39a0e01099 100644 --- a/src/pages/Vaults/Vault/RequestIssueModal/index.tsx +++ b/src/pages/Vaults/Vault/RequestIssueModal/index.tsx @@ -38,7 +38,6 @@ import TokenField from '@/legacy-components/TokenField'; import InformationTooltip from '@/legacy-components/tooltips/InformationTooltip'; import InterlayButtonBase from '@/legacy-components/UI/InterlayButtonBase'; import { useSubstrateSecureState } from '@/lib/substrate'; -import SubmittedIssueRequestModal from '@/pages/Bridge/IssueForm/SubmittedIssueRequestModal'; import { ForeignAssetIdLiteral } from '@/types/currency'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import STATUSES from '@/utils/constants/statuses'; @@ -49,6 +48,8 @@ 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 SubmittedIssueRequestModal from '../SubmittedIssueRequestModal'; + const WRAPPED_TOKEN_AMOUNT = 'amount'; const BTC_ADDRESS = 'btc-address'; diff --git a/src/pages/Bridge/IssueForm/SubmittedIssueRequestModal/index.tsx b/src/pages/Vaults/Vault/SubmittedIssueRequestModal/index.tsx similarity index 97% rename from src/pages/Bridge/IssueForm/SubmittedIssueRequestModal/index.tsx rename to src/pages/Vaults/Vault/SubmittedIssueRequestModal/index.tsx index e5afdc0146..8461d4f704 100644 --- a/src/pages/Bridge/IssueForm/SubmittedIssueRequestModal/index.tsx +++ b/src/pages/Vaults/Vault/SubmittedIssueRequestModal/index.tsx @@ -36,7 +36,7 @@ const SubmittedIssueRequestModal = ({ { hasWithdrawRewardsBtn={!isReadOnlyVault} /> - {collateralToken && ( - - )} - {collateralToken && ( - - )} - {collateralToken && ( - - )} + + {collateralToken && ( + + )} + {collateralToken && ( + + )} + {collateralToken && ( + + )} + ); diff --git a/src/pages/Vaults/Vault/components/TransactionHistory/TransactionHistory.styles.tsx b/src/pages/Vaults/Vault/components/TransactionHistory/TransactionHistory.styles.tsx deleted file mode 100644 index 00345cb445..0000000000 --- a/src/pages/Vaults/Vault/components/TransactionHistory/TransactionHistory.styles.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import styled from 'styled-components'; - -import { Card, CTALink, H3, P, Stack, Strong, Table, Tabs, theme } from '@/component-library'; -import { hideScrollbar } from '@/component-library/css'; - -const StyledWrapper = styled(Card)` - display: flex; - justify-content: space-between; - align-items: center; -`; - -const StyledStack = styled(Stack)` - width: 100%; - text-align: right; -`; - -const StyledTabs = styled(Tabs)` - text-align: right; -`; - -const StyledTitle = styled(H3)` - font-size: ${theme.text.xl2}; - line-height: ${theme.lineHeight.lg}; - padding: ${theme.spacing.spacing3} 0; -`; - -const StyledStatus = styled.div` - display: flex; - padding: ${theme.spacing.spacing2} ${theme.spacing.spacing4}; - font-weight: ${theme.fontWeight.book}; - font-size: ${theme.text.xs}; - line-height: ${theme.lineHeight.base}; - color: #ffffff; -`; - -const StyledTable = styled(Table)` - text-align: left; - margin-top: ${theme.spacing.spacing4}; - - /* TODO: better solution for this. Require UX input on hover state. - Should be handled in the component, not the application. */ - tr:hover { - cursor: pointer; - } -`; - -const StyledRequestCell = styled.div` - display: flex; - flex-direction: column; -`; - -const StyledRequest = styled(Strong)` - font-size: ${theme.text.s}; - line-height: ${theme.lineHeight.s}; -`; - -const StyledDate = styled(P)` - font-size: ${theme.text.xs}; - line-height: ${theme.lineHeight.s}; - white-space: nowrap; -`; - -const StyledTableWrapper = styled.div` - overflow-x: auto; - ${hideScrollbar()} -`; - -const StyledCTALink = styled(CTALink)` - align-self: flex-end; -`; - -export { - StyledCTALink, - StyledDate, - StyledRequest, - StyledRequestCell, - StyledStack, - StyledStatus, - StyledTable, - StyledTableWrapper, - StyledTabs, - StyledTitle, - StyledWrapper -}; diff --git a/src/pages/Vaults/Vault/components/TransactionHistory/TransactionHistory.tsx b/src/pages/Vaults/Vault/components/TransactionHistory/TransactionHistory.tsx deleted file mode 100644 index d06896ca1e..0000000000 --- a/src/pages/Vaults/Vault/components/TransactionHistory/TransactionHistory.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useId } from '@react-aria/utils'; -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { CardProps, TabsItem } from '@/component-library'; -import { PAGES } from '@/utils/constants/links'; - -import { - StyledCTALink, - StyledStack, - StyledTableWrapper, - StyledTabs, - StyledTitle, - StyledWrapper -} from './TransactionHistory.styles'; -import { TransactionTable, TransactionTableData } from './TransactionTable'; - -type Props = { - transactions: Array; -}; - -type InheritAttrs = Omit; - -type TransactionHistoryProps = Props & InheritAttrs; - -const tabKeys = ['all', 'pending', 'issue', 'redeem', 'replace'] as const; - -const TransactionHistory = (props: TransactionHistoryProps): JSX.Element => { - const { t } = useTranslation(); - const titleId = useId(); - const [filteredTransactionData, setFilteredTransactiondata] = useState>( - props.transactions - ); - const [tab, setTab] = useState('all'); - - useEffect(() => { - if (tab === 'all') { - setFilteredTransactiondata(props.transactions); - } else if (tab === 'pending') { - setFilteredTransactiondata(props.transactions.filter((data) => data.status === 'pending')); - } else { - setFilteredTransactiondata(props.transactions.filter((data) => data.request.toLowerCase() === tab)); - } - }, [tab, props.transactions]); - - const table = ( - - - - ); - - return props.transactions.length === 0 ? ( - No transactions found - ) : ( - <> - Transactions history - - - setTab(e as string)}> - {tabKeys.map((key) => ( - - {null} - - ))} - - - {'View All >'} - - - - - ); -}; - -export { TransactionHistory }; -export type { TransactionHistoryProps }; diff --git a/src/pages/Vaults/Vault/components/TransactionHistory/TransactionStatusTag.tsx b/src/pages/Vaults/Vault/components/TransactionHistory/TransactionStatusTag.tsx deleted file mode 100644 index 11790a0ef2..0000000000 --- a/src/pages/Vaults/Vault/components/TransactionHistory/TransactionStatusTag.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useTranslation } from 'react-i18next'; - -import { LoadingSpinner } from '@/component-library/LoadingSpinner'; -import { Status } from '@/component-library/utils/prop-types'; - -import { StatusTag, StatusTagProps } from '../StatusTag'; - -type TransactionStatus = 'pending' | 'cancelled' | 'completed' | 'confirmed' | 'received' | 'retried'; - -const transactionStatus: Record = { - pending: 'warning', - cancelled: 'error', - completed: 'success', - confirmed: 'warning', - received: 'success', - retried: 'success' -} as const; - -type Props = { - status: TransactionStatus; -}; - -type InheritAttr = Omit; - -type TransactionStatusTagProps = Props & InheritAttr; - -const TransactionStatusTag = ({ status, ...props }: TransactionStatusTagProps): JSX.Element => { - const { t } = useTranslation(); - - const tagStatus = transactionStatus[status]; - - return ( - - {status === 'pending' && } - {t(status)} - - ); -}; - -export { TransactionStatusTag }; -export type { TransactionStatus, TransactionStatusTagProps }; diff --git a/src/pages/Vaults/Vault/components/TransactionHistory/TransactionTable.tsx b/src/pages/Vaults/Vault/components/TransactionHistory/TransactionTable.tsx deleted file mode 100644 index 07d29106af..0000000000 --- a/src/pages/Vaults/Vault/components/TransactionHistory/TransactionTable.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { HTMLAttributes, useState } from 'react'; - -import IssueRequestModal from '@/pages/Transactions/IssueRequestsTable/IssueRequestModal'; -import RedeemRequestModal from '@/pages/Transactions/RedeemRequestsTable/RedeemRequestModal'; - -import { StyledDate, StyledRequest, StyledRequestCell, StyledTable } from './TransactionHistory.styles'; -import { TransactionStatus, TransactionStatusTag } from './TransactionStatusTag'; - -const columns = [ - { name: 'Request', uid: 'request' }, - { name: 'Amount', uid: 'amount' }, - { name: 'Status', uid: 'status' } -]; - -type TransactionTableData = { - id: string; - request: string; - date: string; - amount: string; - status: TransactionStatus; - // This `any` is an upstream issue - issue and redeem request data - // hasn't been typed properly. This is a TODO, but out of scope here. - requestData: any; -}; - -type Props = { - data: TransactionTableData[]; -}; - -type NativeAttrs = Omit, keyof Props>; - -type TransactionTableProps = Props & NativeAttrs; - -const RequestCell = ({ request, date }: any) => ( - - {request} - {date} - -); - -const TransactionTable = ({ data, ...props }: TransactionTableProps): JSX.Element => { - const [selectedTableRow, setSelectedTableRow] = useState(undefined); - - const rows = data.map(({ request, amount, requestData, date, status }, key) => ({ - id: key, - amount, - requestData, - type: request, - request: , - status: - })); - - return ( - <> - setSelectedTableRow(rows.find((row) => row.id === key))} - columns={columns} - rows={rows} - {...props} - /> - {/* TODO: these modals should be refactored/replaced */} - {selectedTableRow?.type === 'Issue' && ( - setSelectedTableRow(undefined)} - request={selectedTableRow.requestData} - /> - )} - {selectedTableRow?.type === 'Redeem' && ( - setSelectedTableRow(undefined)} - request={selectedTableRow.requestData} - /> - )} - ; - - ); -}; - -export { TransactionTable }; -export type { TransactionTableData, TransactionTableProps }; diff --git a/src/pages/Vaults/Vault/components/TransactionHistory/index.tsx b/src/pages/Vaults/Vault/components/TransactionHistory/index.tsx deleted file mode 100644 index 509b17bbfb..0000000000 --- a/src/pages/Vaults/Vault/components/TransactionHistory/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export type { TransactionHistoryProps } from './TransactionHistory'; -export { TransactionHistory } from './TransactionHistory'; diff --git a/src/pages/Vaults/Vault/components/index.tsx b/src/pages/Vaults/Vault/components/index.tsx index eefb92e3b4..88e61e045d 100644 --- a/src/pages/Vaults/Vault/components/index.tsx +++ b/src/pages/Vaults/Vault/components/index.tsx @@ -1,17 +1,8 @@ import { InsightListItem, InsightsList, InsightsListProps } from './InsightsList'; import { PageTitle, PageTitleProps } from './PageTitle'; import { Rewards, RewardsProps } from './Rewards'; -import { TransactionHistory, TransactionHistoryProps } from './TransactionHistory'; import { VaultCollateral, VaultCollateralProps } from './VaultCollateral'; import { VaultInfo, VaultInfoProps } from './VaultInfo'; -export { InsightsList, PageTitle, Rewards, TransactionHistory, VaultCollateral, VaultInfo }; -export type { - InsightListItem, - InsightsListProps, - PageTitleProps, - RewardsProps, - TransactionHistoryProps, - VaultCollateralProps, - VaultInfoProps -}; +export { InsightsList, PageTitle, Rewards, VaultCollateral, VaultInfo }; +export type { InsightListItem, InsightsListProps, PageTitleProps, RewardsProps, VaultCollateralProps, VaultInfoProps }; diff --git a/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/CreateVaultWizard.styles.tsx b/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/CreateVaultWizard.styles.tsx index 8e0c6454df..1074cbfe75 100644 --- a/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/CreateVaultWizard.styles.tsx +++ b/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/CreateVaultWizard.styles.tsx @@ -62,12 +62,6 @@ const StyledDd = styled.dd` line-height: ${theme.lineHeight.base}; `; -const StyledHr = styled.hr` - border: 0; - border-bottom: ${theme.border.default}; - margin: ${theme.spacing.spacing4} 0; -`; - export { StyledDd, StyledDisclaimerCard, @@ -77,7 +71,6 @@ export { StyledDItem, StyledDl, StyledDt, - StyledHr, StyledModalHeader, StyledWarningIcon }; diff --git a/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/CreateVaultWizard.tsx b/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/CreateVaultWizard.tsx index 22cca7891c..18264808bf 100644 --- a/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/CreateVaultWizard.tsx +++ b/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/CreateVaultWizard.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { RefObject, useState } from 'react'; import { VaultsTableRow } from '../VaultsTable'; import DespositCollateralStep from './DespositCollateralStep'; @@ -9,9 +9,10 @@ type Steps = 1 | 2 | 3; interface CreateVaultWizardProps { vault?: VaultsTableRow; + overlappingModalRef: RefObject; } // TODO: Move this to a generic multi-step component -const CreateVaultWizard = ({ vault }: CreateVaultWizardProps): JSX.Element | null => { +const CreateVaultWizard = ({ vault, overlappingModalRef }: CreateVaultWizardProps): JSX.Element | null => { const [step, setStep] = useState(1); const handleNextStep = () => setStep((s) => (s + 1) as Steps); @@ -28,6 +29,7 @@ const CreateVaultWizard = ({ vault }: CreateVaultWizardProps): JSX.Element | nul onSuccessfulDeposit={handleNextStep} collateralCurrency={vault.collateralCurrency} minCollateralAmount={vault.minCollateralAmount} + overlappingModalRef={overlappingModalRef} /> diff --git a/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/DespositCollateralStep.tsx b/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/DespositCollateralStep.tsx index 62a3bc21fe..62dfddcef8 100644 --- a/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/DespositCollateralStep.tsx +++ b/src/pages/Vaults/VaultsOverview/components/CreateVaultWizard/DespositCollateralStep.tsx @@ -1,28 +1,38 @@ import { CollateralCurrencyExt, newMonetaryAmount } from '@interlay/interbtc-api'; import { MonetaryAmount } from '@interlay/monetary-js'; import { useId } from '@react-aria/utils'; +import { RefObject, useCallback } from 'react'; 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 { ModalBody, ModalDivider, ModalFooter, ModalHeader, TokenInput } from '@/component-library'; import { - CREATE_VAULT_DEPOSIT_FIELD, - CreateVaultFormData, - createVaultSchema, - isFormDisabled, - useForm + AuthCTA, + TransactionDetails, + TransactionDetailsDd, + TransactionDetailsDt, + TransactionDetailsGroup, + TransactionFeeDetails +} from '@/components'; +import { + depositCollateralVaultsSchema, + useForm, + VAULTS_DEPOSIT_COLLATERAL_AMOUNT_FIELD, + VAULTS_DEPOSIT_COLLATERAL_FEE_TOKEN_FIELD, + VaultsDepositCollateralFormData, + VaultsDepositCollateralValidationParams } from '@/lib/form'; import { StepComponentProps, withStep } from '@/utils/hocs/step'; import { Transaction, useTransaction } from '@/utils/hooks/transaction'; +import { isTransactionFormDisabled } from '@/utils/hooks/transaction/utils/form'; import { useDepositCollateral } from '../../utils/use-deposit-collateral'; -import { StyledDd, StyledDItem, StyledDl, StyledDt, StyledHr } from './CreateVaultWizard.styles'; type Props = { collateralCurrency: CollateralCurrencyExt; minCollateralAmount: MonetaryAmount; onSuccessfulDeposit?: () => void; + overlappingModalRef: RefObject; }; type DespositCollateralStepProps = Props & StepComponentProps; @@ -30,40 +40,60 @@ type DespositCollateralStepProps = Props & StepComponentProps; const DepositCollateralStep = ({ onSuccessfulDeposit, collateralCurrency, - minCollateralAmount + minCollateralAmount, + overlappingModalRef }: DespositCollateralStepProps): JSX.Element => { const titleId = useId(); const { t } = useTranslation(); - const { collateral, fee, governance } = useDepositCollateral(collateralCurrency, minCollateralAmount); + + const { collateral } = useDepositCollateral(collateralCurrency, minCollateralAmount); const transaction = useTransaction(Transaction.VAULTS_REGISTER_NEW_COLLATERAL, { onSuccess: onSuccessfulDeposit, showSuccessModal: false }); - const validationParams = { + const getTransactionArgs = useCallback( + (values: VaultsDepositCollateralFormData) => { + const amount = values[VAULTS_DEPOSIT_COLLATERAL_AMOUNT_FIELD]; + const monetaryAmount = newMonetaryAmount(amount || 0, collateralCurrency, true); + + return { monetaryAmount }; + }, + [collateralCurrency] + ); + + const handleSubmit = (data: VaultsDepositCollateralFormData) => { + const transactionData = getTransactionArgs(data); + + transaction.execute(transactionData.monetaryAmount); + }; + + const validationParams: VaultsDepositCollateralValidationParams = { minAmount: collateral.min.raw, - maxAmount: collateral.balance.raw, - governanceBalance: governance.raw, - transactionFee: fee.raw + maxAmount: collateral.balance.raw }; - const handleSubmit = (data: CreateVaultFormData) => { - if (!data.deposit) return; + const form = useForm({ + initialValues: { + [VAULTS_DEPOSIT_COLLATERAL_AMOUNT_FIELD]: '', + [VAULTS_DEPOSIT_COLLATERAL_FEE_TOKEN_FIELD]: transaction.fee.defaultCurrency.ticker + }, + validationSchema: depositCollateralVaultsSchema(validationParams), + onSubmit: handleSubmit, + onComplete: (values) => { + const transactionData = getTransactionArgs(values); - const amount = newMonetaryAmount(data.deposit || 0, collateral.currency, true); - transaction.execute(amount); - }; + const feeTicker = values[VAULTS_DEPOSIT_COLLATERAL_FEE_TOKEN_FIELD]; - const form = useForm({ - initialValues: { deposit: undefined }, - validationSchema: createVaultSchema(validationParams), - onSubmit: handleSubmit + transaction.fee.setCurrency(feeTicker).estimate(transactionData.monetaryAmount); + } }); - const inputCollateralAmount = newSafeMonetaryAmount(form.values.deposit || 0, collateral.currency, true); + const amount = form.values[VAULTS_DEPOSIT_COLLATERAL_AMOUNT_FIELD]; + const monetaryAmount = newSafeMonetaryAmount(amount || 0, collateral.currency, true); - const isBtnDisabled = isFormDisabled(form); + const isBtnDisabled = isTransactionFormDisabled(form, transaction.fee); return ( <> @@ -71,41 +101,35 @@ const DepositCollateralStep = ({
- - - - - {t('vault.minimum_required_collateral')} - - {collateral.min.amount} {collateral.currency.ticker} ({collateral.min.usd}) - - - - - {t('fees')} - - - {fee.amount} {GOVERNANCE_TOKEN.ticker} - {' '} - ({fee.usd}) - - - - + - + + + {t('vault.minimum_required_collateral')} + + {collateral.min.amount} {collateral.currency.ticker} ({collateral.min.usd}) + + + + + {t('vault.deposit_collateral')} - +
diff --git a/src/pages/Vaults/VaultsOverview/components/CreateVaults/CreateVaults.tsx b/src/pages/Vaults/VaultsOverview/components/CreateVaults/CreateVaults.tsx index 8d21f8d080..093f7c4fc5 100644 --- a/src/pages/Vaults/VaultsOverview/components/CreateVaults/CreateVaults.tsx +++ b/src/pages/Vaults/VaultsOverview/components/CreateVaults/CreateVaults.tsx @@ -1,5 +1,5 @@ import { useId } from '@react-aria/utils'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { H3, Modal, Stack } from '@/component-library'; @@ -30,6 +30,8 @@ const CreateVaults = ({ vaults = [], ...props }: CreateVaultsProps): JSX.Element open: false }); + const overlappingModalRef = useRef(null); + const handleClickAddVault = (vault: VaultsTableRow) => setCollateralModal({ open: true, data: vault }); const handleCloseModal = () => setCollateralModal((s) => ({ ...s, open: false })); @@ -47,8 +49,13 @@ const CreateVaults = ({ vaults = [], ...props }: CreateVaultsProps): JSX.Element

{t('vault.create_vault')}

- - + !overlappingModalRef.current?.contains(el)} + > +
); diff --git a/src/pages/Vaults/VaultsOverview/utils/use-deposit-collateral.tsx b/src/pages/Vaults/VaultsOverview/utils/use-deposit-collateral.tsx index e3a464749b..f4ca6df865 100644 --- a/src/pages/Vaults/VaultsOverview/utils/use-deposit-collateral.tsx +++ b/src/pages/Vaults/VaultsOverview/utils/use-deposit-collateral.tsx @@ -2,7 +2,7 @@ import { CollateralCurrencyExt, CurrencyExt, newMonetaryAmount } from '@interlay import { MonetaryAmount } from '@interlay/monetary-js'; import { displayMonetaryAmount, displayMonetaryAmountInUSDFormat } from '@/common/utils/utils'; -import { GOVERNANCE_TOKEN, GOVERNANCE_TOKEN_SYMBOL, TRANSACTION_FEE_AMOUNT } from '@/config/relay-chains'; +import { GOVERNANCE_TOKEN } from '@/config/relay-chains'; 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'; @@ -25,27 +25,18 @@ type UseDepositCollateral = { raw: MonetaryAmount; }; }; - governance: { - raw: MonetaryAmount; - }; - fee: { - amount: string; - usd: string; - raw: MonetaryAmount; - }; }; const useDepositCollateral = ( collateralCurrency: CollateralCurrencyExt, minCollateral: MonetaryAmount ): UseDepositCollateral => { - const { getAvailableBalance, getBalance } = useGetBalances(); + const { getAvailableBalance } = useGetBalances(); const prices = useGetPrices(); const collateralUSDAmount = getTokenPrice(prices, collateralCurrency.ticker)?.usd || 0; const isGovernanceCollateral = collateralCurrency === GOVERNANCE_TOKEN; - const freeGovernanceBalance = getBalance(GOVERNANCE_TOKEN.ticker)?.free || newMonetaryAmount(0, GOVERNANCE_TOKEN); const collateralTokenBalance = getAvailableBalance(collateralCurrency.ticker) || newMonetaryAmount(0, collateralCurrency); @@ -66,17 +57,6 @@ const useDepositCollateral = ( usd: displayMonetaryAmountInUSDFormat(minCollateral, collateralUSDAmount), raw: minCollateral } - }, - fee: { - amount: displayMonetaryAmount(TRANSACTION_FEE_AMOUNT), - usd: displayMonetaryAmountInUSDFormat( - TRANSACTION_FEE_AMOUNT, - getTokenPrice(prices, GOVERNANCE_TOKEN_SYMBOL)?.usd - ), - raw: TRANSACTION_FEE_AMOUNT - }, - governance: { - raw: freeGovernanceBalance } }; }; diff --git a/src/parts/Sidebar/SidebarContent/Navigation/SidebarNavLink/index.tsx b/src/parts/Sidebar/SidebarContent/Navigation/SidebarNavLink/index.tsx index 0eebbc7bf4..e1e779b549 100644 --- a/src/parts/Sidebar/SidebarContent/Navigation/SidebarNavLink/index.tsx +++ b/src/parts/Sidebar/SidebarContent/Navigation/SidebarNavLink/index.tsx @@ -7,7 +7,7 @@ import InterlayRouterNavLink, { } from '@/legacy-components/UI/InterlayRouterNavLink'; interface CustomProps { - external: boolean; + external?: boolean; href: string; } diff --git a/src/parts/Sidebar/SidebarContent/Navigation/index.tsx b/src/parts/Sidebar/SidebarContent/Navigation/index.tsx index 065c5be8e2..a71aea1543 100644 --- a/src/parts/Sidebar/SidebarContent/Navigation/index.tsx +++ b/src/parts/Sidebar/SidebarContent/Navigation/index.tsx @@ -3,15 +3,10 @@ import { ArrowPathRoundedSquareIcon, ArrowsRightLeftIcon, BanknotesIcon, - BookOpenIcon, ChartBarSquareIcon, CircleStackIcon, - ClipboardDocumentListIcon, CpuChipIcon, - DocumentTextIcon, - HandRaisedIcon, PresentationChartBarIcon, - ScaleIcon, Square3Stack3DIcon, UserIcon } from '@heroicons/react/24/outline'; @@ -23,6 +18,7 @@ import { matchPath } from 'react-router'; import { useLocation } from 'react-router-dom'; import { StoreType } from '@/common/types/util.types'; +import { Accordion, AccordionItem } from '@/component-library'; import { INTERLAY_DOCS_LINK } from '@/config/links'; import { CROWDLOAN_LINK, @@ -72,7 +68,7 @@ const Navigation = ({ const isWalletEnabled = useFeatureFlag(FeatureFlags.WALLET); const isStrategiesEnabled = useFeatureFlag(FeatureFlags.STRATEGIES); - const NAVIGATION_ITEMS = React.useMemo( + const PRIMARY_NAVIGATION_ITEMS = React.useMemo( () => [ { name: 'nav_wallet', @@ -115,12 +111,6 @@ const Navigation = ({ icon: Square3Stack3DIcon, disabled: !isAMMEnabled }, - { - name: 'nav_transactions', - link: PAGES.TRANSACTIONS, - icon: ClipboardDocumentListIcon, - hidden: false - }, { name: 'nav_staking', link: PAGES.STAKING, @@ -143,11 +133,16 @@ const Navigation = ({ link: '#', icon: () => null, separator: true - }, + } + ], + [isWalletEnabled, isStrategiesEnabled, isLendingEnabled, isAMMEnabled, selectedAccount?.address, vaultClientLoaded] + ); + + const SECONDARY_NAVIGATION_ITEMS = React.useMemo( + () => [ { name: 'nav_use_wrapped', link: USE_WRAPPED_CURRENCY_LINK, - icon: HandRaisedIcon, hidden: !USE_WRAPPED_CURRENCY_LINK, external: true, rest: { @@ -158,7 +153,6 @@ const Navigation = ({ { name: 'nav_crowdloan', link: CROWDLOAN_LINK, - icon: BanknotesIcon, external: true, // This will suppress the link on testnet hidden: process.env.REACT_APP_BITCOIN_NETWORK !== 'mainnet' || !CROWDLOAN_LINK, @@ -170,7 +164,6 @@ const Navigation = ({ { name: 'nav_docs', link: INTERLAY_DOCS_LINK, - icon: BookOpenIcon, external: true, rest: { target: '_blank', @@ -180,7 +173,6 @@ const Navigation = ({ { name: 'nav_governance', link: GOVERNANCE_LINK, - icon: ScaleIcon, external: true, hidden: !GOVERNANCE_LINK, rest: { @@ -191,7 +183,6 @@ const Navigation = ({ { name: 'nav_terms_and_conditions', link: TERMS_AND_CONDITIONS_LINK, - icon: DocumentTextIcon, external: true, hidden: !TERMS_AND_CONDITIONS_LINK, rest: { @@ -200,12 +191,12 @@ const Navigation = ({ } } ], - [isWalletEnabled, isStrategiesEnabled, isLendingEnabled, isAMMEnabled, selectedAccount?.address, vaultClientLoaded] + [] ); return ( ); }; diff --git a/src/parts/Sidebar/SidebarContent/SocialMediaContainer/index.tsx b/src/parts/Sidebar/SidebarContent/SocialMediaContainer/index.tsx index 877ccb3438..899ba353e0 100644 --- a/src/parts/Sidebar/SidebarContent/SocialMediaContainer/index.tsx +++ b/src/parts/Sidebar/SidebarContent/SocialMediaContainer/index.tsx @@ -13,19 +13,19 @@ import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; const SOCIAL_MEDIA_ITEMS = [ { link: INTERLAY_TWITTER_LINK, - icon: + icon: }, { link: INTERLAY_DISCORD_LINK, - icon: + icon: }, { link: INTERLAY_GITHUB_LINK, - icon: + icon: }, { link: INTERLAY_EMAIL_LINK, - icon: + icon: } ]; @@ -35,11 +35,11 @@ const SocialMediaContainer = ({ className, ...rest }: React.ComponentPropsWithRe ( 'flex-1', 'flex', 'flex-col', - { 'bg-white': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT }, - { 'dark:bg-kintsugiMidnight': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA } + { 'bg-interlayHaiti-50': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT }, + { 'dark:bg-kintsugiMidnight-900': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA } )} > {onSmallScreen && } diff --git a/src/parts/Topbar/GetGovernanceTokenUI/index.tsx b/src/parts/Topbar/GetGovernanceTokenUI/index.tsx index 36f23fc7b7..f6637bdd01 100644 --- a/src/parts/Topbar/GetGovernanceTokenUI/index.tsx +++ b/src/parts/Topbar/GetGovernanceTokenUI/index.tsx @@ -13,10 +13,9 @@ import { ReactComponent as StellaSwapLogoIcon } from '@/assets/img/exchanges/ste import { ReactComponent as ZenlinkLogoIcon } from '@/assets/img/exchanges/zenlink-logo.svg'; import { showBuyModal } from '@/common/actions/general.actions'; import { StoreType } from '@/common/types/util.types'; +import { CTA } from '@/component-library'; import { GOVERNANCE_TOKEN_SYMBOL } from '@/config/relay-chains'; -import InterlayDefaultOutlinedButton, { - Props as InterlayDefaultOutlinedButtonProps -} from '@/legacy-components/buttons/InterlayDefaultOutlinedButton'; +import { Props as InterlayDefaultOutlinedButtonProps } from '@/legacy-components/buttons/InterlayDefaultContainedButton'; import TitleWithUnderline from '@/legacy-components/TitleWithUnderline'; import InterlayLink from '@/legacy-components/UI/InterlayLink'; import InterlayModal, { InterlayModalInnerWrapper } from '@/legacy-components/UI/InterlayModal'; @@ -120,9 +119,9 @@ const GetGovernanceTokenUI = (props: InterlayDefaultOutlinedButtonProps): JSX.El return ( <> - + {getGovernanceTokenLabel} - + diff --git a/src/parts/Topbar/index.tsx b/src/parts/Topbar/index.tsx index 63098f3442..384b9b50d3 100644 --- a/src/parts/Topbar/index.tsx +++ b/src/parts/Topbar/index.tsx @@ -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 { POLKADOT } from '@/utils/constants/relay-chain-names'; 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'; @@ -137,7 +138,13 @@ const Topbar = (): JSX.Element => { )} - +
+ +
{accountLabel} diff --git a/src/parts/Wrapper/Wrapper.style.tsx b/src/parts/Wrapper/Wrapper.style.tsx new file mode 100644 index 0000000000..a53ca66a46 --- /dev/null +++ b/src/parts/Wrapper/Wrapper.style.tsx @@ -0,0 +1,12 @@ +import styled from 'styled-components'; + +import dysonsphere from '@/assets/img/dysonsphere.svg'; + +const StyledWrapper = styled('div')` + background: url(${dysonsphere}) no-repeat; + background-size: 40%; + background-position: right bottom; + background-attachment: fixed; +`; + +export { StyledWrapper }; diff --git a/src/parts/Wrapper/index.tsx b/src/parts/Wrapper/index.tsx new file mode 100644 index 0000000000..8ff410f2d3 --- /dev/null +++ b/src/parts/Wrapper/index.tsx @@ -0,0 +1,12 @@ +import { StyledWrapper } from './Wrapper.style'; + +interface Props { + className?: string; + children: React.ReactNode; +} + +const Wrapper = ({ className, children }: Props): JSX.Element => { + return {children}; +}; + +export default Wrapper; diff --git a/src/types/bridge.ts b/src/types/bridge.ts new file mode 100644 index 0000000000..30d919ba5c --- /dev/null +++ b/src/types/bridge.ts @@ -0,0 +1,7 @@ +enum BridgeActions { + ISSUE = 'issue', + REDEEM = 'redeem', + BURN = 'burn' +} + +export { BridgeActions }; diff --git a/src/utils/constants/general.ts b/src/utils/constants/general.ts index b9367a2534..91ea4b33d4 100644 --- a/src/utils/constants/general.ts +++ b/src/utils/constants/general.ts @@ -1,3 +1,3 @@ -const TABLE_PAGE_LIMIT = 20; +const TABLE_PAGE_LIMIT = 5; export { TABLE_PAGE_LIMIT }; diff --git a/src/utils/constants/links.ts b/src/utils/constants/links.ts index 7a62ca51f9..fdff968488 100644 --- a/src/utils/constants/links.ts +++ b/src/utils/constants/links.ts @@ -15,7 +15,6 @@ const PAGES = Object.freeze({ BRIDGE: '/bridge', STRATEGIES: '/strategies', TRANSFER: '/transfer', - TRANSACTIONS: '/transactions', TX: '/tx', STAKING: '/staking', DASHBOARD: '/dashboard', diff --git a/src/utils/helpers/is-valid-polkadot-address.ts b/src/utils/helpers/is-valid-polkadot-address.ts deleted file mode 100644 index 5ba84a3ed7..0000000000 --- a/src/utils/helpers/is-valid-polkadot-address.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { decodeAddress, encodeAddress } from '@polkadot/keyring'; -import { hexToU8a, isHex } from '@polkadot/util'; - -const isValidPolkadotAddress = (address: string): boolean => { - try { - encodeAddress(isHex(address) ? hexToU8a(address) : decodeAddress(address)); - - return true; - } catch { - return false; - } -}; - -export default isValidPolkadotAddress; diff --git a/src/utils/hooks/api/bridge/use-get-issue-data.tsx b/src/utils/hooks/api/bridge/use-get-issue-data.tsx new file mode 100644 index 0000000000..483c446f7b --- /dev/null +++ b/src/utils/hooks/api/bridge/use-get-issue-data.tsx @@ -0,0 +1,77 @@ +import { newMonetaryAmount } from '@interlay/interbtc-api'; +import { BitcoinAmount, Currency, MonetaryAmount } from '@interlay/monetary-js'; +import Big from 'big.js'; +import { useCallback } from 'react'; +import { useErrorHandler } from 'react-error-boundary'; +import { useQuery } from 'react-query'; + +import { BLOCKTIME_REFETCH_INTERVAL } from '@/utils/constants/api'; + +import { useGetCurrencies } from '../use-get-currencies'; + +type IssueData = { + dustValue: MonetaryAmount; + issueFee: MonetaryAmount; + griefingCollateralRate: Big; +}; + +const getIssueData = async (): Promise => { + const [issueFee, griefingCollateralRate, dustValue] = await Promise.all([ + window.bridge.fee.getIssueFee(), + window.bridge.fee.getIssueGriefingCollateralRate(), + window.bridge.issue.getDustValue() + ]); + + return { + dustValue, + issueFee: new BitcoinAmount(issueFee), + griefingCollateralRate + }; +}; + +type UseGetIssueDataResult = { + data: IssueData | undefined; + getSecurityDeposit: ( + btcAmount: MonetaryAmount, + ticker: string | undefined + ) => Promise | undefined>; + refetch: () => void; +}; + +const useGetIssueData = (): UseGetIssueDataResult => { + const { getCurrencyFromTicker, isLoading: isLoadingCurrencies } = useGetCurrencies(true); + + const { data, error, refetch } = useQuery({ + queryKey: 'issue-data', + queryFn: getIssueData, + refetchInterval: BLOCKTIME_REFETCH_INTERVAL + }); + + useErrorHandler(error); + + const getSecurityDeposit = useCallback( + async (btcAmount: MonetaryAmount, ticker: string | undefined) => { + if (isLoadingCurrencies || ticker === undefined) { + return; + } + const griefingCollateralCurrency = getCurrencyFromTicker(ticker); + const btcToGriefingCollateralCurrency = await window.bridge.oracle.getExchangeRate(griefingCollateralCurrency); + const { griefingCollateralRate } = data || {}; + + if (!btcToGriefingCollateralCurrency || !griefingCollateralRate) + return newMonetaryAmount(0, griefingCollateralCurrency); + + return btcToGriefingCollateralCurrency.toCounter(btcAmount).mul(griefingCollateralRate); + }, + [data, getCurrencyFromTicker, isLoadingCurrencies] + ); + + return { + data, + refetch, + getSecurityDeposit + }; +}; + +export { useGetIssueData }; +export type { IssueData, UseGetIssueDataResult }; diff --git a/src/utils/hooks/api/bridge/use-get-issue-request-limits.tsx b/src/utils/hooks/api/bridge/use-get-issue-request-limits.tsx new file mode 100644 index 0000000000..355a8958e7 --- /dev/null +++ b/src/utils/hooks/api/bridge/use-get-issue-request-limits.tsx @@ -0,0 +1,30 @@ +import { IssueLimits } from '@interlay/interbtc-api/build/src/parachain/issue'; +import { useErrorHandler } from 'react-error-boundary'; +import { useQuery, UseQueryOptions } from 'react-query'; + +import { BLOCKTIME_REFETCH_INTERVAL } from '@/utils/constants/api'; + +const getIssueRequestLimits = async (): Promise => window.bridge.issue.getRequestLimits(); + +type UseGetIssueRequestLimitResult = { + data: IssueLimits | undefined; + refetch: () => void; +}; + +type UseGetIssueRequestLimitProps = UseQueryOptions; + +const useGetIssueRequestLimit = (props?: UseGetIssueRequestLimitProps): UseGetIssueRequestLimitResult => { + const queryResult = useQuery({ + queryKey: 'issue-request-limit', + queryFn: getIssueRequestLimits, + refetchInterval: BLOCKTIME_REFETCH_INTERVAL, + ...props + }); + + useErrorHandler(queryResult.error); + + return queryResult; +}; + +export { useGetIssueRequestLimit }; +export type { UseGetIssueRequestLimitResult }; diff --git a/src/utils/hooks/api/bridge/use-get-max-burnable-tokens.tsx b/src/utils/hooks/api/bridge/use-get-max-burnable-tokens.tsx new file mode 100644 index 0000000000..22c3848929 --- /dev/null +++ b/src/utils/hooks/api/bridge/use-get-max-burnable-tokens.tsx @@ -0,0 +1,59 @@ +import { CollateralCurrencyExt, isCurrencyEqual } from '@interlay/interbtc-api'; +import { Currency, MonetaryAmount } from '@interlay/monetary-js'; +import { useCallback } from 'react'; +import { useErrorHandler } from 'react-error-boundary'; +import { useQuery } from 'react-query'; + +import { BLOCKTIME_REFETCH_INTERVAL } from '@/utils/constants/api'; + +import useAccountId from '../../use-account-id'; +import { useGetCollateralCurrencies } from '../use-get-collateral-currencies'; + +type MaxBurnableTokensData = { + amounts: MonetaryAmount[]; + hasBurnableTokens: boolean; +}; + +const getMaxBurnableTokensData = async (currencies: CollateralCurrencyExt[]): Promise => { + const amounts = await Promise.all(currencies.map((currency) => window.bridge.redeem.getMaxBurnableTokens(currency))); + + return { + amounts, + hasBurnableTokens: !!amounts?.find((amount) => !amount.isZero()) + }; +}; + +type UseGetMaxBurnableTokensResult = { + data: MaxBurnableTokensData | undefined; + getMaxBurnableTokens: (currency: Currency) => MonetaryAmount | undefined; + refetch: () => void; +}; + +const useGetMaxBurnableTokens = (): UseGetMaxBurnableTokensResult => { + const { data: collateralCurrencies } = useGetCollateralCurrencies(true); + + const accountId = useAccountId(); + + const { data, error, refetch } = useQuery({ + queryKey: ['max-burnable-tokens', accountId?.toString()], + queryFn: () => collateralCurrencies && getMaxBurnableTokensData(collateralCurrencies), + refetchInterval: BLOCKTIME_REFETCH_INTERVAL, + enabled: !!accountId && !!collateralCurrencies + }); + + useErrorHandler(error); + + const getMaxBurnableTokens = useCallback( + (currency: Currency) => data?.amounts.find((amount) => isCurrencyEqual(amount.currency, currency)), + [data?.amounts] + ); + + return { + data, + getMaxBurnableTokens, + refetch + }; +}; + +export { useGetMaxBurnableTokens }; +export type { MaxBurnableTokensData, UseGetMaxBurnableTokensResult }; diff --git a/src/utils/hooks/api/bridge/use-get-redeem-data.tsx b/src/utils/hooks/api/bridge/use-get-redeem-data.tsx new file mode 100644 index 0000000000..1cef3fcb3f --- /dev/null +++ b/src/utils/hooks/api/bridge/use-get-redeem-data.tsx @@ -0,0 +1,98 @@ +import { InterbtcPrimitivesVaultId } from '@interlay/interbtc-api'; +import { Currency, MonetaryAmount } from '@interlay/monetary-js'; +import Big from 'big.js'; +import { useCallback } from 'react'; +import { useErrorHandler } from 'react-error-boundary'; +import { useQuery } from 'react-query'; + +import { RELAY_CHAIN_NATIVE_TOKEN } from '@/config/relay-chains'; +import { BLOCKTIME_REFETCH_INTERVAL } from '@/utils/constants/api'; + +import { useGetExchangeRate } from '../use-get-exchange-rate'; + +const getPremiumRedeemVaults = async (): Promise>> => + window.bridge.vaults.getPremiumRedeemVaults().catch(() => new Map()); + +type RedeemData = { + dustValue: MonetaryAmount; + feeRate: Big; + redeemLimit: MonetaryAmount; + premium?: { + feeRate: Big; + redeemLimit: MonetaryAmount; + }; + currentInclusionFee: MonetaryAmount; +}; + +const getRedeemData = async (): Promise => { + const [ + premiumRedeemFeeRate, + dustValue, + premiumRedeemVaults, + feeRate, + currentInclusionFee, + vaultsWithRedeemableTokens + ] = await Promise.all([ + window.bridge.redeem.getPremiumRedeemFeeRate(), + window.bridge.redeem.getDustValue(), + getPremiumRedeemVaults(), + window.bridge.redeem.getFeeRate(), + window.bridge.redeem.getCurrentInclusionFee(), + window.bridge.vaults.getVaultsWithRedeemableTokens() + ]); + + const redeemLimit = vaultsWithRedeemableTokens.values().next().value; + + const premiumRedeemLimit = premiumRedeemVaults.values().next().value; + + const premium = premiumRedeemLimit + ? { + feeRate: premiumRedeemFeeRate, + redeemLimit: premiumRedeemLimit + } + : undefined; + + return { + dustValue, + feeRate, + redeemLimit, + premium, + currentInclusionFee + }; +}; + +type UseGetRedeemDataResult = { + data: RedeemData | undefined; + getCompensationAmount: (btcAmount: MonetaryAmount) => MonetaryAmount | undefined; + refetch: () => void; +}; + +const useGetRedeemData = (): UseGetRedeemDataResult => { + const { data: btcToRelayChainToken } = useGetExchangeRate(RELAY_CHAIN_NATIVE_TOKEN); + + const { data, error, refetch } = useQuery({ + queryKey: 'redeem-data', + queryFn: getRedeemData, + refetchInterval: BLOCKTIME_REFETCH_INTERVAL + }); + + useErrorHandler(error); + + const getCompensationAmount = useCallback( + (btcAmount: MonetaryAmount) => { + if (!btcToRelayChainToken || !data?.premium) return; + + return btcToRelayChainToken.toCounter(btcAmount).mul(data.premium.feeRate); + }, + [btcToRelayChainToken, data] + ); + + return { + data, + refetch, + getCompensationAmount + }; +}; + +export { useGetRedeemData }; +export type { RedeemData, UseGetRedeemDataResult }; diff --git a/src/utils/hooks/api/bridge/use-get-vaults.tsx b/src/utils/hooks/api/bridge/use-get-vaults.tsx new file mode 100644 index 0000000000..3172414927 --- /dev/null +++ b/src/utils/hooks/api/bridge/use-get-vaults.tsx @@ -0,0 +1,132 @@ +import { CurrencyExt, InterbtcPrimitivesVaultId } from '@interlay/interbtc-api'; +import { Currency, MonetaryAmount } from '@interlay/monetary-js'; +import { useCallback } from 'react'; +import { useErrorHandler } from 'react-error-boundary'; +import { useQuery, UseQueryOptions } from 'react-query'; + +import { BridgeActions } from '@/types/bridge'; +import { BLOCKTIME_REFETCH_INTERVAL } from '@/utils/constants/api'; + +import { useGetCurrencies } from '../use-get-currencies'; + +const getPremiumRedeemVaults = async (): Promise>> => + window.bridge.vaults.getPremiumRedeemVaults().catch(() => new Map()); + +type BridgeVaultData = { + id: string; + vaultId: InterbtcPrimitivesVaultId; + amount: MonetaryAmount; + collateralCurrency: CurrencyExt; +}; + +type GetBridgeVaultData = { + list: BridgeVaultData[]; + map: Map>; + premium: T extends BridgeActions.REDEEM + ? { + list: BridgeVaultData[]; + map: Map>; + } + : never; +}; + +type UseGetBridgeVaultResult = { + data: GetBridgeVaultData | undefined; + getAvailableVaults: T extends BridgeActions.REDEEM + ? (requiredCapacity: MonetaryAmount, onlyPremiumVaults?: boolean) => BridgeVaultData[] | undefined + : (requiredCapacity: MonetaryAmount) => BridgeVaultData[] | undefined; + + refetch: () => void; +}; + +type UseGetVaultsOptions = UseQueryOptions< + GetBridgeVaultData, + unknown, + GetBridgeVaultData, + string[] +>; + +const useGetVaults = ( + action: T, + options?: UseGetVaultsOptions +): UseGetBridgeVaultResult => { + const { getCurrencyFromIdPrimitive, isLoading: isLoadingCurrencies } = useGetCurrencies(true); + + const composeVaultData = useCallback( + (map: Map>): BridgeVaultData[] => + [...map].map(([vaultId, amount], idx) => ({ + id: idx.toString(), + vaultId, + amount, + collateralCurrency: getCurrencyFromIdPrimitive(vaultId.currencies.collateral) + })), + [getCurrencyFromIdPrimitive] + ); + + const getVaults = useCallback(async (): Promise> => { + const isRedeem = action === BridgeActions.REDEEM; + const map = await (isRedeem + ? window.bridge.vaults.getVaultsWithRedeemableTokens() + : window.bridge.vaults.getVaultsWithIssuableTokens()); + + const list = composeVaultData(map); + + switch (action) { + case BridgeActions.REDEEM: { + const premiumVaultsMap = await getPremiumRedeemVaults(); + const premiumVaultsList = composeVaultData(premiumVaultsMap); + + const data: GetBridgeVaultData = { + list, + map, + premium: { + map: premiumVaultsMap, + list: premiumVaultsList + } + }; + + return data as GetBridgeVaultData; + } + default: + case BridgeActions.ISSUE: + return { + list, + map + } as GetBridgeVaultData; + } + }, [action, composeVaultData]); + + const { data, error, refetch } = useQuery({ + queryKey: ['vaults', action], + queryFn: getVaults, + refetchInterval: BLOCKTIME_REFETCH_INTERVAL, + enabled: !isLoadingCurrencies, + ...options + }); + + const getAvailableVaults = useCallback( + (requiredCapacity: MonetaryAmount, onlyPremiumVaults?: boolean | never) => { + const list = onlyPremiumVaults ? data?.premium.list : data?.list; + + return list + ?.filter((vault) => vault.amount.gte(requiredCapacity)) + .sort((vaultA, vaultB) => { + const vaultAId = vaultA.vaultId.accountId.toString(); + const vaultBId = vaultB.vaultId.accountId.toString(); + return vaultAId < vaultBId ? -1 : vaultAId > vaultBId ? 1 : 0; + }); + }, + [data] + ); + + useErrorHandler(error); + + return { + data, + refetch, + getAvailableVaults + }; +}; + +export { useGetVaults }; +export type { BridgeVaultData, UseGetBridgeVaultResult }; diff --git a/src/utils/hooks/api/oracle/use-get-oracle-currencies.ts b/src/utils/hooks/api/oracle/use-get-oracle-currencies.ts new file mode 100644 index 0000000000..ea7b2867f1 --- /dev/null +++ b/src/utils/hooks/api/oracle/use-get-oracle-currencies.ts @@ -0,0 +1,37 @@ +import { CurrencyExt, InterbtcPrimitivesCurrencyId } from '@interlay/interbtc-api'; +import { useQuery } from 'react-query'; + +import { useGetCurrencies } from '../use-get-currencies'; + +const getOracleCurrencies = ( + getCurrencyFromIdPrimitive: (currencyPrimitive: InterbtcPrimitivesCurrencyId) => CurrencyExt +) => async (): Promise> => { + const keys = await window.bridge.api.query.oracle.aggregate.keys(); + + // MEMO: Primitive decoding, because`storageKeyToNthInner` is not decoding proper OracleKey type + const currencies = keys + .map((key) => key.toHuman() as any) + .filter((value) => value[0] !== 'FeeEstimation') + .map(([{ ExchangeRate }]) => + getCurrencyFromIdPrimitive(window.bridge.api.createType('InterbtcPrimitivesCurrencyId', ExchangeRate)) + ); + + return currencies; +}; + +interface UseGetOracleCurrenciesResult { + data: Array | undefined; +} + +const useGetOracleCurrencies = (): UseGetOracleCurrenciesResult => { + const { getCurrencyFromIdPrimitive, isLoading: isLoadingCurrencies } = useGetCurrencies(true); + + const { data } = useQuery({ + queryKey: 'getOracleCurrencies', + queryFn: getOracleCurrencies(getCurrencyFromIdPrimitive), + enabled: !isLoadingCurrencies + }); + + return { data }; +}; +export { useGetOracleCurrencies }; diff --git a/src/utils/hooks/api/tokens/use-get-balances.tsx b/src/utils/hooks/api/tokens/use-get-balances.tsx index 27ba5f300c..d975710a09 100644 --- a/src/utils/hooks/api/tokens/use-get-balances.tsx +++ b/src/utils/hooks/api/tokens/use-get-balances.tsx @@ -1,4 +1,4 @@ -import { ChainBalance, CurrencyExt, newMonetaryAmount } from '@interlay/interbtc-api'; +import { ChainBalance, CurrencyExt } from '@interlay/interbtc-api'; import { AccountId } from '@polkadot/types/interfaces'; import { useCallback } from 'react'; import { useErrorHandler } from 'react-error-boundary'; @@ -6,7 +6,6 @@ import { useQuery, UseQueryResult } from 'react-query'; import { useSelector } from 'react-redux'; import { StoreType } from '@/common/types/util.types'; -import { GOVERNANCE_TOKEN, TRANSACTION_FEE_AMOUNT } from '@/config/relay-chains'; import { useSubstrateSecureState } from '@/lib/substrate'; import { REFETCH_INTERVAL } from '@/utils/constants/api'; import { useGetCurrencies } from '@/utils/hooks/api/use-get-currencies'; @@ -55,25 +54,7 @@ const useGetBalances = (): UseGetBalances => { const getBalance = useCallback((ticker: string) => data?.[ticker], [data]); - // return available balance as well known as free field (ChainBalance). - // if the ticker is governance, the necessary for fees will be deducted - // from the return value - const getAvailableBalance = useCallback( - (ticker: string) => { - const { transferable } = getBalance(ticker) || {}; - - if (ticker === GOVERNANCE_TOKEN.ticker) { - if (!transferable) return undefined; - - const governanceBalance = transferable.sub(TRANSACTION_FEE_AMOUNT); - - return governanceBalance.toBig().gte(0) ? governanceBalance : newMonetaryAmount(0, governanceBalance.currency); - } - - return transferable; - }, - [getBalance] - ); + const getAvailableBalance = useCallback((ticker: string) => getBalance(ticker)?.transferable, [getBalance]); return { ...queryResult, getBalance, getAvailableBalance }; }; diff --git a/src/utils/hooks/api/use-get-collateral-currencies.tsx b/src/utils/hooks/api/use-get-collateral-currencies.tsx index 2e88864af9..2fde1a7d1f 100644 --- a/src/utils/hooks/api/use-get-collateral-currencies.tsx +++ b/src/utils/hooks/api/use-get-collateral-currencies.tsx @@ -3,6 +3,7 @@ import { useQuery, UseQueryResult } from 'react-query'; const getCurrencies = async (): Promise> => getCollateralCurrencies(window.bridge.api); +// TODO: adapt to lastest approach const useGetCollateralCurrencies = (bridgeLoaded: boolean): UseQueryResult> => { return useQuery({ queryKey: 'getCollateralCurrencies', queryFn: getCurrencies, enabled: bridgeLoaded }); }; diff --git a/src/utils/hooks/api/use-get-exchange-rate.tsx b/src/utils/hooks/api/use-get-exchange-rate.tsx new file mode 100644 index 0000000000..81e176c3e1 --- /dev/null +++ b/src/utils/hooks/api/use-get-exchange-rate.tsx @@ -0,0 +1,26 @@ +import { CurrencyExt } from '@interlay/interbtc-api'; +import { Currency, ExchangeRate } from '@interlay/monetary-js'; +import { useErrorHandler } from 'react-error-boundary'; +import { useQuery, UseQueryResult } from 'react-query'; + +type UseGetExchangeRateResult = UseQueryResult, unknown>; + +const getExchangeRateData = ( + collateralCurrency: CurrencyExt, + wrappedCurrency?: Currency +): Promise> => + window.bridge.oracle.getExchangeRate(collateralCurrency, wrappedCurrency); + +const useGetExchangeRate = (collateralCurrency: CurrencyExt, wrappedCurrency?: Currency): UseGetExchangeRateResult => { + const queryResult = useQuery({ + queryKey: ['exchange-rates', collateralCurrency, wrappedCurrency], + queryFn: () => getExchangeRateData(collateralCurrency, wrappedCurrency) + }); + + useErrorHandler(queryResult.error); + + return queryResult; +}; + +export { useGetExchangeRate }; +export type { UseGetExchangeRateResult }; diff --git a/src/utils/hooks/api/vaults/use-get-vault-transactions.tsx b/src/utils/hooks/api/vaults/use-get-vault-transactions.tsx index f789377278..37143436bf 100644 --- a/src/utils/hooks/api/vaults/use-get-vault-transactions.tsx +++ b/src/utils/hooks/api/vaults/use-get-vault-transactions.tsx @@ -5,7 +5,6 @@ import { useQuery } from 'react-query'; import { formatDateTimePrecise } from '@/common/utils/utils'; import { ACCOUNT_ID_TYPE_NAME } from '@/config/general'; -import { TransactionTableData } from '@/pages/Vaults/Vault/components/TransactionHistory/TransactionTable'; import genericFetcher, { GENERIC_FETCHER } from '@/services/fetchers/generic-fetcher'; import issuesFetcher, { getIssueWithStatus, ISSUES_FETCHER } from '@/services/fetchers/issues-fetcher'; import redeemsFetcher, { getRedeemWithStatus, REDEEMS_FETCHER } from '@/services/fetchers/redeems-fetcher'; @@ -16,6 +15,19 @@ import { getCurrencyEqualityCondition } from '@/utils/helpers/currencies'; import { useGetCurrencies } from '../use-get-currencies'; +type TransactionStatus = 'pending' | 'cancelled' | 'completed' | 'confirmed' | 'received' | 'retried'; + +type TransactionTableData = { + id: string; + request: string; + date: string; + amount: string; + status: TransactionStatus; + // This `any` is an upstream issue - issue and redeem request data + // hasn't been typed properly. This is a TODO, but out of scope here. + requestData: any; +}; + // TODO: Bad stuff happening here! `getIssueWithStatus` and `getRedeemWithStatus` are // mutating the data which is why `status` is being set like this. We need to refactor // the modal and fetchers to handle all use cases better. diff --git a/src/utils/hooks/transaction/extrinsics/lib.ts b/src/utils/hooks/transaction/extrinsics/lib.ts index 0d2b90a727..aed8b5b968 100644 --- a/src/utils/hooks/transaction/extrinsics/lib.ts +++ b/src/utils/hooks/transaction/extrinsics/lib.ts @@ -50,12 +50,16 @@ const getLibExtrinsic = async (params: LibActions): Promise => { 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_REPAY_ALL: { + const [underlyingCurrency] = params.args; + return window.bridge.loans.repayAll(underlyingCurrency); + } 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_WITHDRAW_ALL: { + const [underlyingCurrency] = params.args; + return window.bridge.loans.withdrawAll(underlyingCurrency); + } case Transaction.LOANS_DISABLE_COLLATERAL: return window.bridge.loans.disableAsCollateral(...params.args); case Transaction.LOANS_ENABLE_COLLATERAL: diff --git a/src/utils/hooks/transaction/types/hook.ts b/src/utils/hooks/transaction/types/hook.ts new file mode 100644 index 0000000000..3ee83e942a --- /dev/null +++ b/src/utils/hooks/transaction/types/hook.ts @@ -0,0 +1,109 @@ +import { CurrencyExt } from '@interlay/interbtc-api'; +import { MonetaryAmount } from '@interlay/monetary-js'; +import { ExtrinsicStatus } from '@polkadot/types/interfaces'; +import { ISubmittableResult } from '@polkadot/types/types'; +import { UseMutationOptions, UseMutationResult } from 'react-query'; + +import { Transaction, TransactionActions, TransactionArgs } from '.'; + +type FeeEstimateResult = { + amount?: MonetaryAmount; + isValid?: boolean; +}; + +type TransactionResult = { status: 'success' | 'error'; data: ISubmittableResult; error?: Error }; + +type ExecuteArgs = { + // Executes the transaction + execute(...args: TransactionArgs): void; + // Similar to execute but returns a promise which can be awaited. + executeAsync(...args: TransactionArgs): Promise; +}; + +type ExecuteTypeArgs = { + execute(type: D, ...args: TransactionArgs): void; + executeAsync(type: D, ...args: TransactionArgs): Promise; +}; + +type ExecuteFunctions = ExecuteArgs | ExecuteTypeArgs; + +type EstimateArgs = { + estimate(...args: TransactionArgs): void; + setCurrency(ticker?: string): { estimate(...args: TransactionArgs): void }; +}; + +type EstimateTypeArgs = { + estimate(type: D, ...args: TransactionArgs): void; + setCurrency(ticker?: string): { estimate(type: D, ...args: TransactionArgs): void }; +}; + +type EstimateFunctions = EstimateArgs | EstimateTypeArgs; + +type EstimateFeeParams = { ticker: string; params: TransactionActions }; + +type ReactQueryUseFeeEstimateResult = Omit< + UseMutationResult, + 'mutate' | 'mutateAsync' +>; + +type UseFeeEstimateResult = { + defaultCurrency: CurrencyExt; + detailsProps: { + defaultCurrency: CurrencyExt; + amount?: MonetaryAmount; + showInsufficientBalance?: boolean; + }; +} & ReactQueryUseFeeEstimateResult & + EstimateFunctions; + +type ReactQueryUseTransactionResult = Omit< + UseMutationResult, + 'mutate' | 'mutateAsync' +>; + +type UseTransactionResult = { + reject: (error?: Error) => void; + isSigned: boolean; + fee: UseFeeEstimateResult; +} & ReactQueryUseTransactionResult & + ExecuteFunctions; + +type UseTransactionOptions = Omit< + UseMutationOptions, + 'mutationFn' +> & { + customStatus?: ExtrinsicStatus['type']; + onSigning?: (variables: TransactionActions) => void; + showSuccessModal?: boolean; +}; + +type UseTransactionWithType = Omit< + Exclude, ExecuteTypeArgs>, + 'fee' +> & { + fee: Exclude, EstimateTypeArgs>; +}; + +type UseTransactionWithoutType = Omit< + Exclude, ExecuteArgs>, + 'fee' +> & { + fee: Exclude, EstimateArgs>; +}; + +export type { + EstimateArgs, + EstimateFeeParams, + EstimateFunctions, + EstimateTypeArgs, + ExecuteArgs, + ExecuteFunctions, + ExecuteTypeArgs, + FeeEstimateResult, + TransactionResult, + UseFeeEstimateResult, + UseTransactionOptions, + UseTransactionResult, + UseTransactionWithoutType, + UseTransactionWithType +}; diff --git a/src/utils/hooks/transaction/types/index.ts b/src/utils/hooks/transaction/types/index.ts index 81d43097a0..bad9729c68 100644 --- a/src/utils/hooks/transaction/types/index.ts +++ b/src/utils/hooks/transaction/types/index.ts @@ -66,7 +66,7 @@ type TransactionEvents = { interface TransactionAction { accountAddress: string; - events: TransactionEvents; + events?: TransactionEvents; timestamp: number; customStatus?: ExtrinsicStatus['type']; } diff --git a/src/utils/hooks/transaction/types/loans.ts b/src/utils/hooks/transaction/types/loans.ts index 27797c68d9..e6d33723e0 100644 --- a/src/utils/hooks/transaction/types/loans.ts +++ b/src/utils/hooks/transaction/types/loans.ts @@ -1,4 +1,5 @@ -import { InterBtcApi } from '@interlay/interbtc-api'; +import { CurrencyExt, InterBtcApi } from '@interlay/interbtc-api'; +import { MonetaryAmount } from '@interlay/monetary-js'; import { Transaction } from '../types'; import { TransactionAction } from '.'; @@ -43,9 +44,11 @@ interface LoansRepayAction extends TransactionAction { args: Parameters; } +type CustomLoansRepayAllArgs = [calculatedLimit: MonetaryAmount]; + interface LoansRepayAllAction extends TransactionAction { type: Transaction.LOANS_REPAY_ALL; - args: Parameters; + args: [...Parameters, ...CustomLoansRepayAllArgs]; } type LoansActions = diff --git a/src/utils/hooks/transaction/use-transaction.ts b/src/utils/hooks/transaction/use-transaction.ts index 3fa2cda32e..a83761d337 100644 --- a/src/utils/hooks/transaction/use-transaction.ts +++ b/src/utils/hooks/transaction/use-transaction.ts @@ -1,122 +1,154 @@ -import { ExtrinsicStatus } from '@polkadot/types/interfaces'; -import { ISubmittableResult } from '@polkadot/types/types'; +import { CurrencyExt, LiquidityPool } from '@interlay/interbtc-api'; +import { MonetaryAmount } from '@interlay/monetary-js'; import { mergeProps } from '@react-aria/utils'; -import { useCallback, useState } from 'react'; -import { MutationFunction, useMutation, UseMutationOptions, UseMutationResult } from 'react-query'; +import { useCallback, useRef, useState } from 'react'; +import { useErrorHandler } from 'react-error-boundary'; +import { MutationFunction, useMutation } from 'react-query'; +import { useInterval } from 'react-use'; +import { GOVERNANCE_TOKEN } from '@/config/relay-chains'; import { useSubstrate } from '@/lib/substrate'; +import { REFETCH_INTERVAL } from '@/utils/constants/api'; +import { useGetLiquidityPools } from '../api/amm/use-get-liquidity-pools'; +import { useGetBalances } from '../api/tokens/use-get-balances'; +import { useGetCurrencies } from '../api/use-get-currencies'; import { getExtrinsic, getStatus } from './extrinsics'; -import { Transaction, TransactionActions, TransactionArgs } from './types'; +import { Transaction, TransactionActions } from './types'; +import { + EstimateFeeParams, + FeeEstimateResult, + TransactionResult, + UseTransactionOptions, + UseTransactionResult, + UseTransactionWithoutType, + UseTransactionWithType +} from './types/hook'; import { useTransactionNotifications } from './use-transaction-notifications'; +import { estimateTransactionFee, getActionAmount, wrapWithTxFeeSwap } from './utils/fee'; +import { getParams } from './utils/params'; import { submitTransaction } from './utils/submit'; -type TransactionResult = { status: 'success' | 'error'; data: ISubmittableResult; error?: Error }; +const defaultFeeCurrency = GOVERNANCE_TOKEN; -// 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 ExecuteFunctions = ExecuteArgs | ExecuteTypeArgs; - -type ReactQueryUseMutationResult = Omit< - UseMutationResult, - 'mutate' | 'mutateAsync' ->; - -type UseTransactionResult = { - reject: (error?: Error) => void; - isSigned: boolean; -} & ReactQueryUseMutationResult & - ExecuteFunctions; - -const mutateTransaction: MutationFunction = async (params) => { - const extrinsics = await getExtrinsic(params); +const mutateTransaction: ( + feeAmount: MonetaryAmount | undefined, + pools: Array +) => MutationFunction = (feeAmount, pools) => async (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; + const baseExtrinsic = await getExtrinsic(params); + const feeWrappedExtrinsic = wrapWithTxFeeSwap(feeAmount, baseExtrinsic, pools); + + return submitTransaction( + window.bridge.api, + params.accountAddress, + feeWrappedExtrinsic, + expectedStatus, + params.events + ); }; // The three declared functions are use to infer types on diferent implementations -function useTransaction( - type: T, - options?: UseTransactionOptions -): Exclude, ExecuteTypeArgs>; -function useTransaction( - options?: UseTransactionOptions -): Exclude, ExecuteArgs>; +function useTransaction(type: T, options?: UseTransactionOptions): UseTransactionWithType; +function useTransaction(options?: UseTransactionOptions): UseTransactionWithoutType; function useTransaction( typeOrOptions?: T | UseTransactionOptions, options?: UseTransactionOptions ): UseTransactionResult { const { state } = useSubstrate(); + const { data: pools } = useGetLiquidityPools(); + const { getCurrencyFromTicker } = useGetCurrencies(true); + const { getBalance } = useGetBalances(); const [isSigned, setSigned] = useState(false); const { showSuccessModal, customStatus, ...mutateOptions } = (typeof typeOrOptions === 'string' ? options : typeOrOptions) || {}; - const notifications = useTransactionNotifications({ showSuccessModal }); + const mutateFee: ( + pools: Array + ) => MutationFunction = useCallback( + (pools) => async ({ ticker, params }) => { + const currency = getCurrencyFromTicker(ticker); - const handleMutate = () => setSigned(false); + const feeBalance = getBalance(currency.ticker)?.transferable; - const handleSigning = () => setSigned(true); + // returning undefined means that action amount is not based on fee currency + const actionAmount = getActionAmount(params, currency); - const handleError = (error: Error) => console.error(error.message); + const availableBalance = actionAmount ? feeBalance?.sub(actionAmount) : feeBalance; + + const amount = await estimateTransactionFee(currency, pools || [], params); + + return { + amount, + isValid: !!availableBalance && !!amount && availableBalance.gte(amount) + }; + }, + [getBalance, getCurrencyFromTicker] + ); + + const { mutate: feeMutate, ...feeMutation } = useMutation( + mutateFee(pools || []) + ); + + useErrorHandler(feeMutation.error); + + const estimateFeeParamsRef = useRef(); + + const handleEstimateFee = useCallback( + (ticker: string = defaultFeeCurrency.ticker) => ( + ...args: Parameters['fee']['estimate']> + ) => { + const params = getParams(args, typeOrOptions, customStatus); + + const variables = { ticker, params }; + + estimateFeeParamsRef.current = variables; + + feeMutate(variables); + }, + [typeOrOptions, customStatus, feeMutate] + ); + + const handleSetCurrency = (ticker?: string) => ({ estimate: handleEstimateFee(ticker) }); + + // Re-estimate fee based on latest stored variables + useInterval(() => { + if (!estimateFeeParamsRef.current || feeMutation.isLoading) return; + + feeMutate(estimateFeeParamsRef.current); + }, REFETCH_INTERVAL.MINUTE); + + const notifications = useTransactionNotifications({ showSuccessModal }); const { onSigning, ...optionsProp } = mergeProps( mutateOptions, { - onMutate: handleMutate, - onSigning: handleSigning, - onError: handleError + onMutate: () => setSigned(false), + onSigning: () => setSigned(true), + onError: (error: Error) => console.error(error.message), + onSuccess: () => feeMutation.reset() }, notifications.mutationProps ); - const { mutate, mutateAsync, ...transactionMutation } = useMutation(mutateTransaction, optionsProp); + const { mutate, mutateAsync, ...transactionMutation } = useMutation( + mutateTransaction(feeMutation.data?.amount, pools || []), + optionsProp + ); // Handles params for both type of implementations - const getParams = useCallback( + const getBaseParams = 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 }; - } + const params = getParams(args, typeOrOptions, customStatus); // Execution should only ran when authenticated const accountAddress = state.selectedAccount?.address; const variables = { ...params, - accountAddress, - timestamp: new Date().getTime(), - customStatus + accountAddress } as TransactionActions; return { @@ -131,20 +163,20 @@ function useTransaction( const handleExecute = useCallback( (...args: Parameters['execute']>) => { - const params = getParams(args); + const params = getBaseParams(args); return mutate(params); }, - [getParams, mutate] + [getBaseParams, mutate] ); const handleExecuteAsync = useCallback( (...args: Parameters['executeAsync']>) => { - const params = getParams(args); + const params = getBaseParams(args); return mutateAsync(params); }, - [getParams, mutateAsync] + [getBaseParams, mutateAsync] ); const handleReject = (error?: Error) => { @@ -161,9 +193,21 @@ function useTransaction( isSigned, reject: handleReject, execute: handleExecute, - executeAsync: handleExecuteAsync + executeAsync: handleExecuteAsync, + fee: { + ...feeMutation, + defaultCurrency: defaultFeeCurrency, + estimate: handleEstimateFee(), + setCurrency: handleSetCurrency, + detailsProps: { + defaultCurrency: defaultFeeCurrency, + amount: feeMutation.data?.amount, + // could possible be undefined, so we want to check for that + showInsufficientBalance: feeMutation.data?.isValid === false + } + } }; } export { useTransaction }; -export type { TransactionResult, UseTransactionResult }; +export type { FeeEstimateResult, TransactionResult, UseTransactionOptions, UseTransactionResult }; diff --git a/src/utils/hooks/transaction/utils/fee.ts b/src/utils/hooks/transaction/utils/fee.ts new file mode 100644 index 0000000000..2eeaaf24e2 --- /dev/null +++ b/src/utils/hooks/transaction/utils/fee.ts @@ -0,0 +1,179 @@ +import { + CurrencyExt, + CurrencyId, + ExtrinsicData, + isCurrencyEqual, + LiquidityPool, + MultiPath, + newCurrencyId, + Trade +} from '@interlay/interbtc-api'; +import { MonetaryAmount } from '@interlay/monetary-js'; +import { SubmittableExtrinsic } from '@polkadot/api/types'; + +import { GOVERNANCE_TOKEN } from '@/config/relay-chains'; + +import { getExtrinsic } from '../extrinsics'; +import { Transaction, TransactionActions } from '../types'; + +// 50% on top of trade to be safe (slippage, different weight) +const OUTPUT_AMOUNT_SAFE_OFFSET_MULTIPLIER = 1.5; + +const constructSwapPathPrimitive = (path: MultiPath): Array => { + const inputCurrency = newCurrencyId(window.bridge.api, path[0].input); + return [inputCurrency, ...path.map(({ output }) => newCurrencyId(window.bridge.api, output))]; +}; + +// Recursively double input amount until the trade with higher than minimum output +// amount is found. +const getOptimalTradeForTxFeeSwap = ( + minOutputAmount: MonetaryAmount, + inputAmount: MonetaryAmount, + pools: Array +): Trade => { + const trade = window.bridge.amm.getOptimalTrade(inputAmount, minOutputAmount.currency, pools); + + if (trade === null || trade.outputAmount.lt(minOutputAmount)) { + // If the output amount is lower than minimum trade or minimum txFee + // then double the input currency amount and check again. + return getOptimalTradeForTxFeeSwap(minOutputAmount, inputAmount.mul(2), pools); + } + return trade; +}; + +const getTxFeeSwapData = async ( + nativeTxFee: MonetaryAmount, + feeCurrency: CurrencyExt, + baseExtrinsic: SubmittableExtrinsic<'promise'>, + pools: Array +): Promise<{ swapPathPrimitive: Array; inputAmount: MonetaryAmount }> => { + // First we construct reverse direction trade to get estimated swap path and amount + const reverseDirectionTrade = window.bridge.amm.getOptimalTrade(nativeTxFee, feeCurrency, pools); + if (reverseDirectionTrade === null) { + throw new Error( + `Not possible to exchange ${feeCurrency.name} for ${nativeTxFee.currency.name}: trade path not found.` + ); + } + // Final native token transaction fee is estimated for base extrinsic wrapped in multiTransactionPayment call. + // NOTE: We assume here the reverse direction trade has similar weight. + const reverseDirectionExtrinsic = window.bridge.api.tx.multiTransactionPayment.withFeeSwapPath( + constructSwapPathPrimitive(reverseDirectionTrade.path), + reverseDirectionTrade.outputAmount.toString(true), + baseExtrinsic + ); + const withSwapTxFee = await window.bridge.transaction.getFeeEstimate(reverseDirectionExtrinsic); + const { inputAmount, path } = getOptimalTradeForTxFeeSwap( + withSwapTxFee.mul(OUTPUT_AMOUNT_SAFE_OFFSET_MULTIPLIER), + reverseDirectionTrade.outputAmount, + pools + ); + const swapPathPrimitive = constructSwapPathPrimitive(path); + + return { inputAmount, swapPathPrimitive }; +}; + +const estimateTransactionFee: ( + feeCurrency: CurrencyExt, + pools: Array, + params: TransactionActions +) => Promise> = async (feeCurrency, pools, params) => { + const baseExtrinsicData = await getExtrinsic(params); + const baseTxFee = await window.bridge.transaction.getFeeEstimate(baseExtrinsicData.extrinsic); + + if (isCurrencyEqual(feeCurrency, GOVERNANCE_TOKEN)) { + return baseTxFee; + } + + const { inputAmount: wrappedInSwapTxFee } = await getTxFeeSwapData( + baseTxFee, + feeCurrency, + baseExtrinsicData.extrinsic, + pools + ); + + return wrappedInSwapTxFee; +}; + +const wrapWithTxFeeSwap = ( + feeAmount: MonetaryAmount | undefined, + baseExtrinsicData: ExtrinsicData, + pools: Array +): ExtrinsicData => { + if (feeAmount === undefined || isCurrencyEqual(feeAmount.currency, GOVERNANCE_TOKEN)) { + return baseExtrinsicData; + } + + const trade = window.bridge.amm.getOptimalTrade(feeAmount, GOVERNANCE_TOKEN, pools); + + if (trade === null) { + throw new Error(`Trade path for ${feeAmount.currency.name} -> ${GOVERNANCE_TOKEN.name} not found.`); + } + + const swapPath = constructSwapPathPrimitive(trade.path); + const wrappedCall = window.bridge.api.tx.multiTransactionPayment.withFeeSwapPath( + swapPath, + feeAmount.toString(true), + baseExtrinsicData.extrinsic + ); + + return { extrinsic: wrappedCall }; +}; + +// MEMO: if we ever need toadd QTOKENS as a possible fee +// token, we will need to handle it here for loan withdraw and +// withdrawAll +const getActionAmount = ( + params: TransactionActions, + feeCurrency: CurrencyExt +): MonetaryAmount | undefined => { + let amounts: MonetaryAmount[] | undefined; + + switch (params.type) { + case Transaction.REDEEM_REQUEST: { + const [amount] = params.args; + amounts = [amount]; + break; + } + case Transaction.TOKENS_TRANSFER: { + const [, amount] = params.args; + amounts = [amount]; + break; + } + /* START - AMM */ + case Transaction.AMM_SWAP: { + const [trade] = params.args; + amounts = [trade.inputAmount]; + break; + } + case Transaction.AMM_ADD_LIQUIDITY: { + const [pooledAmounts] = params.args; + amounts = pooledAmounts; + break; + } + case Transaction.AMM_REMOVE_LIQUIDITY: { + const [amount] = params.args; + amounts = [amount]; + break; + } + /* END - AMM */ + /* START - LOANS */ + case Transaction.LOANS_REPAY: + case Transaction.LOANS_LEND: { + const [, amount] = params.args; + amounts = [amount]; + break; + } + case Transaction.LOANS_REPAY_ALL: { + const [, calculatedLimit] = params.args; + amounts = [calculatedLimit]; + break; + } + /* END - LOANS */ + } + + if (!amounts) return; + + return amounts.find((amount) => isCurrencyEqual(amount.currency, feeCurrency)); +}; + +export { estimateTransactionFee, getActionAmount, wrapWithTxFeeSwap }; diff --git a/src/utils/hooks/transaction/utils/form.ts b/src/utils/hooks/transaction/utils/form.ts new file mode 100644 index 0000000000..ccfbc58c61 --- /dev/null +++ b/src/utils/hooks/transaction/utils/form.ts @@ -0,0 +1,11 @@ +import { isFormDisabled, useForm } from '@/lib/form'; + +import { Transaction } from '../types'; +import { UseFeeEstimateResult } from '../types/hook'; + +const isTransactionFormDisabled = ( + form: ReturnType, + fee: UseFeeEstimateResult +): boolean => isFormDisabled(form) || !(fee.data && fee.data.isValid); + +export { isTransactionFormDisabled }; diff --git a/src/utils/hooks/transaction/utils/params.ts b/src/utils/hooks/transaction/utils/params.ts new file mode 100644 index 0000000000..2eaaf68810 --- /dev/null +++ b/src/utils/hooks/transaction/utils/params.ts @@ -0,0 +1,29 @@ +import { ExtrinsicStatus } from '@polkadot/types/interfaces'; + +import { Transaction, TransactionActions } from '../types'; +import { ExecuteFunctions } from '../types/hook'; + +const getParams = ( + args: Parameters['execute']>, + typeOrOptions?: T | Record, + customStatus?: ExtrinsicStatus['type'] +): TransactionActions => { + 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 }; + } + + return { + ...params, + timestamp: new Date().getTime(), + customStatus + } as TransactionActions; +}; + +export { getParams }; diff --git a/src/utils/hooks/use-select-currency.tsx b/src/utils/hooks/use-select-currency.tsx new file mode 100644 index 0000000000..e8eb954114 --- /dev/null +++ b/src/utils/hooks/use-select-currency.tsx @@ -0,0 +1,103 @@ +import { CurrencyExt, LiquidityPool } from '@interlay/interbtc-api'; +import { MonetaryAmount } from '@interlay/monetary-js'; +import Big from 'big.js'; +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; + +import { StoreType } from '@/common/types/util.types'; +import { convertMonetaryAmountToValueInUSD, formatUSD } from '@/common/utils/utils'; +import { TokenData } from '@/component-library'; +import { GOVERNANCE_TOKEN } from '@/config/relay-chains'; + +import { getCoinIconProps } from '../helpers/coin-icon'; +import { getTokenPrice } from '../helpers/prices'; +import { useGetLiquidityPools } from './api/amm/use-get-liquidity-pools'; +import { useGetOracleCurrencies } from './api/oracle/use-get-oracle-currencies'; +import { useGetBalances } from './api/tokens/use-get-balances'; +import { useGetCurrencies } from './api/use-get-currencies'; +import { useGetPrices } from './api/use-get-prices'; + +type SelectCurrencyResult = { + items: TokenData[]; +}; + +const canBeSwappedForNativeCurrency = (pools: Array) => (currency: CurrencyExt): boolean => { + const trade = window.bridge.amm.getOptimalTrade(new MonetaryAmount(currency, 1), GOVERNANCE_TOKEN, pools); + return trade !== null; +}; + +const canBeUsedAsIssueGriefingCollateral = (oracleCurrencies: Array) => ( + currency: CurrencyExt +): boolean => { + return oracleCurrencies.map(({ ticker }) => ticker).includes(currency.ticker); +}; + +enum SelectCurrencyFilter { + TRADEABLE_FOR_NATIVE_CURRENCY = 'TRADEABLE_FOR_NATIVE_CURRENCY', + ISSUE_GRIEFING_COLLATERAL_CURRENCY = 'ISSUE_GRIEFING_COLLATERAL_CURRENCY' +} + +const useSelectCurrency = (filter?: SelectCurrencyFilter): SelectCurrencyResult => { + const { bridgeLoaded } = useSelector((state: StoreType) => state.general); + + const { data: currencies } = useGetCurrencies(bridgeLoaded); + + const { getAvailableBalance } = useGetBalances(); + const prices = useGetPrices(); + + const { data: pools } = useGetLiquidityPools(); + const { data: oracleCurrencies } = useGetOracleCurrencies(); + + const filteredCurrencies = useMemo(() => { + if (currencies === undefined) { + return []; + } + switch (filter) { + case SelectCurrencyFilter.TRADEABLE_FOR_NATIVE_CURRENCY: { + if (pools === undefined) { + return []; + } + return currencies.filter(canBeSwappedForNativeCurrency(pools)); + } + case SelectCurrencyFilter.ISSUE_GRIEFING_COLLATERAL_CURRENCY: { + if (oracleCurrencies === undefined) { + return []; + } + return currencies.filter(canBeUsedAsIssueGriefingCollateral(oracleCurrencies)); + } + default: + return currencies; + } + }, [currencies, pools, filter, oracleCurrencies]); + + const items = useMemo(() => { + let parsedTokenData = filteredCurrencies.map((currency) => { + const balance = getAvailableBalance(currency.ticker); + const balanceUSD = balance + ? convertMonetaryAmountToValueInUSD(balance, getTokenPrice(prices, currency.ticker)?.usd) + : 0; + + return { + balance: balance?.toBig() || Big(0), + balanceUSD: formatUSD(balanceUSD || 0, { compact: true }), + value: currency.ticker, + ...getCoinIconProps(currency) + }; + }); + + if (filter !== undefined) { + parsedTokenData = parsedTokenData.sort((currencyA, currencyB) => + currencyB.balance.sub(currencyA.balance).toNumber() + ); + } + + return parsedTokenData.map((currency) => ({ ...currency, balance: currency.balance.toString() })); + }, [filteredCurrencies, getAvailableBalance, filter, prices]); + + return { + items + }; +}; + +export { SelectCurrencyFilter, useSelectCurrency }; +export type { SelectCurrencyResult }; diff --git a/src/utils/hooks/use-tab-page-location.tsx b/src/utils/hooks/use-tab-page-location.tsx new file mode 100644 index 0000000000..4ac2e537e4 --- /dev/null +++ b/src/utils/hooks/use-tab-page-location.tsx @@ -0,0 +1,37 @@ +import { Key } from 'react'; +import { useHistory, useLocation } from 'react-router'; + +import { TabsProps } from '@/component-library'; + +const queryString = require('query-string'); + +type UseTabPageLocationResult = { + tabsProps: Pick; +}; + +const useTabPageLocation = (): UseTabPageLocationResult => { + const history = useHistory(); + const location = useLocation(); + const currentQueryParameters = queryString.parse(location.search); + + const handleSelectionChange = (key: Key) => { + const queryParameters = queryString.parse(location.search); + queryParameters.tab = key; + const updatedQueryString = queryString.stringify(queryParameters); + + history.replace({ + pathname: location.pathname, + search: updatedQueryString + }); + }; + + return { + tabsProps: { + defaultSelectedKey: currentQueryParameters.tab, + onSelectionChange: handleSelectionChange + } + }; +}; + +export { useTabPageLocation }; +export type { UseTabPageLocationResult }; diff --git a/yarn.lock b/yarn.lock index 8b0e526b36..8835312fcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2120,10 +2120,10 @@ dependencies: axios "^0.21.1" -"@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== +"@interlay/interbtc-api@2.3.4": + version "2.3.4" + resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-2.3.4.tgz#43454d12d68a48126d2f19c592d4b15fa19141a0" + integrity sha512-2oOZbXUQ5B16cCDcKzV9+4RGaxLOwCOihUjAPQeIB2dmbXmLJHp6ijl+70C6UuSHGv3As2dNYj/+z9k/THFgZw== dependencies: "@interlay/esplora-btc-api" "0.4.0" "@interlay/interbtc-types" "1.12.0" From 8656b5b70eac10fadde4bec5278bd532c9596d6d Mon Sep 17 00:00:00 2001 From: Thomas Jeatt Date: Mon, 26 Jun 2023 11:02:38 +0100 Subject: [PATCH 050/241] chore: release v2.35.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a4d532a34f..fe18101cd5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interbtc-ui", - "version": "2.34.1", + "version": "2.35.0", "private": true, "dependencies": { "@craco/craco": "^6.1.1", From f3a7a1290a0a41887433bed76367c4abbbc428a7 Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Tue, 27 Jun 2023 10:39:33 +0100 Subject: [PATCH 051/241] Tom/feature/wallet buttons (#1346) * refactor: add tab props * feature: add bridge button to assets table * refactor: don't show buy button for wrapped token --- .../Transfer/TransferForms/TransferForms.tsx | 49 ++++++++++--------- .../AvailableAssetsTable/ActionsCell.tsx | 17 +++++-- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/pages/Transfer/TransferForms/TransferForms.tsx b/src/pages/Transfer/TransferForms/TransferForms.tsx index 7d7cd2b233..50c7ce47e5 100644 --- a/src/pages/Transfer/TransferForms/TransferForms.tsx +++ b/src/pages/Transfer/TransferForms/TransferForms.tsx @@ -1,30 +1,35 @@ import { Flex, Tabs, TabsItem } from '@/component-library'; import MainContainer from '@/parts/MainContainer'; +import { useTabPageLocation } from '@/utils/hooks/use-tab-page-location'; import { CrossChainTransferForm, TransferForm } from './components'; import { StyledCard, StyledFormWrapper, StyledWrapper } from './TransferForms.styles'; -const TransferForms = (): JSX.Element => ( - - - - - - - - - - - - - - - - - - - - -); +const TransferForms = (): JSX.Element => { + const { tabsProps } = useTabPageLocation(); + + return ( + + + + + + + + + + + + + + + + + + + + + ); +}; export default TransferForms; diff --git a/src/pages/Wallet/WalletOverview/components/AvailableAssetsTable/ActionsCell.tsx b/src/pages/Wallet/WalletOverview/components/AvailableAssetsTable/ActionsCell.tsx index 4eeb9f33c4..309604552e 100644 --- a/src/pages/Wallet/WalletOverview/components/AvailableAssetsTable/ActionsCell.tsx +++ b/src/pages/Wallet/WalletOverview/components/AvailableAssetsTable/ActionsCell.tsx @@ -76,10 +76,6 @@ const ActionsCell = ({ {t('redeem')} )} - {/* TODO: add when xcm re-vamp is added */} - {/* - {t('transfer')} - */} {isPooledAsset && ( )} + {!isWrappedToken && ( + + Bridge + + )}
); From 0eb4d0045bd458fb9879fb3170ad624e50e287a2 Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Tue, 27 Jun 2023 10:39:54 +0100 Subject: [PATCH 052/241] [wallet] add default currencies to wallet (#1335) * refactor: add default currencies to wallet * refactor: use NATIVE_CURRENCIES --- .../components/AvailableAssetsTable/AvailableAssetsTable.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/Wallet/WalletOverview/components/AvailableAssetsTable/AvailableAssetsTable.tsx b/src/pages/Wallet/WalletOverview/components/AvailableAssetsTable/AvailableAssetsTable.tsx index 923a7eeec2..974f399d1c 100644 --- a/src/pages/Wallet/WalletOverview/components/AvailableAssetsTable/AvailableAssetsTable.tsx +++ b/src/pages/Wallet/WalletOverview/components/AvailableAssetsTable/AvailableAssetsTable.tsx @@ -8,6 +8,7 @@ import { useMediaQuery } from '@/component-library/utils/use-media-query'; import { Cell } from '@/components'; import { AssetCell, DataGrid } from '@/components/DataGrid'; import { GOVERNANCE_TOKEN, WRAPPED_TOKEN } from '@/config/relay-chains'; +import { NATIVE_CURRENCIES } from '@/utils/constants/currency'; import { getCoinIconProps } from '@/utils/helpers/coin-icon'; import { getTokenPrice } from '@/utils/helpers/prices'; import { BalanceData } from '@/utils/hooks/api/tokens/use-get-balances'; @@ -46,7 +47,9 @@ const AvailableAssetsTable = ({ balances, pooledTickers }: AvailableAssetsTableP const rows: AvailableAssetsRows[] = useMemo(() => { const data = balances ? Object.values(balances) : []; - const filteredData = showZeroBalances ? data : data.filter((balance) => !balance.transferable.isZero()); + const filteredData = showZeroBalances + ? data + : data.filter((balance) => NATIVE_CURRENCIES.includes(balance.currency) || !balance.transferable.isZero()); return filteredData.map( ({ currency, transferable }): AvailableAssetsRows => { From 08aa1081b634a79a05347ce195435d84fbd3f99d Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Tue, 27 Jun 2023 10:58:58 +0100 Subject: [PATCH 053/241] chore: update navigation (#1344) --- src/assets/locales/en/translation.json | 3 +-- src/pages/Transfer/TransferForms/TransferForms.tsx | 2 +- src/parts/Sidebar/SidebarContent/Navigation/index.tsx | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index cc589b2767..5c9a686eb8 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -76,11 +76,10 @@ "redeem": "Redeem", "nav_bridge": "Bridge", "nav_strategies": "Strategies", - "nav_transfer": "Transfer", + "nav_transfer": "Transfer / Bridge", "nav_lending": "Lending", "nav_swap": "Swap", "nav_pools": "Pools", - "nav_transactions": "My Transactions", "nav_staking": "Staking", "nav_stats": "Stats", "nav_dashboard": "Dashboard", diff --git a/src/pages/Transfer/TransferForms/TransferForms.tsx b/src/pages/Transfer/TransferForms/TransferForms.tsx index 50c7ce47e5..d368455ab9 100644 --- a/src/pages/Transfer/TransferForms/TransferForms.tsx +++ b/src/pages/Transfer/TransferForms/TransferForms.tsx @@ -19,7 +19,7 @@ const TransferForms = (): JSX.Element => {
- + diff --git a/src/parts/Sidebar/SidebarContent/Navigation/index.tsx b/src/parts/Sidebar/SidebarContent/Navigation/index.tsx index a71aea1543..790ee68503 100644 --- a/src/parts/Sidebar/SidebarContent/Navigation/index.tsx +++ b/src/parts/Sidebar/SidebarContent/Navigation/index.tsx @@ -83,7 +83,7 @@ const Navigation = ({ disabled: !isStrategiesEnabled }, { - name: 'nav_bridge', + name: `${WRAPPED_TOKEN_SYMBOL}`, link: PAGES.BRIDGE, icon: ArrowPathIcon, hidden: false From 2bca493e05923f943b43886a588b3b95e7e2b7f5 Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Tue, 27 Jun 2023 13:20:17 +0100 Subject: [PATCH 054/241] refatctor: remove LBANK configuration and assets (#1355) --- src/assets/img/exchanges/lbank-logo.svg | 1 - src/components/FundWallet/use-entities.tsx | 5 ----- src/parts/Topbar/GetGovernanceTokenUI/index.tsx | 5 ----- 3 files changed, 11 deletions(-) delete mode 100644 src/assets/img/exchanges/lbank-logo.svg diff --git a/src/assets/img/exchanges/lbank-logo.svg b/src/assets/img/exchanges/lbank-logo.svg deleted file mode 100644 index 8affe1645f..0000000000 --- a/src/assets/img/exchanges/lbank-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/FundWallet/use-entities.tsx b/src/components/FundWallet/use-entities.tsx index afb4868502..5ab066ed40 100644 --- a/src/components/FundWallet/use-entities.tsx +++ b/src/components/FundWallet/use-entities.tsx @@ -5,7 +5,6 @@ import BANXA_KITNSUGI from '@/assets/img/banxa-white.png'; import { ReactComponent as AcalaLogoIcon } from '@/assets/img/exchanges/acala-logo.svg'; import { ReactComponent as GateLogoIcon } from '@/assets/img/exchanges/gate-logo.svg'; import { ReactComponent as KrakenLogoIcon } from '@/assets/img/exchanges/kraken-logo.svg'; -import { ReactComponent as LbankLogoIcon } from '@/assets/img/exchanges/lbank-logo.svg'; import { ReactComponent as MexcLogoForInterlayIcon } from '@/assets/img/exchanges/mexc-logo-for-interlay.svg'; import { ReactComponent as MexcLogoForKintsugiIcon } from '@/assets/img/exchanges/mexc-logo-for-kintsugi.svg'; import { ReactComponent as StellaSwapLogoIcon } from '@/assets/img/exchanges/stellaswap-logo.svg'; @@ -57,10 +56,6 @@ const useEntities = (): UseEntitiesResult => { { pathname: 'https://www.mexc.com/exchange/INTR_USDT', icon: - }, - { - pathname: 'https://www.lbank.info/exchange/intr/usdt', - icon: } ]; diff --git a/src/parts/Topbar/GetGovernanceTokenUI/index.tsx b/src/parts/Topbar/GetGovernanceTokenUI/index.tsx index f6637bdd01..e37aad3b50 100644 --- a/src/parts/Topbar/GetGovernanceTokenUI/index.tsx +++ b/src/parts/Topbar/GetGovernanceTokenUI/index.tsx @@ -6,7 +6,6 @@ import { useDispatch, useSelector } from 'react-redux'; import { ReactComponent as AcalaLogoIcon } from '@/assets/img/exchanges/acala-logo.svg'; import { ReactComponent as GateLogoIcon } from '@/assets/img/exchanges/gate-logo.svg'; import { ReactComponent as KrakenLogoIcon } from '@/assets/img/exchanges/kraken-logo.svg'; -import { ReactComponent as LbankLogoIcon } from '@/assets/img/exchanges/lbank-logo.svg'; import { ReactComponent as MexcLogoForInterlayIcon } from '@/assets/img/exchanges/mexc-logo-for-interlay.svg'; import { ReactComponent as MexcLogoForKintsugiIcon } from '@/assets/img/exchanges/mexc-logo-for-kintsugi.svg'; import { ReactComponent as StellaSwapLogoIcon } from '@/assets/img/exchanges/stellaswap-logo.svg'; @@ -48,10 +47,6 @@ if (process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT) { { link: 'https://www.mexc.com/exchange/INTR_USDT', icon: - }, - { - link: 'https://www.lbank.info/exchange/intr/usdt', - icon: } ]; } else if (process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA) { From 6d32c91c8400241ae589f0fd0b73e789f0c30bdd Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Tue, 27 Jun 2023 13:20:27 +0100 Subject: [PATCH 055/241] feature: add LDOT icon (#1356) --- src/component-library/CoinIcon/icons/LDOT.tsx | 44 +++++++++++++++++++ src/component-library/CoinIcon/icons/index.ts | 1 + src/component-library/CoinIcon/utils.ts | 2 + 3 files changed, 47 insertions(+) create mode 100644 src/component-library/CoinIcon/icons/LDOT.tsx diff --git a/src/component-library/CoinIcon/icons/LDOT.tsx b/src/component-library/CoinIcon/icons/LDOT.tsx new file mode 100644 index 0000000000..b826a718de --- /dev/null +++ b/src/component-library/CoinIcon/icons/LDOT.tsx @@ -0,0 +1,44 @@ +import { forwardRef } from 'react'; + +import { Icon, IconProps } from '@/component-library/Icon'; + +const LDOT = forwardRef((props, ref) => ( + + LDOT + + + + + + + + + + + + + + + + + +)); + +LDOT.displayName = 'LDOT'; + +export { LDOT }; diff --git a/src/component-library/CoinIcon/icons/index.ts b/src/component-library/CoinIcon/icons/index.ts index 610888ac36..fc94af8105 100644 --- a/src/component-library/CoinIcon/icons/index.ts +++ b/src/component-library/CoinIcon/icons/index.ts @@ -8,6 +8,7 @@ export { KAR } from './KAR'; export { KBTC } from './KBTC'; export { KINT } from './KINT'; export { KSM } from './KSM'; +export { LDOT } from './LDOT'; export { LKSM } from './LKSM'; export { LSKSM } from './LSKSM'; export { MOVR } from './MOVR'; diff --git a/src/component-library/CoinIcon/utils.ts b/src/component-library/CoinIcon/utils.ts index c9ebbb691f..04f540e234 100644 --- a/src/component-library/CoinIcon/utils.ts +++ b/src/component-library/CoinIcon/utils.ts @@ -9,6 +9,7 @@ import { KBTC, KINT, KSM, + LDOT, LKSM, LSKSM, MOVR, @@ -34,6 +35,7 @@ export const coins: Record = { KBTC, KINT, KSM, + LDOT, LKSM, USDT, VKSM, From 9f967f52d4b54a70a0bf02d8ea10d1f5fe2790ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Slan=C3=BD?= <47864599+peterslany@users.noreply.github.com> Date: Tue, 27 Jun 2023 15:44:02 +0200 Subject: [PATCH 056/241] Peter/refactor fetch oracle status from chain (#1359) * chore: update monetary to latest 0.7.3 * refactor: fetch oracle status from chain * chore: remove commented-out code --- .../cards/OracleStatusCard/index.tsx | 57 ++++--------------- .../fetchers/oracle-exchange-rates-fetcher.ts | 40 +------------ .../hooks/api/oracle/use-get-oracle-status.ts | 31 ++++++++++ 3 files changed, 43 insertions(+), 85 deletions(-) create mode 100644 src/utils/hooks/api/oracle/use-get-oracle-status.ts diff --git a/src/pages/Dashboard/cards/OracleStatusCard/index.tsx b/src/pages/Dashboard/cards/OracleStatusCard/index.tsx index df6db65a1f..7718452741 100644 --- a/src/pages/Dashboard/cards/OracleStatusCard/index.tsx +++ b/src/pages/Dashboard/cards/OracleStatusCard/index.tsx @@ -1,23 +1,16 @@ import { CurrencyExt } from '@interlay/interbtc-api'; import { Bitcoin, ExchangeRate } from '@interlay/monetary-js'; import clsx from 'clsx'; -import { useErrorHandler, withErrorBoundary } from 'react-error-boundary'; +import { withErrorBoundary } from 'react-error-boundary'; import { useTranslation } from 'react-i18next'; -import { useQuery } from 'react-query'; -import { useSelector } from 'react-redux'; -import { StoreType } from '@/common/types/util.types'; import { RELAY_CHAIN_NATIVE_TOKEN, RELAY_CHAIN_NATIVE_TOKEN_SYMBOL } from '@/config/relay-chains'; import ErrorFallback from '@/legacy-components/ErrorFallback'; import Ring64, { Ring64Subtitle, Ring64Title, Ring64Value } from '@/legacy-components/Ring64'; -import genericFetcher, { GENERIC_FETCHER } from '@/services/fetchers/generic-fetcher'; -import { - BtcToCurrencyOracleStatus, - latestExchangeRateFetcher, - ORACLE_LATEST_EXCHANGE_RATE_FETCHER -} from '@/services/fetchers/oracle-exchange-rates-fetcher'; import { PAGES } from '@/utils/constants/links'; import { getColorShade } from '@/utils/helpers/colors'; +import { OracleStatus, useGetOracleStatus } from '@/utils/hooks/api/oracle/use-get-oracle-status'; +import { useGetExchangeRate } from '@/utils/hooks/api/use-get-exchange-rate'; import Stats, { StatsDd, StatsDt, StatsRouterLink } from '../../Stats'; import DashboardCard from '../DashboardCard'; @@ -28,53 +21,23 @@ interface Props { const OracleStatusCard = ({ hasLinks }: Props): JSX.Element => { const { t } = useTranslation(); - const { bridgeLoaded } = useSelector((state: StoreType) => state.general); - const { - isIdle: oracleTimeoutIdle, - isLoading: oracleTimeoutLoading, - data: oracleTimeout, - error: oracleTimeoutError - } = useQuery([GENERIC_FETCHER, 'oracle', 'getOnlineTimeout'], genericFetcher(), { - enabled: !!bridgeLoaded - }); - useErrorHandler(oracleTimeoutError); - - const { - isIdle: oracleStatusIdle, - isLoading: oracleStatusLoading, - data: oracleStatus, - error: oracleStatusError - } = useQuery( - [ORACLE_LATEST_EXCHANGE_RATE_FETCHER, RELAY_CHAIN_NATIVE_TOKEN, oracleTimeout], - latestExchangeRateFetcher, - { - enabled: !!oracleTimeout - } + const { data: oracleStatus, isLoading: isLoadingOracleStatus } = useGetOracleStatus(); + const { data: relayChainExchangeRate, isLoading: isLoadingExchangeRate } = useGetExchangeRate( + RELAY_CHAIN_NATIVE_TOKEN ); - useErrorHandler(oracleStatusError); const renderContent = () => { // TODO: should use skeleton loaders - if (oracleStatusIdle || oracleStatusLoading || oracleTimeoutIdle || oracleTimeoutLoading) { + if (isLoadingOracleStatus || isLoadingExchangeRate) { return <>Loading...; } - if (oracleTimeout === undefined) { - throw new Error('Something went wrong!'); - } - - const exchangeRate = oracleStatus - ? new ExchangeRate( - Bitcoin, - RELAY_CHAIN_NATIVE_TOKEN, - oracleStatus.exchangeRate.toBig(), - 0, - 0 - ) + const exchangeRate = relayChainExchangeRate + ? new ExchangeRate(Bitcoin, RELAY_CHAIN_NATIVE_TOKEN, relayChainExchangeRate.toBig(), 0, 0) : 0; - const oracleOnline = oracleStatus && oracleStatus.online; + const oracleOnline = oracleStatus && oracleStatus === OracleStatus.ONLINE; let statusText; let statusCircleText; diff --git a/src/services/fetchers/oracle-exchange-rates-fetcher.ts b/src/services/fetchers/oracle-exchange-rates-fetcher.ts index 3102c038ad..8b83a31d76 100644 --- a/src/services/fetchers/oracle-exchange-rates-fetcher.ts +++ b/src/services/fetchers/oracle-exchange-rates-fetcher.ts @@ -5,15 +5,12 @@ import { Bitcoin, ExchangeRate } from '@interlay/monetary-js'; import graphqlFetcher, { GRAPHQL_FETCHER } from '@/services/fetchers/graphql-fetcher'; import { getCurrencyEqualityCondition } from '@/utils/helpers/currencies'; -import oracleExchangeRatesQuery, { composableExchangeRateSubquery } from '../queries/oracle-exchange-rates-query'; +import { composableExchangeRateSubquery } from '../queries/oracle-exchange-rates-query'; -const ORACLE_LATEST_EXCHANGE_RATE_FETCHER = 'oracle-exchange-rate-fetcher'; const ORACLE_ALL_LATEST_UPDATES_FETCHER = 'oracle-all-latest-updates-fetcher'; type BtcToCurrencyOracleStatus = OracleStatus; -type LatestExchangeRateFetcherParams = [key: string, currency: CurrencyExt, onlineTimeout: number]; - type AllOracleLatestUpdatesFetcherParams = [ key: string, currency: CurrencyExt, @@ -40,34 +37,6 @@ function decodeOracleValues( }; } -// TODO: should type properly (`Relay`) -const latestExchangeRateFetcher = async ( - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - { queryKey }: any -): Promise => { - const [key, currency, onlineTimeout] = queryKey as LatestExchangeRateFetcherParams; - - if (key !== ORACLE_LATEST_EXCHANGE_RATE_FETCHER) throw new Error('Invalid key!'); - - // TODO: should type properly (`Relay`) - // TODO: Need to refactor when we want to support lend tokens as collateral for vaults. - const cond = 'foreignAsset' in currency ? `asset_eq: ${currency.foreignAsset.id}` : `token_eq: ${currency.ticker}`; - const latestOracleData = await graphqlFetcher>()({ - queryKey: [GRAPHQL_FETCHER, oracleExchangeRatesQuery(`typeKey: {${cond}}`)] - }); - - // TODO: should type properly (`Relay`) - const rates = latestOracleData?.data?.oracleUpdates || []; - return rates.map((update) => - decodeOracleValues( - update, - currency, - onlineTimeout, - new Map([[update.oracleId, update.oracleId]]) // placeholder, as not used in card - ) - )[0]; -}; - const allLatestSubmissionsFetcher = async ( // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types { queryKey }: any @@ -95,11 +64,6 @@ const allLatestSubmissionsFetcher = async ( .map(([update]) => decodeOracleValues(update, currency, onlineTimeout, namesMap)); }; -export { - allLatestSubmissionsFetcher, - latestExchangeRateFetcher, - ORACLE_ALL_LATEST_UPDATES_FETCHER, - ORACLE_LATEST_EXCHANGE_RATE_FETCHER -}; +export { allLatestSubmissionsFetcher, ORACLE_ALL_LATEST_UPDATES_FETCHER }; export type { BtcToCurrencyOracleStatus }; diff --git a/src/utils/hooks/api/oracle/use-get-oracle-status.ts b/src/utils/hooks/api/oracle/use-get-oracle-status.ts new file mode 100644 index 0000000000..a3b4fd632a --- /dev/null +++ b/src/utils/hooks/api/oracle/use-get-oracle-status.ts @@ -0,0 +1,31 @@ +import { useQuery } from 'react-query'; + +import { REFETCH_INTERVAL } from '@/utils/constants/api'; + +interface UseGetOracleStatusResult { + data: OracleStatus | undefined; + isLoading: boolean; +} + +enum OracleStatus { + ONLINE = 'ONLINE', + OFFLINE = 'OFFLINE' +} + +const getOracleStatus = async (): Promise => { + const isOracleOnline = await window.bridge.oracle.isOnline(); + return isOracleOnline ? OracleStatus.ONLINE : OracleStatus.OFFLINE; +}; + +const useGetOracleStatus = (): UseGetOracleStatusResult => { + const { data, isLoading } = useQuery({ + queryKey: 'oracle-status', + queryFn: getOracleStatus, + enabled: window.bridge !== undefined, + refetchInterval: REFETCH_INTERVAL.MINUTE + }); + + return { data, isLoading }; +}; + +export { OracleStatus, useGetOracleStatus }; From 690d9515001339c87033212f12dc54dc340a7671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Slan=C3=BD?= <47864599+peterslany@users.noreply.github.com> Date: Tue, 27 Jun 2023 15:49:48 +0200 Subject: [PATCH 057/241] Peter/fix add wrapped currency as security deposit option (#1360) * chore: update monetary to latest 0.7.3 * fix: add wrapped token to useGetOracleCurrencies result --- src/utils/hooks/api/oracle/use-get-oracle-currencies.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/hooks/api/oracle/use-get-oracle-currencies.ts b/src/utils/hooks/api/oracle/use-get-oracle-currencies.ts index ea7b2867f1..f0edcd8351 100644 --- a/src/utils/hooks/api/oracle/use-get-oracle-currencies.ts +++ b/src/utils/hooks/api/oracle/use-get-oracle-currencies.ts @@ -1,6 +1,8 @@ import { CurrencyExt, InterbtcPrimitivesCurrencyId } from '@interlay/interbtc-api'; import { useQuery } from 'react-query'; +import { WRAPPED_TOKEN } from '@/config/relay-chains'; + import { useGetCurrencies } from '../use-get-currencies'; const getOracleCurrencies = ( @@ -16,7 +18,8 @@ const getOracleCurrencies = ( getCurrencyFromIdPrimitive(window.bridge.api.createType('InterbtcPrimitivesCurrencyId', ExchangeRate)) ); - return currencies; + // Add wrapped token manually as its exchange rate is always available - equal to BTC. + return [WRAPPED_TOKEN, ...currencies]; }; interface UseGetOracleCurrenciesResult { From 8cf597dee8d5792d8d15e7af8384c732035fb340 Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Wed, 28 Jun 2023 08:36:04 +0100 Subject: [PATCH 058/241] chore: update price impact warning copy (#1358) --- src/assets/locales/en/translation.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index 5c9a686eb8..e27d381bb9 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -603,8 +603,8 @@ "insufficient_liquidity_trade": "Insufficient liquidity for this trade", "from": "From", "to": "To", - "swap_has_price_inpact_of": "Considering external price sources, this swap has a price impact of", - "you_are_swapping_input_for_output": "Your are swapping {{inputAmount}} {{inputTicker}} ({{inputAmountUSD}}) for {{outputAmount}} {{outputTicker}} ({{outputAmountUSD}})", + "swap_has_price_inpact_of": "According to external price sources, this swap would result in a monetary loss of", + "you_are_swapping_input_for_output": "You are swapping {{inputAmount}} {{inputTicker}} ({{inputAmountUSD}}) for {{outputAmount}} {{outputTicker}} ({{outputAmountUSD}})", "cancel_swap": "Cancel swap", "confirm_swap": "Confirm swap" }, From 349df79f929e46eb49548b7d479a8f6d32583cd3 Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Wed, 28 Jun 2023 08:37:31 +0100 Subject: [PATCH 059/241] [transfer/bridge] open correct tab (#1366) * fix: bridge query parameter * fix: revert to previous tab name --- src/pages/Transfer/TransferForms/TransferForms.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Transfer/TransferForms/TransferForms.tsx b/src/pages/Transfer/TransferForms/TransferForms.tsx index d368455ab9..92dea5234f 100644 --- a/src/pages/Transfer/TransferForms/TransferForms.tsx +++ b/src/pages/Transfer/TransferForms/TransferForms.tsx @@ -19,7 +19,7 @@ const TransferForms = (): JSX.Element => {
- + From 9281aa2d459ba3a6baf4af02c6035407028625e5 Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Wed, 28 Jun 2023 10:08:02 +0100 Subject: [PATCH 060/241] refactor: close redeem modal (#1367) * refactor: close redeem modal * fix: correct user messaging copy * fix: remove unnecessary translation * fix: correct copy --- src/assets/locales/en/translation.json | 4 ++-- .../LegacyRedeemModal/LegacyRedeemModal.tsx | 20 +++---------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index e27d381bb9..c508449cb8 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -173,13 +173,13 @@ "with_added": "(≈ {{amountPrice}}) with added", "as_compensation_instead": "(≈ {{compensationPrice}}) as compensation instead.", "view_progress": "View Progress", + "close": "Close", "btc_destination_address": "BTC destination address", "you_will_receive": "Redeem {{wrappedTokenSymbol}} 1:1 for BTC", "waiting_for": "Waiting for", "vault": "Vault", - "typically_takes": "This typically takes only a few minutes but may sometimes take up to 6 hours.", + "typically_takes": "The BTC typically takes only a few minutes to arrive but may take up to 6 hours.", "from_vault": "from Vault", - "we_will_inform_you_btc": "We will inform you when the BTC payment is executed.", "redeem_processed": "Your Redeem request is being processed", "retried": "Retried", "error_more_than_6_blocks_behind": "You can't redeem {{wrappedTokenSymbol}} at the moment because {{wrappedTokenSymbol}} parachain is more than 6 blocks behind.", diff --git a/src/pages/Bridge/BridgeOverview/components/LegacyRedeemModal/LegacyRedeemModal.tsx b/src/pages/Bridge/BridgeOverview/components/LegacyRedeemModal/LegacyRedeemModal.tsx index d22d63372a..2742546756 100644 --- a/src/pages/Bridge/BridgeOverview/components/LegacyRedeemModal/LegacyRedeemModal.tsx +++ b/src/pages/Bridge/BridgeOverview/components/LegacyRedeemModal/LegacyRedeemModal.tsx @@ -8,16 +8,12 @@ import { Modal, ModalBody } from '@/component-library'; import AddressWithCopyUI from '@/legacy-components/AddressWithCopyUI'; import InterlayDefaultContainedButton from '@/legacy-components/buttons/InterlayDefaultContainedButton'; import { Props as ModalProps } from '@/legacy-components/UI/InterlayModal'; -import InterlayRouterLink from '@/legacy-components/UI/InterlayRouterLink'; import { ForeignAssetIdLiteral } from '@/types/currency'; -import { PAGES, QUERY_PARAMETERS } from '@/utils/constants/links'; import { KUSAMA, POLKADOT } from '@/utils/constants/relay-chain-names'; import { getColorShade } from '@/utils/helpers/colors'; import { getTokenPrice } from '@/utils/helpers/prices'; import { useGetPrices } from '@/utils/hooks/api/use-get-prices'; -const queryString = require('query-string'); - const USER_BTC_ADDRESS = 'user-btc-address'; interface CustomProps { @@ -82,7 +78,6 @@ const LegacyRedeemModal = ({ open, onClose, request }: CustomProps & Omit
-

{t('redeem_page.we_will_inform_you_btc')}

- - - {t('redeem_page.view_progress')} - - + + {t('redeem_page.close')} +
From 92554fc7c543d700b49d6f9da39526571d79aec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sim=C3=A3o?= Date: Wed, 28 Jun 2023 12:24:17 +0100 Subject: [PATCH 061/241] feat: change LoadingSpinner styles and CTA loading spinner (#1372) --- src/component-library/CTA/CTA.style.tsx | 16 ++++++++++++++-- src/component-library/CTA/CTA.tsx | 7 +++---- src/component-library/theme/theme.interlay.css | 8 ++++---- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/component-library/CTA/CTA.style.tsx b/src/component-library/CTA/CTA.style.tsx index 017da13651..481439e4e9 100644 --- a/src/component-library/CTA/CTA.style.tsx +++ b/src/component-library/CTA/CTA.style.tsx @@ -1,7 +1,8 @@ import styled from 'styled-components'; +import { LoadingSpinner } from '../LoadingSpinner'; import { theme } from '../theme'; -import { CTASizes } from '../utils/prop-types'; +import { CTASizes, CTAVariants } from '../utils/prop-types'; interface StyledCTAProps { $fullWidth: boolean; @@ -9,6 +10,10 @@ interface StyledCTAProps { $isFocusVisible?: boolean; } +type StyledLoadingSpinnerProps = { + $variant: CTAVariants; +}; + const BaseCTA = styled.button` display: inline-flex; align-items: center; @@ -70,5 +75,12 @@ const LoadingWrapper = styled.span` margin-right: ${theme.spacing.spacing2}; `; -export { LoadingWrapper, OutlinedCTA, PrimaryCTA, SecondaryCTA, TextCTA }; +const StyledLoadingSpinner = styled(LoadingSpinner)` + border-top-color: ${({ $variant }) => theme.cta[$variant].text}; + border-right-color: ${({ $variant }) => theme.cta[$variant].text}; + border-bottom-color: ${({ $variant }) => theme.cta[$variant].text}; + border-left-color: transparent; +`; + +export { LoadingWrapper, OutlinedCTA, PrimaryCTA, SecondaryCTA, StyledLoadingSpinner, TextCTA }; export type { StyledCTAProps }; diff --git a/src/component-library/CTA/CTA.tsx b/src/component-library/CTA/CTA.tsx index dfe98e7cf2..5c00938b02 100644 --- a/src/component-library/CTA/CTA.tsx +++ b/src/component-library/CTA/CTA.tsx @@ -4,11 +4,10 @@ import { mergeProps } from '@react-aria/utils'; import { PressEvent } from '@react-types/shared'; import { ButtonHTMLAttributes, forwardRef } from 'react'; -import { LoadingSpinner } from '../LoadingSpinner'; import { useDOMRef } from '../utils/dom'; import { CTASizes } from '../utils/prop-types'; import { BaseCTA, BaseCTAProps } from './BaseCTA'; -import { LoadingWrapper } from './CTA.style'; +import { LoadingWrapper, StyledLoadingSpinner } from './CTA.style'; const loadingSizes: Record = { 'x-small': 14, @@ -54,9 +53,9 @@ const CTA = forwardRef( > {loading && ( - Date: Wed, 28 Jun 2023 12:24:44 +0100 Subject: [PATCH 062/241] feat: replace legacy toast with new notification toast (#1370) --- src/assets/locales/en/translation.json | 8 ++ .../NotificationToast.styles.tsx} | 0 .../NotificationToast/NotificationToast.tsx | 103 ++++++++++++++ .../NotificationToast/TransactionToast.tsx | 84 +++++++++++ src/components/NotificationToast/index.tsx | 4 + .../TransactionModal/TransactionModal.tsx | 4 +- .../TransactionToast/TransactionToast.tsx | 132 ------------------ src/components/TransactionToast/index.tsx | 2 - src/components/index.tsx | 4 +- src/parts/Topbar/index.tsx | 19 ++- src/utils/context/Notifications.tsx | 29 +++- .../use-transaction-notifications.tsx | 4 +- src/utils/hooks/use-sign-message.ts | 16 ++- 13 files changed, 251 insertions(+), 158 deletions(-) rename src/components/{TransactionToast/TransactionToast.styles.tsx => NotificationToast/NotificationToast.styles.tsx} (100%) create mode 100644 src/components/NotificationToast/NotificationToast.tsx create mode 100644 src/components/NotificationToast/TransactionToast.tsx create mode 100644 src/components/NotificationToast/index.tsx delete mode 100644 src/components/TransactionToast/TransactionToast.tsx delete mode 100644 src/components/TransactionToast/index.tsx diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index c508449cb8..02fd2fceef 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -162,6 +162,8 @@ "fee_token": "Fee token", "claim_rewards": "Claim Rewards", "tx_fees": "Tx fees", + "view_subscan": "View Subscan", + "redeem_page": { "maximum_in_single_request": "Max redeemable in single request", "redeem": "Redeem", @@ -739,5 +741,11 @@ "fee_for_transaction_to_be_included_in_the_parachain": "The fee for the transaction to be included in the parachain", "premium_redeem": "Premium Redeem", "premium_redeem_info": "If you select premium redeem, you will try to redeem with a vault that is below the secure collateral threshold. This operation has a higher risk since the vault might be liquidated. For this higher risk, you will receive a compensation." + }, + "notifications": { + "signature_submission_failed": "Signature submission failed", + "signature_submission_successful": "Signature submission successful", + "funding_account_failed": "Funding account failed", + "funding_account_successful": "Funding account successful" } } diff --git a/src/components/TransactionToast/TransactionToast.styles.tsx b/src/components/NotificationToast/NotificationToast.styles.tsx similarity index 100% rename from src/components/TransactionToast/TransactionToast.styles.tsx rename to src/components/NotificationToast/NotificationToast.styles.tsx diff --git a/src/components/NotificationToast/NotificationToast.tsx b/src/components/NotificationToast/NotificationToast.tsx new file mode 100644 index 0000000000..f4a6b8bb64 --- /dev/null +++ b/src/components/NotificationToast/NotificationToast.tsx @@ -0,0 +1,103 @@ +import { useHover } from '@react-aria/interactions'; +import { mergeProps } from '@react-aria/utils'; +import { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { CheckCircle, XCircle } from '@/assets/icons'; +import { CTA, Divider, Flex, FlexProps, LoadingSpinner, P } from '@/component-library'; +import { useCountdown } from '@/utils/hooks/use-countdown'; + +import { StyledProgressBar, StyledWrapper } from './NotificationToast.styles'; + +type NotificationToastVariant = 'success' | 'error' | 'loading'; + +const loadingSpinner = ; + +const getIcon = (variant: NotificationToastVariant) => + ({ + loading: loadingSpinner, + success: , + error: + }[variant]); + +type Props = { + title?: ReactNode; + variant?: NotificationToastVariant; + description?: string; + timeout?: number; + action?: ReactNode; + onDismiss?: () => void; +}; + +type InheritAttrs = Omit; + +type NotificationToastProps = Props & InheritAttrs; + +const NotificationToast = ({ + variant = 'success', + title, + timeout = 8000, + description, + onDismiss, + action, + ...props +}: NotificationToastProps): JSX.Element => { + const { t } = useTranslation(); + + const showCountdown = variant === 'success' || variant === 'error'; + + const { value: countdown, start, stop } = useCountdown({ + timeout, + disabled: !showCountdown, + onEndCountdown: onDismiss + }); + + const { hoverProps } = useHover({ + onHoverStart: stop, + onHoverEnd: start, + isDisabled: !showCountdown + }); + + const icon = getIcon(variant); + + return ( + + + + {icon} + + +

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+
+ {showCountdown && ( + + )} + + {action && ( + <> + {action} + + + )} + + {t('dismiss')} + + +
+ ); +}; + +export { NotificationToast }; +export type { NotificationToastProps, NotificationToastVariant }; diff --git a/src/components/NotificationToast/TransactionToast.tsx b/src/components/NotificationToast/TransactionToast.tsx new file mode 100644 index 0000000000..9c0152a319 --- /dev/null +++ b/src/components/NotificationToast/TransactionToast.tsx @@ -0,0 +1,84 @@ +import { TFunction } from 'i18next'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; + +import { updateTransactionModal } from '@/common/actions/general.actions'; +import { CTA, CTALink } from '@/component-library'; +import { TransactionStatus } from '@/utils/hooks/transaction/types'; + +import { NotificationToast, NotificationToastProps, NotificationToastVariant } from './NotificationToast'; + +const getData = (t: TFunction, variant: TransactionStatus) => + (({ + [TransactionStatus.CONFIRM]: { + title: t('transaction.confirm_transaction'), + status: 'loading' + }, + [TransactionStatus.SUBMITTING]: { + title: t('transaction.transaction_processing'), + status: 'loading' + }, + [TransactionStatus.SUCCESS]: { + title: t('transaction.transaction_successful'), + status: 'success' + }, + [TransactionStatus.ERROR]: { + title: t('transaction.transaction_failed'), + status: 'error' + } + } as Record)[variant]); + +type Props = { + variant?: TransactionStatus; + url?: string; + errorMessage?: string; +}; + +type InheritAttrs = Omit; + +type TransactionToastProps = Props & InheritAttrs; + +const TransactionToast = ({ + variant = TransactionStatus.SUCCESS, + url, + description, + errorMessage, + onDismiss, + ...props +}: TransactionToastProps): JSX.Element => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + const handleViewDetails = () => { + dispatch(updateTransactionModal(true, { variant: TransactionStatus.ERROR, description, errorMessage })); + onDismiss?.(); + }; + + const { title, status } = getData(t, variant); + + const action = url ? ( + + {t('view_subscan')} + + ) : ( + errorMessage && ( + + View Details + + ) + ); + + return ( + + ); +}; + +export { TransactionToast }; +export type { TransactionToastProps }; diff --git a/src/components/NotificationToast/index.tsx b/src/components/NotificationToast/index.tsx new file mode 100644 index 0000000000..4d0a75a2a1 --- /dev/null +++ b/src/components/NotificationToast/index.tsx @@ -0,0 +1,4 @@ +export type { NotificationToastProps } from './NotificationToast'; +export { NotificationToast } from './NotificationToast'; +export type { TransactionToastProps } from './TransactionToast'; +export { TransactionToast } from './TransactionToast'; diff --git a/src/components/TransactionModal/TransactionModal.tsx b/src/components/TransactionModal/TransactionModal.tsx index 6ab35ada22..2ae9d82102 100644 --- a/src/components/TransactionModal/TransactionModal.tsx +++ b/src/components/TransactionModal/TransactionModal.tsx @@ -17,7 +17,7 @@ import { P, TextLink } from '@/component-library'; -import { NotificationToast, useNotifications } from '@/utils/context/Notifications'; +import { NotificationToastType, useNotifications } from '@/utils/context/Notifications'; import { TransactionStatus } from '@/utils/hooks/transaction/types'; import { StyledCard, StyledCheckCircle, StyledXCircle } from './TransactionModal.style'; @@ -63,7 +63,7 @@ const TransactionModal = (): JSX.Element => { // 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, + type: NotificationToastType.TRANSACTION, props: { variant: variant, url, description } }); } diff --git a/src/components/TransactionToast/TransactionToast.tsx b/src/components/TransactionToast/TransactionToast.tsx deleted file mode 100644 index fc413baba1..0000000000 --- a/src/components/TransactionToast/TransactionToast.tsx +++ /dev/null @@ -1,132 +0,0 @@ -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 deleted file mode 100644 index 36ce2db462..0000000000 --- a/src/components/TransactionToast/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export type { TransactionToastProps } from './TransactionToast'; -export { TransactionToast } from './TransactionToast'; diff --git a/src/components/index.tsx b/src/components/index.tsx index fbd8dc51df..44534b7e16 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -12,6 +12,8 @@ export type { LoanPositionsTableProps } from './LoanPositionsTable'; export { LoanPositionsTable } from './LoanPositionsTable'; export type { NotificationsPopoverProps } from './NotificationsPopover'; export { NotificationsPopover } from './NotificationsPopover'; +export type { NotificationToastProps, TransactionToastProps } from './NotificationToast'; +export { NotificationToast, TransactionToast } from './NotificationToast'; export type { PlusDividerProps } from './PlusDivider'; export { PlusDivider } from './PlusDivider'; export type { PoolsTableProps } from './PoolsTable'; @@ -22,5 +24,3 @@ export { ToastContainer } from './ToastContainer'; export * from './TransactionDetails'; export type { TransactionFeeDetailsProps } from './TransactionFeeDetails'; export { TransactionFeeDetails } from './TransactionFeeDetails'; -export type { TransactionToastProps } from './TransactionToast'; -export { TransactionToast } from './TransactionToast'; diff --git a/src/parts/Topbar/index.tsx b/src/parts/Topbar/index.tsx index 384b9b50d3..0326055ca6 100644 --- a/src/parts/Topbar/index.tsx +++ b/src/parts/Topbar/index.tsx @@ -5,7 +5,6 @@ import clsx from 'clsx'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; -import { toast } from 'react-toastify'; import { showAccountModalAction, showSignTermsModalAction } from '@/common/actions/general.actions'; import { StoreType } from '@/common/types/util.types'; @@ -22,7 +21,7 @@ import InterlayLink from '@/legacy-components/UI/InterlayLink'; import { KeyringPair, useSubstrate, useSubstrateSecureState } from '@/lib/substrate'; import { BitcoinNetwork } from '@/types/bitcoin'; import { POLKADOT } from '@/utils/constants/relay-chain-names'; -import { useNotifications } from '@/utils/context/Notifications'; +import { NotificationToastType, 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'; @@ -40,7 +39,7 @@ const Topbar = (): JSX.Element => { const isBanxaEnabled = useFeatureFlag(FeatureFlags.BANXA); const { setSelectedAccount, removeSelectedAccount } = useSubstrate(); const { selectProps } = useSignMessage(); - const { list } = useNotifications(); + const notifications = useNotifications(); const governanceTokenBalanceIsZero = getAvailableBalance(GOVERNANCE_TOKEN.ticker)?.isZero(); @@ -50,10 +49,16 @@ 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.'); + + notifications.show('faucet', { + type: NotificationToastType.STANDARD, + props: { variant: 'success', title: t('notifications.funding_account_successful') } + }); } catch (error) { - toast.error(`Funding failed. ${error.message}`); + notifications.show('faucet', { + type: NotificationToastType.STANDARD, + props: { variant: 'error', title: t('notifications.funding_account_failed') } + }); } }; @@ -143,7 +148,7 @@ const Topbar = (): JSX.Element => { 'bg-white': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT })} > - + {accountLabel} diff --git a/src/utils/context/Notifications.tsx b/src/utils/context/Notifications.tsx index 3dd7f48752..e12fec9967 100644 --- a/src/utils/context/Notifications.tsx +++ b/src/utils/context/Notifications.tsx @@ -1,25 +1,32 @@ import { Overlay } from '@react-aria/overlays'; import { mergeProps } from '@react-aria/utils'; -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useMemo, 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 { + NotificationToast, + NotificationToastProps, + 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 { +enum NotificationToastType { + STANDARD, TRANSACTION } -type NotificationToastAction = { type: NotificationToast.TRANSACTION; props: TransactionToastProps }; - -const toastComponentMap = { [NotificationToast.TRANSACTION]: TransactionToast }; +type NotificationToastAction = + | { type: NotificationToastType.TRANSACTION; props: TransactionToastProps } + | { type: NotificationToastType.STANDARD; props: NotificationToastProps }; type ToastMap = Record; @@ -63,6 +70,14 @@ const useNotifications = (): NotificationsConfig => React.useContext(Notificatio const NotificationsProvider: React.FC = ({ children }) => { const toastContainerRef = useRef(null); + const toastComponentMap = useMemo( + () => ({ + [NotificationToastType.STANDARD]: NotificationToast, + [NotificationToastType.TRANSACTION]: TransactionToast + }), + [] + ); + const dispatch = useDispatch(); const { account } = useWallet(); @@ -137,5 +152,5 @@ const NotificationsProvider: React.FC = ({ children }) => { ); }; -export { NotificationsContext, NotificationsProvider, NotificationToast, useNotifications }; +export { NotificationsContext, NotificationsProvider, NotificationToastType, useNotifications }; export type { NotificationToastAction }; diff --git a/src/utils/hooks/transaction/use-transaction-notifications.tsx b/src/utils/hooks/transaction/use-transaction-notifications.tsx index abcb7fda2e..9db3dfa033 100644 --- a/src/utils/hooks/transaction/use-transaction-notifications.tsx +++ b/src/utils/hooks/transaction/use-transaction-notifications.tsx @@ -5,7 +5,7 @@ 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 { NotificationToastAction, NotificationToastType, useNotifications } from '@/utils/context/Notifications'; import { TransactionActions, TransactionStatus } from './types'; import { TransactionResult } from './use-transaction'; @@ -60,7 +60,7 @@ const useTransactionNotifications = ({ // creating or updating notification if (toastInfo.isOnScreen) { const toastAction: NotificationToastAction = { - type: NotificationToast.TRANSACTION, + type: NotificationToastType.TRANSACTION, props: { variant: status, url, diff --git a/src/utils/hooks/use-sign-message.ts b/src/utils/hooks/use-sign-message.ts index ef57dbdfd0..b9fc582ea9 100644 --- a/src/utils/hooks/use-sign-message.ts +++ b/src/utils/hooks/use-sign-message.ts @@ -1,14 +1,15 @@ import { PressEvent } from '@react-types/shared'; import { useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { useMutation, useQuery, useQueryClient } from 'react-query'; import { useDispatch } from 'react-redux'; -import { toast } from 'react-toastify'; import { showSignTermsModalAction } from '@/common/actions/general.actions'; import { TERMS_AND_CONDITIONS_LINK } from '@/config/relay-chains'; import { SIGNER_API_URL } from '@/constants'; import { KeyringPair, useSubstrateSecureState } from '@/lib/substrate'; +import { NotificationToastType, useNotifications } from '../context/Notifications'; import { signMessage } from '../helpers/wallet'; import { LocalStorageKey, TCSignaturesData, useLocalStorage } from './use-local-storage'; @@ -50,7 +51,9 @@ type UseSignMessageResult = { }; const useSignMessage = (): UseSignMessageResult => { + const { t } = useTranslation(); const queryClient = useQueryClient(); + const notifications = useNotifications(); const dispatch = useDispatch(); const [signatures, setSignatures] = useLocalStorage(LocalStorageKey.TC_SIGNATURES); @@ -96,17 +99,22 @@ const useSignMessage = (): UseSignMessageResult => { queryFn: () => selectedAccount && getSignature(selectedAccount) }); - // TODO: add new notification const signMessageMutation = useMutation((account: KeyringPair) => postSignature(account), { onError: (_, variables) => { setSignature(variables.address, false); - toast.error('Something went wrong!'); + notifications.show(variables.address, { + type: NotificationToastType.STANDARD, + props: { variant: 'error', title: t('notifications.signature_submission_failed') } + }); }, onSuccess: (_, variables) => { setSignature(variables.address, true); dispatch(showSignTermsModalAction(false)); refetchSignatureData(); - toast.success('Your signature was submitted successfully.'); + notifications.show(variables.address, { + type: NotificationToastType.STANDARD, + props: { variant: 'success', title: t('notifications.signature_submission_successful') } + }); } }); From 06b0ca4680590fd198a1d0e6b384658be64d59f7 Mon Sep 17 00:00:00 2001 From: tomjeatt <40243778+tomjeatt@users.noreply.github.com> Date: Wed, 28 Jun 2023 14:06:41 +0100 Subject: [PATCH 063/241] fix: UI styling bugs (#1371) * fix: change broken gradient id ref * refactor: add opacity value to navigation separator * fix: update padding * fix: border opacity * fix: use transaction details component * refactor: change how padding is set --- src/assets/img/exchanges/acala-logo.svg | 2 +- src/component-library/Select/Select.style.tsx | 3 +- .../InterlayDefaultContainedButton/index.tsx | 2 +- .../CrossChainTransferForm.styles.tsx | 11 +---- .../CrossChainTransferForm.tsx | 44 ++++++++++--------- .../SidebarContent/Navigation/index.tsx | 2 +- 6 files changed, 29 insertions(+), 35 deletions(-) diff --git a/src/assets/img/exchanges/acala-logo.svg b/src/assets/img/exchanges/acala-logo.svg index 131dd650d9..1f28383591 100644 --- a/src/assets/img/exchanges/acala-logo.svg +++ b/src/assets/img/exchanges/acala-logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/component-library/Select/Select.style.tsx b/src/component-library/Select/Select.style.tsx index 0a19892dfe..a3bce660c4 100644 --- a/src/component-library/Select/Select.style.tsx +++ b/src/component-library/Select/Select.style.tsx @@ -68,8 +68,7 @@ const StyledTriggerValue = styled(Span)` const StyledList = styled(List)` overflow: auto; - padding: 0 ${theme.dialog.medium.body.paddingX} ${theme.dialog.medium.body.paddingY} - ${theme.dialog.medium.body.paddingX}; + padding: 0 ${theme.dialog.medium.body.paddingX} ${theme.dialog.medium.body.paddingX}; `; const StyledChevronDown = styled(ChevronDown)` diff --git a/src/legacy-components/buttons/InterlayDefaultContainedButton/index.tsx b/src/legacy-components/buttons/InterlayDefaultContainedButton/index.tsx index 099a44a7b4..b68a689d7c 100644 --- a/src/legacy-components/buttons/InterlayDefaultContainedButton/index.tsx +++ b/src/legacy-components/buttons/InterlayDefaultContainedButton/index.tsx @@ -29,7 +29,7 @@ const InterlayDefaultContainedButton = React.forwardRef( 'border', 'border-black', - 'border-opacity-25', + 'border-opacity-10', 'font-medium', disabledOrPending diff --git a/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/CrossChainTransferForm.styles.tsx b/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/CrossChainTransferForm.styles.tsx index 4868a48df7..ecc57c9eb0 100644 --- a/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/CrossChainTransferForm.styles.tsx +++ b/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/CrossChainTransferForm.styles.tsx @@ -1,17 +1,10 @@ import styled from 'styled-components'; import { ArrowRightCircle } from '@/assets/icons'; -import { Dl, Flex, theme } from '@/component-library'; +import { Flex, theme } from '@/component-library'; import { ChainSelect } from '../ChainSelect'; -const StyledDl = styled(Dl)` - background-color: ${theme.card.bg.secondary}; - padding: ${theme.spacing.spacing4}; - font-size: ${theme.text.xs}; - border-radius: ${theme.rounded.rg}; -`; - const StyledArrowRightCircle = styled(ArrowRightCircle)` transform: rotate(90deg); align-self: center; @@ -39,4 +32,4 @@ const StyledSourceChainSelect = styled(ChainSelect)` } `; -export { ChainSelectSection, StyledArrowRightCircle, StyledDl, StyledSourceChainSelect }; +export { ChainSelectSection, StyledArrowRightCircle, StyledSourceChainSelect }; diff --git a/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/CrossChainTransferForm.tsx b/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/CrossChainTransferForm.tsx index eea939163f..0205b1c07a 100644 --- a/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/CrossChainTransferForm.tsx +++ b/src/pages/Transfer/TransferForms/components/CrossChainTransferForm/CrossChainTransferForm.tsx @@ -6,8 +6,15 @@ import { ChangeEventHandler, Key, useCallback, useEffect, useState } from 'react import { useTranslation } from 'react-i18next'; import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/common/utils/utils'; -import { Dd, DlGroup, Dt, Flex, LoadingSpinner, TokenInput } from '@/component-library'; -import { AccountSelect, AuthCTA } from '@/components'; +import { Flex, LoadingSpinner, TokenInput } from '@/component-library'; +import { + AccountSelect, + AuthCTA, + TransactionDetails, + TransactionDetailsDd, + TransactionDetailsDt, + TransactionDetailsGroup +} from '@/components'; import { CROSS_CHAIN_TRANSFER_AMOUNT_FIELD, CROSS_CHAIN_TRANSFER_FROM_FIELD, @@ -30,12 +37,7 @@ import { Transaction, useTransaction } from '@/utils/hooks/transaction'; import useAccountId from '@/utils/hooks/use-account-id'; import { ChainSelect } from '../ChainSelect'; -import { - ChainSelectSection, - StyledArrowRightCircle, - StyledDl, - StyledSourceChainSelect -} from './CrossChainTransferForm.styles'; +import { ChainSelectSection, StyledArrowRightCircle, StyledSourceChainSelect } from './CrossChainTransferForm.styles'; const CrossChainTransferForm = (): JSX.Element => { const [destinationChains, setDestinationChains] = useState([]); @@ -249,20 +251,20 @@ const CrossChainTransferForm = (): JSX.Element => { onChange: handleDestinationAccountChange })} /> - - -
+ + + Origin chain transfer fee -
-
{currentToken?.originFee}
-
- -
- Destination chain transfer fee estimate -
-
{`${currentToken?.destFee.toString()} ${currentToken?.value}`}
-
-
+ + {currentToken?.originFee} + + + Destination chain transfer fee estimate + {`${currentToken?.destFee.toString()} ${ + currentToken?.value + }`} + + {isCTADisabled ? 'Enter transfer amount' : t('transfer')} diff --git a/src/parts/Sidebar/SidebarContent/Navigation/index.tsx b/src/parts/Sidebar/SidebarContent/Navigation/index.tsx index 790ee68503..ae6f43164b 100644 --- a/src/parts/Sidebar/SidebarContent/Navigation/index.tsx +++ b/src/parts/Sidebar/SidebarContent/Navigation/index.tsx @@ -198,7 +198,7 @@ const Navigation = ({