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/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__": diff --git a/package.json b/package.json index a7a04ec4d7..735d72e184 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,17 @@ { "name": "interbtc-ui", - "version": "2.31.3", + "version": "2.32.4", "private": true, "dependencies": { "@craco/craco": "^6.1.1", "@headlessui/react": "^1.1.1", - "@heroicons/react": "^2.0.0", - "@interlay/bridge": "^0.2.9", - "@interlay/interbtc-api": "2.2.2", - "@interlay/monetary-js": "0.7.2", + "@heroicons/react": "^2.0.18", + "@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", + "@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/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/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..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", @@ -602,6 +603,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/Geoblock/Geoblock.tsx b/src/components/Geoblock/Geoblock.tsx new file mode 100644 index 0000000000..0a308d5619 --- /dev/null +++ b/src/components/Geoblock/Geoblock.tsx @@ -0,0 +1,14 @@ +import { 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/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/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/config/relay-chains.tsx b/src/config/relay-chains.tsx index de17408744..de9302d73f 100644 --- a/src/config/relay-chains.tsx +++ b/src/config/relay-chains.tsx @@ -1,5 +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,8 +160,11 @@ switch (process.env.REACT_APP_RELAY_CHAIN_NAME) { // TODO: temporary TRANSACTION_FEE_AMOUNT = newMonetaryAmount(0.2, GOVERNANCE_TOKEN, true); XCM_ADAPTERS = { - hydra: new HydraAdapter(), interlay: new InterlayAdapter(), + acala: new AcalaAdapter(), + astar: new AstarAdapter(), + hydra: new HydraAdapter(), + parallel: new ParallelAdapter(), polkadot: new PolkadotAdapter(), statemint: new StatemintAdapter() }; @@ -201,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/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/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/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/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/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/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/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/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..1e7a864185 --- /dev/null +++ b/src/pages/Transfer/CrossChainTransferForm/CrossChainTransferForm.tsx @@ -0,0 +1,301 @@ +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]); + + // 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 ( + + + + ); + } + + 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 52bfdb6ad9..dd99e9e509 100644 --- a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/ChainIcon.tsx +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/ChainIcon.tsx @@ -3,15 +3,35 @@ import { forwardRef, ForwardRefExoticComponent, RefAttributes } from 'react'; import { IconProps } from '@/component-library/Icon'; import { StyledFallbackIcon } from './ChainIcon.style'; -import { HYDRA, INTERLAY, 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, INTERLAY, + 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/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/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/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 14d6a6c839..d3c471eb7a 100644 --- a/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/index.ts +++ b/src/pages/Transfer/CrossChainTransferForm/components/ChainIcon/icons/index.ts @@ -1,7 +1,13 @@ +export { ACALA } from './Acala'; +export { ASTAR } from './Astar'; +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 { PARALLEL } from './Parallel'; export { POLKADOT } from './Polkadot'; export { STATEMINE } from './Statemine'; export { STATEMINT } from './Statemint'; 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/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/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/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/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/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/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/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/api/xcm/use-xcm-bridge.ts b/src/utils/hooks/api/xcm/use-xcm-bridge.ts index 4b086c55c8..491a8931d3 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.isLessThan(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..fe751d615b --- /dev/null +++ b/src/utils/hooks/api/xcm/xcm-endpoints.ts @@ -0,0 +1,28 @@ +import { ChainName } from '@interlay/bridge'; + +type XCMEndpointsRecord = Record; + +const XCMEndpoints: XCMEndpointsRecord = { + 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'], + 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/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 }; diff --git a/src/utils/hooks/use-feature-flag.ts b/src/utils/hooks/use-feature-flag.ts index 9b5676a053..917bd3b3c8 100644 --- a/src/utils/hooks/use-feature-flag.ts +++ b/src/utils/hooks/use-feature-flag.ts @@ -2,14 +2,18 @@ enum FeatureFlags { LENDING = 'lending', AMM = 'amm', WALLET = 'wallet', - BANXA = 'banxa' + BANXA = 'banxa', + EARN_STRATEGIES = 'earn-strategies', + GEOBLOCK = 'geoblock' } 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, + [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 new file mode 100644 index 0000000000..09805c8398 --- /dev/null +++ b/src/utils/hooks/use-geoblocking.ts @@ -0,0 +1,27 @@ +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); + 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(); + }, [isGeoblockEnabled]); +}; + +export { useGeoblocking }; diff --git a/yarn.lock b/yarn.lock index 55a00193f5..269fc4fdba 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" @@ -2027,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" @@ -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.9": - version "0.2.9" - resolved "https://registry.yarnpkg.com/@interlay/bridge/-/bridge-0.2.9.tgz#3b3785ec4752c0c2f0cedaf9a6b62ef3f4235778" - integrity sha512-J2yJNK2yCeUP5dMDgqGsMckRzmh1qM4ge/CGBK5AYHUHzEKr2KcCMY7UrKCaSXzRYMVdLc+f8hd+FeMHCznffg== - 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.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" + "@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" @@ -2073,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.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" - "@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" @@ -2090,20 +2137,20 @@ 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" - 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" @@ -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==