diff --git a/.env.dev b/.env.dev index 883d93374a..4508afb933 100644 --- a/.env.dev +++ b/.env.dev @@ -14,6 +14,9 @@ REACT_APP_FAUCET_URL="http://localhost:3035" REACT_APP_RELAY_CHAIN_NAME="kusama" DOCKER_RELAY_CHAIN_CURRENCY="KSM" REACT_APP_MARKET_DATA_URL="https://api.coingecko.com/api/v3/simple/price?vs_currencies=usd" +REACT_APP_SITE_INFORMATION_MESSAGE="This is an informational message that will be shown on every page of the application." +REACT_APP_SITE_INFORMATION_LINK="https://gobob.xyz" + // Kintsugi testnet diff --git a/.github/ISSUE_TEMPLATE/roadmap.md b/.github/ISSUE_TEMPLATE/roadmap.md new file mode 100644 index 0000000000..b55df6d288 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/roadmap.md @@ -0,0 +1,49 @@ +--- +name: Roadmap +about: Add a new item to the roadmap +title: '' +labels: 'roadmap' +assignees: '' + +--- + +# Abstract + +[ADD HERE] + +A 2-3 sentence description of the proposal. + +# Motivation + +[ADD HERE] + +Provide a motivation for including this proposal. + +# Specification + +[ADD HERE] + +Provide a high-level, functional specification. The specification should be focussed on what the proposal is trying to achieve. + +## [OPTIONAL]Extensions + +[OPTIONAL][ADD HERE] + +### [OPTIONAL][TITLE] + +[OPTIONAL][ADD HERE] + +# Reference Implementation + +[OPTIONAL][ADD HERE] + +Provide a reference implementation, proof of concept, or basic pseudo-code for a better grounds to discuss the suggested implementation. The reference implementation focusses on the how. + +# Security Considerations + +[ADD HERE] + +Provide reasoning around security implications: + +- The proposal itself +- The propsals implications to the existing system diff --git a/.github/workflows/projects.yml b/.github/workflows/projects.yml index 163059d1ba..7883a3c60f 100644 --- a/.github/workflows/projects.yml +++ b/.github/workflows/projects.yml @@ -7,10 +7,18 @@ on: jobs: add-to-project: - name: Add issue and pull request to project + name: Add issue and pull request to backlog runs-on: ubuntu-latest steps: - uses: actions/add-to-project@v0.4.0 with: project-url: https://github.com/orgs/interlay/projects/3 github-token: ${{ secrets.PROJECTS }} + label: roadmap + label-operator: NOT + - uses: actions/add-to-project@v0.4.0 + with: + project-url: https://github.com/orgs/interlay/projects/4 + github-token: ${{ secrets.PROJECTS }} + label: roadmap + label-operator: AND diff --git a/api/market_data.py b/api/market_data.py index 42e5b9698f..6c2c7eeae3 100644 --- a/api/market_data.py +++ b/api/market_data.py @@ -23,7 +23,12 @@ "kintsugi": "/Kintsugi/Token:KINT", "acala-dollar": "/Acala/Token:AUSD", "karura": "/Bifrost/518", - "tether": "/Ethereum/0xdAC17F958D2ee523a2206206994597C13D831ec7" + "tether": "/Ethereum/0xdAC17F958D2ee523a2206206994597C13D831ec7", + "voucher-dot": "/Bifrost-polkadot/2304", + "binancecoin": "/Ethereum/0xB8c77482e45F1F44dE1745F52C74426C631bDD52", + "bnb": "/Ethereum/0xB8c77482e45F1F44dE1745F52C74426C631bDD52", + "tbtc": "/Ethereum/0x18084fbA666a33d37592fA2633fD49a74DD93a88", + "dai": "/Ethereum/0x6B175474E89094C44Da98b954EedeAC495271d0F", } @app.after_request diff --git a/package.json b/package.json index 543bb257f7..4bb28ed380 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "interbtc-ui", - "version": "2.37.0", + "version": "2.38.1", "private": true, "dependencies": { "@craco/craco": "^6.1.1", "@headlessui/react": "^1.1.1", "@heroicons/react": "^2.0.18", "@interlay/bridge": "^0.3.13", - "@interlay/interbtc-api": "2.3.7", + "@interlay/interbtc-api": "2.4.3", "@interlay/monetary-js": "0.7.3", "@polkadot/api": "9.14.2", "@polkadot/extension-dapp": "0.44.1", @@ -15,6 +15,7 @@ "@polkadot/ui-keyring": "^2.9.7", "@reach/tooltip": "^0.16.0", "@react-aria/accordion": "^3.0.0-alpha.14", + "@react-aria/breadcrumbs": "^3.5.3", "@react-aria/button": "^3.6.4", "@react-aria/dialog": "^3.3.1", "@react-aria/focus": "^3.6.1", @@ -157,7 +158,7 @@ "@polkadot/util-crypto": "^10.2.4" }, "scripts": { - "start": "craco start", + "start": "NODE_OPTIONS=--openssl-legacy-provider craco start", "start-regtest": "cross-env REACT_APP_BITCOIN_NETWORK=regtest yarn start", "start-testnet": "cross-env REACT_APP_BITCOIN_NETWORK=testnet yarn start", "generate:defs": "ts-node --skip-project node_modules/.bin/polkadot-types-from-defs --package sample-polkadotjs-typegen/interfaces --input ./src/interfaces", @@ -167,7 +168,7 @@ "type-check": "tsc", "format": "yarn prettier --write src", "setup": "yarn generate:defs && yarn generate:meta", - "build": "REACT_APP_VERSION=$npm_package_version craco build", + "build": "NODE_OPTIONS=--openssl-legacy-provider REACT_APP_VERSION=$npm_package_version craco build", "build-with-webpack-bundle-analysis": "yarn build --stats && webpack-bundle-analyzer build/bundle-stats.json -m static -r build/bundle-stats.html -O", "lint-and-type-check": "yarn lint && yarn type-check", "eject": "react-scripts eject", diff --git a/src/App.tsx b/src/App.tsx index 461c4d81f1..84a8e033ab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,22 +5,20 @@ import * as React from 'react'; import { useErrorHandler, withErrorBoundary } from 'react-error-boundary'; import { useQuery } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; -import { Route, Switch } from 'react-router-dom'; +import { Redirect, Route, Switch } from 'react-router-dom'; import { isVaultClientLoaded } from '@/common/actions/general.actions'; import { StoreType } from '@/common/types/util.types'; +import { Layout, TransactionModal } from '@/components'; import ErrorFallback from '@/legacy-components/ErrorFallback'; import FullLoadingSpinner from '@/legacy-components/FullLoadingSpinner'; import { useSubstrate, useSubstrateSecureState } from '@/lib/substrate'; import graphqlFetcher, { GRAPHQL_FETCHER, GraphqlReturn } from '@/services/fetchers/graphql-fetcher'; import vaultsByAccountIdQuery from '@/services/queries/vaults-by-accountId-query'; -import { BitcoinNetwork } from '@/types/bitcoin'; import { PAGES } from '@/utils/constants/links'; -import { Layout, TransactionModal } from './components'; import * as constants from './constants'; import { FeatureFlags, useFeatureFlag } from './hooks/use-feature-flag'; -import TestnetBanner from './legacy-components/TestnetBanner'; const BTC = React.lazy(() => import(/* webpackChunkName: 'btc' */ '@/pages/BTC')); const Strategies = React.lazy(() => import(/* webpackChunkName: 'strategies' */ '@/pages/Strategies')); @@ -80,7 +78,6 @@ const App = (): JSX.Element => { return ( - {process.env.REACT_APP_BITCOIN_NETWORK === BitcoinNetwork.Testnet && } ( }> @@ -119,7 +116,7 @@ const App = (): JSX.Element => { - + {isStrategiesEnabled && ( @@ -140,6 +137,9 @@ const App = (): JSX.Element => { + + + diff --git a/src/assets/icons/ChevronRight.tsx b/src/assets/icons/ChevronRight.tsx new file mode 100644 index 0000000000..d98eac583a --- /dev/null +++ b/src/assets/icons/ChevronRight.tsx @@ -0,0 +1,21 @@ +import { forwardRef } from 'react'; + +import { Icon, IconProps } from '@/component-library/Icon'; + +const ChevronRight = forwardRef((props, ref) => ( + + + +)); + +ChevronRight.displayName = 'ChevronRight'; + +export { ChevronRight }; diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index 5ad0ea092d..94ef00075d 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -5,6 +5,7 @@ export { ArrowsUpDown } from './ArrowsUpDown'; export { ArrowTopRightOnSquare } from './ArrowTopRightOnSquare'; export { CheckCircle } from './CheckCircle'; export { ChevronDown } from './ChevronDown'; +export { ChevronRight } from './ChevronRight'; export { Cog } from './Cog'; export { DocumentDuplicate } from './DocumentDuplicate'; export { InformationCircle } from './InformationCircle'; diff --git a/src/assets/locales/en/translation.json b/src/assets/locales/en/translation.json index 9d245c5770..d20252b086 100644 --- a/src/assets/locales/en/translation.json +++ b/src/assets/locales/en/translation.json @@ -667,7 +667,8 @@ }, "strategy": { "withdraw_rewards_in_wrapped": "Withdraw rewards in {{wrappedCurrencySymbol}}:", - "update_position": "Update position" + "update_position": "Update position", + "initialize": "Initialize strategy" }, "transaction": { "recent_transactions": "Recent transactions", @@ -779,6 +780,9 @@ "low_risk_approach_generate_passive_income": "Discover a straightforward and low-risk approach to generate passive income. This strategy lends out deposited {{ticker}} to borrowers, allowing you to earn interest effortlessly", "how_does_it_work": "How does it work?", "what_are_the_risk": "What are the risks?", - "discover_fundamental_origins": "Discover the fundamental origins of the position, potential risks involved, the allocation of your capital, and other pertinent details in the docs section." + "discover_fundamental_origins": "Discover the fundamental origins of the position, potential risks involved, the allocation of your capital, and other pertinent details in the docs section.", + "proxy_deposit": "{{currency}} Proxy Deposit", + "proxy_deposit_tooltip": "This amount will be locked while the strategy is active. When you fully exit strategy deposit will be returned.", + "proxy_deposit_insufficient_funds": "Insufficient funds: 26 {{currency}} is required for proxy deposit locking." } } diff --git a/src/common/utils/utils.ts b/src/common/utils/utils.ts index 37bd09e264..693636ce3f 100644 --- a/src/common/utils/utils.ts +++ b/src/common/utils/utils.ts @@ -78,7 +78,8 @@ const formatUSD = (amount: number, options?: { compact?: boolean }): string => { const { format } = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', - notation: options?.compact ? getFormatUSDNotation(amount) : undefined + notation: options?.compact ? getFormatUSDNotation(amount) : undefined, + minimumFractionDigits: amount > 0 && amount < 0.01 ? 3 : undefined }); return format(amount); diff --git a/src/component-library/Alert/Alert.style.tsx b/src/component-library/Alert/Alert.style.tsx index 01f03865af..2940f4b8b6 100644 --- a/src/component-library/Alert/Alert.style.tsx +++ b/src/component-library/Alert/Alert.style.tsx @@ -12,14 +12,14 @@ interface StyledAlertProps { const StyledAlert = styled(Flex)` padding: ${theme.spacing.spacing2}; - color: ${({ $status }) => theme.alert.status[$status]}; border: 1px solid ${({ $status }) => theme.alert.status[$status]}; background-color: ${({ $status }) => theme.alert.bg[$status]}; border-radius: ${theme.rounded.md}; font-size: ${theme.text.xs}; `; -const StyledWarningIcon = styled(WarningIcon)` +const StyledWarningIcon = styled(WarningIcon)` + color: ${({ $status }) => theme.alert.status[$status]}; width: ${theme.spacing.spacing5}; height: ${theme.spacing.spacing5}; flex-shrink: 0; diff --git a/src/component-library/Alert/Alert.tsx b/src/component-library/Alert/Alert.tsx index 2b58cfecf6..aac2b7c679 100644 --- a/src/component-library/Alert/Alert.tsx +++ b/src/component-library/Alert/Alert.tsx @@ -12,7 +12,7 @@ type AlertProps = Props & InheritAttrs; const Alert = ({ status = 'success', children, ...props }: AlertProps): JSX.Element => ( - +
{children}
); diff --git a/src/component-library/Breadcrumbs/BreadcrumbItem.tsx b/src/component-library/Breadcrumbs/BreadcrumbItem.tsx new file mode 100644 index 0000000000..e8884970bb --- /dev/null +++ b/src/component-library/Breadcrumbs/BreadcrumbItem.tsx @@ -0,0 +1,56 @@ +import { AriaBreadcrumbsProps, useBreadcrumbItem } from '@react-aria/breadcrumbs'; +import { mergeProps } from '@react-aria/utils'; +import { AnchorHTMLAttributes, Fragment, useRef } from 'react'; + +import { ChevronRight } from '@/assets/icons'; + +import { TextLinkProps } from '../TextLink'; +import { StyledLinkBreadcrumb, StyledSpanBreadcrumb } from './Breadcrumbs.style'; + +type Props = { + isDisabled?: boolean; + isCurrent: boolean; + to: TextLinkProps['to']; +}; + +type InheritAttrs = Omit; + +type NativeAttrs = Omit, keyof (Props & InheritAttrs)>; + +type BreadcrumbItemProps = Props & NativeAttrs & InheritAttrs; + +const BreadcrumbItem = ({ children, isDisabled, isCurrent, to, ...props }: BreadcrumbItemProps): JSX.Element => { + const ref = useRef(null); + const { itemProps } = useBreadcrumbItem( + { + ...props, + children, + isDisabled: isCurrent, + elementType: isCurrent ? 'span' : 'a' + }, + ref + ); + + const commonProps: Pick = { + size: 's', + color: isCurrent ? 'secondary' : 'tertiary' + }; + + return ( + + {isCurrent ? ( + + {children} + + ) : ( + + {children} + + )} + {isCurrent === false && } + + ); +}; + +export { BreadcrumbItem }; +export type { BreadcrumbItemProps }; diff --git a/src/component-library/Breadcrumbs/Breadcrumbs.stories.tsx b/src/component-library/Breadcrumbs/Breadcrumbs.stories.tsx new file mode 100644 index 0000000000..8cdd9ca6cc --- /dev/null +++ b/src/component-library/Breadcrumbs/Breadcrumbs.stories.tsx @@ -0,0 +1,20 @@ +import { Meta, Story } from '@storybook/react'; + +import { BreadcrumbItem, Breadcrumbs, BreadcrumbsProps } from '.'; + +const Template: Story = (args) => ( + + Strategies + BTC Passive Income + +); + +const Default = Template.bind({}); +Default.args = {}; + +export { Default }; + +export default { + title: 'Forms/Breadcrumbs', + component: Breadcrumbs +} as Meta; diff --git a/src/component-library/Breadcrumbs/Breadcrumbs.style.tsx b/src/component-library/Breadcrumbs/Breadcrumbs.style.tsx new file mode 100644 index 0000000000..2a5ee1e0fa --- /dev/null +++ b/src/component-library/Breadcrumbs/Breadcrumbs.style.tsx @@ -0,0 +1,40 @@ +import styled from 'styled-components'; + +import { Span } from '../Text'; +import { TextLink } from '../TextLink'; +import { theme } from '../theme'; + +type StyledBreadcrumbProps = { + $isDisabled?: boolean; +}; + +const StyledNav = styled.nav``; + +const StyledList = styled.ul` + flex-wrap: nowrap; + flex: 1 0; + justify-content: flex-start; + margin: 0; + padding: 0; + list-style-type: none; + display: flex; +`; + +const StyledListItem = styled.li` + justify-content: flex-start; + align-items: center; + display: inline-flex; + position: relative; +`; + +const StyledSpanBreadcrumb = styled(Span)` + padding: 0 ${theme.spacing.spacing2}; + cursor: default; +`; + +const StyledLinkBreadcrumb = styled(TextLink)` + padding: 0 ${theme.spacing.spacing2}; + text-decoration: none; +`; + +export { StyledLinkBreadcrumb, StyledList, StyledListItem, StyledNav, StyledSpanBreadcrumb }; diff --git a/src/component-library/Breadcrumbs/Breadcrumbs.tsx b/src/component-library/Breadcrumbs/Breadcrumbs.tsx new file mode 100644 index 0000000000..16e79c8ced --- /dev/null +++ b/src/component-library/Breadcrumbs/Breadcrumbs.tsx @@ -0,0 +1,55 @@ +import { AriaBreadcrumbsProps, useBreadcrumbs } from '@react-aria/breadcrumbs'; +import { Children, forwardRef, HTMLAttributes, isValidElement, ReactElement } from 'react'; + +import { useDOMRef } from '../utils/dom'; +import { BreadcrumbItem } from './BreadcrumbItem'; +import { StyledList, StyledListItem, StyledNav } from './Breadcrumbs.style'; + +type Props = { + onAction?: (key: React.Key) => void; + isDisabled?: boolean; +}; + +type NativeAttrs = Omit, keyof Props>; + +type InheritAttrs = Omit; + +type BreadcrumbsProps = Props & NativeAttrs & InheritAttrs; + +const Breadcrumbs = forwardRef( + ({ children, isDisabled, ...props }, ref): JSX.Element => { + const domRef = useDOMRef(ref); + + const { navProps } = useBreadcrumbs(props); + + const childArray: ReactElement[] = []; + Children.forEach(children, (child) => { + if (isValidElement(child)) { + childArray.push(child); + } + }); + + const lastIndex = childArray.length - 1; + + const breadcrumbItems = childArray.map((child, index) => { + const isCurrent = index === lastIndex; + + return ( + + + + ); + }); + + return ( + + {breadcrumbItems} + + ); + } +); + +Breadcrumbs.displayName = 'Breadcrumbs'; + +export { Breadcrumbs }; +export type { BreadcrumbsProps }; diff --git a/src/component-library/Breadcrumbs/index.tsx b/src/component-library/Breadcrumbs/index.tsx new file mode 100644 index 0000000000..f22a966e3c --- /dev/null +++ b/src/component-library/Breadcrumbs/index.tsx @@ -0,0 +1,10 @@ +import { BreadcrumbItem as LibBreadcrumbItem, BreadcrumbItemProps as LibBreadcrumbItemProps } from './BreadcrumbItem'; + +type BreadcrumbItemProps = Omit; + +const BreadcrumbItem = (props: BreadcrumbItemProps): JSX.Element => ; + +export type { BreadcrumbsProps } from './Breadcrumbs'; +export { Breadcrumbs } from './Breadcrumbs'; +export { BreadcrumbItem }; +export type { BreadcrumbItemProps }; diff --git a/src/component-library/index.tsx b/src/component-library/index.tsx index 5fc8268b6c..791aab74ce 100644 --- a/src/component-library/index.tsx +++ b/src/component-library/index.tsx @@ -2,6 +2,8 @@ export type { AccordionItemProps, AccordionProps } from './Accordion'; export { Accordion, AccordionItem } from './Accordion'; export type { AlertProps } from './Alert'; export { Alert } from './Alert'; +export type { BreadcrumbItemProps, BreadcrumbsProps } from './Breadcrumbs'; +export { BreadcrumbItem, Breadcrumbs } from './Breadcrumbs'; export type { CardProps } from './Card'; export { Card } from './Card'; export type { CoinIconProps } from './CoinIcon'; diff --git a/src/component-library/theme/theme.ts b/src/component-library/theme/theme.ts index 1d17b0c68b..c52e6df778 100644 --- a/src/component-library/theme/theme.ts +++ b/src/component-library/theme/theme.ts @@ -517,6 +517,7 @@ const theme = { }, icon: { sizes: { + xs: 'var(--spacing-3)', s: 'var(--spacing-4)', md: 'var(--spacing-6)', lg: 'var(--spacing-8)', diff --git a/src/component-library/utils/format.ts b/src/component-library/utils/format.ts index 8f520ada3f..505d55414b 100644 --- a/src/component-library/utils/format.ts +++ b/src/component-library/utils/format.ts @@ -8,7 +8,8 @@ const formatUSD = (amount: number, options?: { compact?: boolean }): string => { const { format } = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', - notation: options?.compact ? getFormatUSDNotation(amount) : undefined + notation: options?.compact ? getFormatUSDNotation(amount) : undefined, + minimumFractionDigits: amount > 0 && amount < 0.01 ? 3 : undefined }); return format(amount); diff --git a/src/components/AuthCTA/AuthCTA.tsx b/src/components/AuthCTA/AuthCTA.tsx index f799fce007..0fd9408a08 100644 --- a/src/components/AuthCTA/AuthCTA.tsx +++ b/src/components/AuthCTA/AuthCTA.tsx @@ -7,19 +7,16 @@ import { CTA, CTAProps } from '@/component-library'; import { SIGNER_API_URL } from '@/constants'; import { useSignMessage } from '@/hooks/use-sign-message'; import { useSubstrateSecureState } from '@/lib/substrate'; -import { useGetParachainStatus } from '@/utils/hooks/api/system/use-get-parachain-status'; enum AuthStatus { UNAUTH, AUTH, - UNSIGNED, - BLOCKED + UNSIGNED } const useAuthCTAProps = (props: AuthCTAProps): AuthCTAProps => { const { t } = useTranslation(); const { hasSignature, buttonProps } = useSignMessage(); - const { data: parachainStatus } = useGetParachainStatus(); const { selectedAccount } = useSubstrateSecureState(); @@ -28,12 +25,8 @@ const useAuthCTAProps = (props: AuthCTAProps): AuthCTAProps => { return AuthStatus.UNAUTH; } - if (!parachainStatus?.isRunning) { - return AuthStatus.BLOCKED; - } - return !SIGNER_API_URL || hasSignature ? AuthStatus.AUTH : AuthStatus.UNSIGNED; - }, [hasSignature, parachainStatus, selectedAccount]); + }, [hasSignature, selectedAccount]); const dispatch = useDispatch(); @@ -54,13 +47,6 @@ const useAuthCTAProps = (props: AuthCTAProps): AuthCTAProps => { disabled: false, children: t('sign_t&cs') }; - case AuthStatus.BLOCKED: - return { - ...buttonProps, - type: 'button', - disabled: true, - children - }; case AuthStatus.UNAUTH: default: return { diff --git a/src/components/MainContainer/MainContainer.tsx b/src/components/MainContainer/MainContainer.tsx index d32ae26b10..773a413f72 100644 --- a/src/components/MainContainer/MainContainer.tsx +++ b/src/components/MainContainer/MainContainer.tsx @@ -1,11 +1,19 @@ import { FlexProps } from '@/component-library'; +import { SiteInformation } from '@/components'; import { StyledContainer } from './MainContainer.styles'; type MainContainerProps = FlexProps; -const MainContainer = ({ direction = 'column', gap = 'spacing8', ...props }: MainContainerProps): JSX.Element => ( - -); +const MainContainer = ({ direction = 'column', gap = 'spacing8', ...props }: MainContainerProps): JSX.Element => { + const showSiteInformationMessage = !!process.env.REACT_APP_SITE_INFORMATION_MESSAGE; + + return ( + + {showSiteInformationMessage && } + {props.children} + + ); +}; export { MainContainer }; diff --git a/src/components/SiteInformation/SiteInformation.tsx b/src/components/SiteInformation/SiteInformation.tsx new file mode 100644 index 0000000000..6610025083 --- /dev/null +++ b/src/components/SiteInformation/SiteInformation.tsx @@ -0,0 +1,21 @@ +import { Alert, TextLink } from '@/component-library'; + +const SiteInformation = (): JSX.Element => { + const hasLink = !!process.env.REACT_APP_SITE_INFORMATION_LINK; + + return ( + + {process.env.REACT_APP_SITE_INFORMATION_MESSAGE} + {hasLink && ( + <> + {' '} + + More information + + + )} + + ); +}; + +export { SiteInformation }; diff --git a/src/components/SiteInformation/index.tsx b/src/components/SiteInformation/index.tsx new file mode 100644 index 0000000000..a614215aa9 --- /dev/null +++ b/src/components/SiteInformation/index.tsx @@ -0,0 +1 @@ +export { SiteInformation } from './SiteInformation'; diff --git a/src/components/SlippageManager/SlippageManager.tsx b/src/components/SlippageManager/SlippageManager.tsx index 126db75d19..aa13b8add5 100644 --- a/src/components/SlippageManager/SlippageManager.tsx +++ b/src/components/SlippageManager/SlippageManager.tsx @@ -42,6 +42,9 @@ const SlippageManager = forwardRef( onSelectionChange={handleSelectionChange} defaultSelectedKeys={[value.toString()]} > + + 0% + 0.1% diff --git a/src/components/index.tsx b/src/components/index.tsx index 6069e639c8..800c588f63 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -27,6 +27,7 @@ export { PlusDivider } from './PlusDivider'; export type { PoolsTableProps } from './PoolsTable'; export { PoolsTable } from './PoolsTable'; export { ReceivableAssets } from './ReceivableAssets'; +export { SiteInformation } from './SiteInformation'; export type { SlippageManagerProps } from './SlippageManager'; export { SlippageManager } from './SlippageManager'; export type { ToastContainerProps } from './ToastContainer'; diff --git a/src/hooks/api/loans/use-get-account-lending-statistics.tsx b/src/hooks/api/loans/use-get-account-lending-statistics.tsx index 88d750d77f..b56c5cce1a 100644 --- a/src/hooks/api/loans/use-get-account-lending-statistics.tsx +++ b/src/hooks/api/loans/use-get-account-lending-statistics.tsx @@ -1,4 +1,5 @@ import { TickerToData } from '@interlay/interbtc-api'; +import { AccountId } from '@polkadot/types/interfaces'; import Big from 'big.js'; import { useMemo } from 'react'; @@ -92,11 +93,11 @@ const getAccountPositionsStats = ( }; }; -const useGetAccountLendingStatistics = (): UseGetAccountLendingStatistics => { +const useGetAccountLendingStatistics = (proxyAccount?: AccountId): UseGetAccountLendingStatistics => { const { data: { lendPositions, borrowPositions }, refetch: positionsRefetch - } = useGetAccountPositions(); + } = useGetAccountPositions(proxyAccount); const { data: loanAssets, refetch: loanAssetsRefetch } = useGetLoanAssets(); const prices = useGetPrices(); diff --git a/src/hooks/api/loans/use-get-account-positions-earnings.tsx b/src/hooks/api/loans/use-get-account-positions-earnings.tsx index fe59c2173d..66fffb92a1 100644 --- a/src/hooks/api/loans/use-get-account-positions-earnings.tsx +++ b/src/hooks/api/loans/use-get-account-positions-earnings.tsx @@ -1,5 +1,6 @@ import { CurrencyExt, newMonetaryAmount, TickerToData } from '@interlay/interbtc-api'; import { MonetaryAmount } from '@interlay/monetary-js'; +import { AccountId } from '@polkadot/types/interfaces'; import Big from 'big.js'; import { gql, GraphQLClient } from 'graphql-request'; import { useCallback } from 'react'; @@ -68,12 +69,15 @@ type UseGetAccountPositionsEarningsResult = { }; const useGetAccountPositionsEarnings = ( - lendPositions: CollateralPosition[] | undefined + lendPositions: CollateralPosition[] | undefined, + proxyAccount?: AccountId ): UseGetAccountPositionsEarningsResult => { - const { account } = useWallet(); + const { account: primaryAccount } = useWallet(); + + const account = proxyAccount || primaryAccount; const { refetch, isLoading, data, error } = useQuery({ - queryKey: ['loan-earnings', account], + queryKey: ['loan-earnings', account, proxyAccount], queryFn: () => lendPositions && account && getEarnedAmountByTicker(account.toString(), lendPositions), enabled: !!lendPositions && !!account, refetchOnWindowFocus: false, diff --git a/src/hooks/api/loans/use-get-account-positions.tsx b/src/hooks/api/loans/use-get-account-positions.tsx index 6b745db598..895d1425fd 100644 --- a/src/hooks/api/loans/use-get-account-positions.tsx +++ b/src/hooks/api/loans/use-get-account-positions.tsx @@ -4,10 +4,10 @@ import { useCallback } from 'react'; import { useErrorHandler } from 'react-error-boundary'; import { useQuery } from 'react-query'; +import { useWallet } from '@/hooks/use-wallet'; import { BorrowPosition, CollateralPosition } from '@/types/loans'; import { BLOCKTIME_REFETCH_INTERVAL } from '@/utils/constants/api'; -import useAccountId from '../../use-account-id'; import { useGetAccountPositionsEarnings } from './use-get-account-positions-earnings'; const getLendPositionsOfAccount = async (accountId: AccountId): Promise> => @@ -19,13 +19,15 @@ interface UseGetLendPositionsOfAccountResult { refetch: () => void; } -const useGetLendPositionsOfAccount = (): UseGetLendPositionsOfAccountResult => { - const accountId = useAccountId(); +const useGetLendPositionsOfAccount = (proxyAccount?: AccountId): UseGetLendPositionsOfAccountResult => { + const { account: primaryAccount } = useWallet(); + + const account = proxyAccount || primaryAccount; const { data, error, refetch, isLoading } = useQuery({ - queryKey: ['getLendPositionsOfAccount', accountId], - queryFn: () => accountId && getLendPositionsOfAccount(accountId), - enabled: !!accountId, + queryKey: ['getLendPositionsOfAccount', account?.toString(), proxyAccount], + queryFn: () => account && getLendPositionsOfAccount(account), + enabled: !!account, refetchInterval: BLOCKTIME_REFETCH_INTERVAL }); @@ -40,19 +42,15 @@ interface UseGetBorrowPositionsOfAccountResult { refetch: () => void; } -const useGetBorrowPositionsOfAccount = (): UseGetBorrowPositionsOfAccountResult => { - const accountId = useAccountId(); +const useGetBorrowPositionsOfAccount = (proxyAccount?: AccountId): UseGetBorrowPositionsOfAccountResult => { + const { account: primaryAccount } = useWallet(); - const { data, error, refetch, isLoading } = useQuery({ - queryKey: ['getBorrowPositionsOfAccount', accountId], - queryFn: async () => { - if (!accountId) { - throw new Error('Something went wrong!'); - } + const account = proxyAccount || primaryAccount; - return await window.bridge.loans.getBorrowPositionsOfAccount(accountId); - }, - enabled: !!accountId, + const { data, error, refetch, isLoading } = useQuery({ + queryKey: ['getBorrowPositionsOfAccount', account?.toString()], + queryFn: () => account && window.bridge.loans.getBorrowPositionsOfAccount(account), + enabled: !!account, refetchInterval: BLOCKTIME_REFETCH_INTERVAL }); @@ -76,21 +74,22 @@ type UseGetAccountPositionsResult = { refetch: () => void; }; -const useGetAccountPositions = (): UseGetAccountPositionsResult => { +const useGetAccountPositions = (proxyAccount?: AccountId): UseGetAccountPositionsResult => { const { data: lendPositionsWithoutEarnings, isLoading: isLendPositionsLoading, refetch: lendPositionsRefetch - } = useGetLendPositionsOfAccount(); + } = useGetLendPositionsOfAccount(proxyAccount); const { data: borrowPositions, isLoading: isBorrowPositionsLoading, refetch: borrowPositionsRefetch - } = useGetBorrowPositionsOfAccount(); + } = useGetBorrowPositionsOfAccount(proxyAccount); const { getPositionEarnings, isLoading: isAccountEarningsLoading } = useGetAccountPositionsEarnings( - lendPositionsWithoutEarnings + lendPositionsWithoutEarnings, + proxyAccount ); const lendPositions: CollateralPosition[] | undefined = lendPositionsWithoutEarnings?.map((position) => ({ diff --git a/src/hooks/api/loans/use-get-loan-available-amounts.tsx b/src/hooks/api/loans/use-get-loan-available-amounts.tsx index a35a261b19..5457668bb6 100644 --- a/src/hooks/api/loans/use-get-loan-available-amounts.tsx +++ b/src/hooks/api/loans/use-get-loan-available-amounts.tsx @@ -1,5 +1,6 @@ import { CurrencyExt, newMonetaryAmount } from '@interlay/interbtc-api'; import { MonetaryAmount } from '@interlay/monetary-js'; +import { AccountId } from '@polkadot/types/interfaces'; import { useCallback } from 'react'; import { useGetAccountLendingStatistics } from '@/hooks/api/loans/use-get-account-lending-statistics'; @@ -138,10 +139,11 @@ type UseGetLoanAvailableAmountsResult = { const useGetLoanAvailableAmounts = ( action: BorrowAction | LendAction, asset: LoanAsset, - position?: CollateralPosition | BorrowPosition + position?: CollateralPosition | BorrowPosition, + proxyAccount?: AccountId | undefined ): UseGetLoanAvailableAmountsResult => { const { getAvailableBalance } = useGetBalances(); - const { data: statistics } = useGetAccountLendingStatistics(); + const { data: statistics } = useGetAccountLendingStatistics(proxyAccount); const maxCalculatedAmount = getMaxCalculatedAmount(action, asset, position, statistics); diff --git a/src/hooks/api/oracle/use-get-oracle-status.ts b/src/hooks/api/oracle/use-get-oracle-status.ts index a3b4fd632a..f570eec613 100644 --- a/src/hooks/api/oracle/use-get-oracle-status.ts +++ b/src/hooks/api/oracle/use-get-oracle-status.ts @@ -1,5 +1,6 @@ import { useQuery } from 'react-query'; +import { RELAY_CHAIN_NATIVE_TOKEN } from '@/config/relay-chains'; import { REFETCH_INTERVAL } from '@/utils/constants/api'; interface UseGetOracleStatusResult { @@ -13,7 +14,7 @@ enum OracleStatus { } const getOracleStatus = async (): Promise => { - const isOracleOnline = await window.bridge.oracle.isOnline(); + const isOracleOnline = await window.bridge.oracle.isOnline(RELAY_CHAIN_NATIVE_TOKEN); return isOracleOnline ? OracleStatus.ONLINE : OracleStatus.OFFLINE; }; diff --git a/src/hooks/api/use-get-dex-volume.tsx b/src/hooks/api/use-get-dex-volume.tsx index 82ce60870b..4d494d20f3 100644 --- a/src/hooks/api/use-get-dex-volume.tsx +++ b/src/hooks/api/use-get-dex-volume.tsx @@ -57,7 +57,11 @@ const GET_DEX_VOLUMES = gql` ...AmountFields } } - endVolumes: cumulativeDexTradingVolumes(limit: 1, orderBy: tillTimestamp_DESC, where: { tillTimestamp_lte: $end }) { + endVolumes: cumulativeDexTradingVolumes( + limit: 1 + orderBy: tillTimestamp_DESC + where: { tillTimestamp_lte: $end, tillTimestamp_gte: $start } + ) { tillTimestamp amounts { ...AmountFields @@ -95,6 +99,10 @@ const useGetDexVolumes = (range: DateRangeVolume): UseGetCurrenciesResult => { const data = await graphQLClient.request(GET_DEX_VOLUMES, { start, end }); + if (!data.startVolumes.length || !data.endVolumes.length) { + return {}; + } + const [startVolumes] = data.startVolumes; const [endVolumes] = data.endVolumes; diff --git a/src/hooks/api/xcm/xcm-endpoints.ts b/src/hooks/api/xcm/xcm-endpoints.ts index 73fbbe80c7..7f40bd7a80 100644 --- a/src/hooks/api/xcm/xcm-endpoints.ts +++ b/src/hooks/api/xcm/xcm-endpoints.ts @@ -13,8 +13,7 @@ const XCMEndpoints: XCMEndpointsRecord = { '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' + 'wss://karura-rpc-3.aca-api.network/ws' ], kintsugi: ['wss://api-kusama.interlay.io/parachain'], kusama: ['wss://kusama-rpc.polkadot.io', 'wss://kusama-rpc.dwellir.com'], diff --git a/src/hooks/transaction/extrinsics/lib.ts b/src/hooks/transaction/extrinsics/lib.ts index 04950ac7a6..4e8d09ad4f 100644 --- a/src/hooks/transaction/extrinsics/lib.ts +++ b/src/hooks/transaction/extrinsics/lib.ts @@ -1,5 +1,8 @@ import { ExtrinsicData } from '@interlay/interbtc-api'; +import { DEFAULT_PROXY_ACCOUNT_AMOUNT, PROXY_ACCOUNT_RESERVE_AMOUNT } from '@/utils/constants/account'; +import { proxifyExtrinsic } from '@/utils/helpers/extrinsic'; + import { LibActions, Transaction } from '../types'; const getLibExtrinsic = async (params: LibActions): Promise => { @@ -67,13 +70,109 @@ const getLibExtrinsic = async (params: LibActions): Promise => { /* END - LOANS */ /* START - STRATEGIES */ - case Transaction.STRATEGIES_DEPOSIT: - return window.bridge.loans.lend(...params.args); - case Transaction.STRATEGIES_WITHDRAW: - return window.bridge.loans.withdraw(...params.args); + case Transaction.STRATEGIES_INITIALIZE_PROXY: { + // Initialize 10 proxy accounts and then if we deposit for the first time, the proxy + // account will be assigned and stored in identity pallet. + const createProxiesExtrinsics = [...Array(DEFAULT_PROXY_ACCOUNT_AMOUNT).keys()].map((index) => + window.bridge.api.tx.proxy.createPure('Any', 0, index) + ); + const batchedExtrinsics = window.bridge.transaction.buildBatchExtrinsic(createProxiesExtrinsics); + + return { extrinsic: batchedExtrinsics }; + } + // Since we use proxy accounts for strategies, first argument is always proxy account for which + // the action should be performed - this account must be passed. + // Second argument is always boolean denoting if the proxy account identity was set or not. + case Transaction.STRATEGIES_DEPOSIT: { + const [strategyType, proxyAccount, isIdentitySet, ...args] = params.args; + const [, depositAmount] = args; + + const transferExtrinsic = window.bridge.tokens.transfer(proxyAccount.toString(), depositAmount); + + const strategyDepositExtrinsic = (await window.bridge.loans.lend(...args)).extrinsic; + const proxiedStrategyDepositExtrinsic = proxifyExtrinsic(proxyAccount, strategyDepositExtrinsic); + + if (isIdentitySet) { + const batchedExtrinsics = window.bridge.transaction.buildBatchExtrinsic([ + transferExtrinsic.extrinsic, + proxiedStrategyDepositExtrinsic + ]); + + return { extrinsic: batchedExtrinsics }; + } else { + const identityLockAmountTransferExtrinsic = window.bridge.tokens.transfer( + proxyAccount.toString(), + PROXY_ACCOUNT_RESERVE_AMOUNT + ); + const strategyAccountIdentity = { + additional: [[{ Raw: 'strategyType' }, { Raw: strategyType }]] + }; + const setIdentityExtrinsic = window.bridge.api.tx.identity.setIdentity(strategyAccountIdentity); + const proxiedSetIdentityExtrinsic = proxifyExtrinsic(proxyAccount, setIdentityExtrinsic); + + const batchedExtrinsicsWithIdentity = window.bridge.transaction.buildBatchExtrinsic([ + identityLockAmountTransferExtrinsic.extrinsic, + proxiedSetIdentityExtrinsic, + transferExtrinsic.extrinsic, + proxiedStrategyDepositExtrinsic + ]); + + return { extrinsic: batchedExtrinsicsWithIdentity }; + } + } + + case Transaction.STRATEGIES_WITHDRAW: { + const primaryAccount = window.bridge.account; + if (!primaryAccount) { + throw new Error('Strategy primary account not found.'); + } + + const [, proxyAccount, ...args] = params.args; + const [, withdrawalAmount] = args; + + const strategyWithdrawalExtrinsic = (await window.bridge.loans.withdraw(...args)).extrinsic; + const proxiedStrategyWithdrawExtrinsic = proxifyExtrinsic(proxyAccount, strategyWithdrawalExtrinsic); + + const transferExtrinsic = window.bridge.tokens.transfer(primaryAccount.toString(), withdrawalAmount).extrinsic; + const proxiedTransferExtrinsic = proxifyExtrinsic(proxyAccount, transferExtrinsic); + + const batchExtrinsic = window.bridge.transaction.buildBatchExtrinsic([ + proxiedStrategyWithdrawExtrinsic, + proxiedTransferExtrinsic + ]); + + return { extrinsic: batchExtrinsic }; + } + case Transaction.STRATEGIES_ALL_WITHDRAW: { - const [underlyingCurrency] = params.args; - return window.bridge.loans.withdrawAll(underlyingCurrency); + const primaryAccount = window.bridge.account; + if (!primaryAccount) { + throw new Error('Primary account not found.'); + } + + const [, proxyAccount, underlyingCurrency, withdrawalAmount] = params.args; + + const clearIdentityExtrinsic = window.bridge.api.tx.identity.clearIdentity(); + + const transferIdentityReserveAmount = window.bridge.tokens.transfer( + primaryAccount.toString(), + PROXY_ACCOUNT_RESERVE_AMOUNT + ).extrinsic; + + const strategyWithdrawalExtrinsic = (await window.bridge.loans.withdrawAll(underlyingCurrency)).extrinsic; + + const transferExtrinsic = window.bridge.tokens.transfer(primaryAccount.toString(), withdrawalAmount).extrinsic; + + const batchExtrinsic = window.bridge.transaction.buildBatchExtrinsic([ + clearIdentityExtrinsic, + transferIdentityReserveAmount, + strategyWithdrawalExtrinsic, + transferExtrinsic + ]); + + const proxiedBatchExtrinsic = proxifyExtrinsic(proxyAccount, batchExtrinsic); + + return { extrinsic: proxiedBatchExtrinsic }; } /* END - STRATEGIES */ diff --git a/src/hooks/transaction/hooks/use-transaction.ts b/src/hooks/transaction/hooks/use-transaction.ts index 14e39be1b6..0294d7ad59 100644 --- a/src/hooks/transaction/hooks/use-transaction.ts +++ b/src/hooks/transaction/hooks/use-transaction.ts @@ -70,7 +70,16 @@ function useTransaction( onReady: () => onSigning(params) }; - return submitTransaction(window.bridge.api, params.accountAddress, feeWrappedExtrinsic, expectedStatus, events); + const shouldDryRun = params.type !== Transaction.XCM_TRANSFER; + + return submitTransaction( + window.bridge.api, + params.accountAddress, + feeWrappedExtrinsic, + expectedStatus, + events, + shouldDryRun + ); }, [feeData?.amount, onSigning, pools] ); diff --git a/src/hooks/transaction/submission/submit.ts b/src/hooks/transaction/submission/submit.ts index 2eaa71bc9a..ddf21fca36 100644 --- a/src/hooks/transaction/submission/submit.ts +++ b/src/hooks/transaction/submission/submit.ts @@ -15,7 +15,8 @@ const handleTransaction = async ( account: AddressOrPair, extrinsicData: ExtrinsicData, expectedStatus?: ExtrinsicStatus['type'], - callbacks?: TransactionEvents + callbacks?: TransactionEvents, + shouldDryRun?: boolean ) => { let isComplete = false; @@ -28,7 +29,7 @@ const handleTransaction = async ( // Extrinsic is signed at first and then we use the same signed extrinsic // for dry-running and submission. .signAsync(account, { nonce: -1 }) - .then(dryRun) + .then((signedExtrinsic) => (shouldDryRun ? dryRun(signedExtrinsic) : signedExtrinsic)) .then((signedExtrinsic) => signedExtrinsic.send(callback)) .then((unsub) => (unsubscribe = unsub)) .catch((error) => reject(error)); @@ -59,6 +60,7 @@ const handleTransaction = async ( * @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) + * @param {boolean} shouldDryRun wether dry run should be executed * @return {Promise} transaction data that also can contain meta data in case of error */ const submitTransaction = async ( @@ -66,9 +68,16 @@ const submitTransaction = async ( account: AddressOrPair, extrinsicData: ExtrinsicData, expectedStatus?: ExtrinsicStatus['type'], - callbacks?: TransactionEvents + callbacks?: TransactionEvents, + shouldDryRun?: boolean ): Promise => { - const { result, unsubscribe } = await handleTransaction(account, extrinsicData, expectedStatus, callbacks); + const { result, unsubscribe } = await handleTransaction( + account, + extrinsicData, + expectedStatus, + callbacks, + shouldDryRun + ); unsubscribe(); diff --git a/src/hooks/transaction/types/index.ts b/src/hooks/transaction/types/index.ts index cbacc3a1a9..0e755769c3 100644 --- a/src/hooks/transaction/types/index.ts +++ b/src/hooks/transaction/types/index.ts @@ -51,6 +51,7 @@ enum Transaction { LOANS_REPAY = 'LOANS_REPAY', LOANS_REPAY_ALL = 'LOANS_REPAY_ALL', // Stategies + STRATEGIES_INITIALIZE_PROXY = 'STRATEGIES_INITIALIZE_PROXY', STRATEGIES_DEPOSIT = 'STRATEGIES_DEPOSIT', STRATEGIES_WITHDRAW = 'STRATEGIES_WITHDRAW', STRATEGIES_ALL_WITHDRAW = 'STRATEGIES_ALL_WITHDRAW', diff --git a/src/hooks/transaction/types/strategies.ts b/src/hooks/transaction/types/strategies.ts index aaf0703b4b..be8319c512 100644 --- a/src/hooks/transaction/types/strategies.ts +++ b/src/hooks/transaction/types/strategies.ts @@ -1,22 +1,34 @@ import { InterBtcApi } from '@interlay/interbtc-api'; +import { AccountId } from '@polkadot/types/interfaces'; + +import { StrategyType } from '@/pages/Strategies/types'; import { Transaction } from '.'; +interface StrategiesInitializeProxyAction { + type: Transaction.STRATEGIES_INITIALIZE_PROXY; + args: [StrategyType]; +} + interface StrategiesDepositAction { type: Transaction.STRATEGIES_DEPOSIT; - args: Parameters; + args: [StrategyType, AccountId, boolean, ...Parameters]; } interface StrategiesWithdrawAction { type: Transaction.STRATEGIES_WITHDRAW; - args: Parameters; + args: [StrategyType, AccountId, ...Parameters]; } interface StrategiesWithdrawAllAction { type: Transaction.STRATEGIES_ALL_WITHDRAW; - args: Parameters; + args: [StrategyType, AccountId, ...Parameters]; } -type StrategiesActions = StrategiesDepositAction | StrategiesWithdrawAction | StrategiesWithdrawAllAction; +type StrategiesActions = + | StrategiesInitializeProxyAction + | StrategiesDepositAction + | StrategiesWithdrawAction + | StrategiesWithdrawAllAction; export type { StrategiesActions }; diff --git a/src/hooks/transaction/utils/description.ts b/src/hooks/transaction/utils/description.ts index 8aebc07828..d2f5ffb5ed 100644 --- a/src/hooks/transaction/utils/description.ts +++ b/src/hooks/transaction/utils/description.ts @@ -251,7 +251,7 @@ const getTranslationArgs = ( /* START - STRATEGIES */ case Transaction.STRATEGIES_DEPOSIT: { - const [currency, amount] = params.args; + const [, , , currency, amount] = params.args; return { key: isPast ? 'transaction.deposited_amount' : 'transaction.depositing_amount', @@ -262,7 +262,7 @@ const getTranslationArgs = ( }; } case Transaction.STRATEGIES_WITHDRAW: { - const [currency, amount] = params.args; + const [, , currency, amount] = params.args; return { key: isPast ? 'transaction.withdrew_amount' : 'transaction.withdrawing_amount', @@ -273,7 +273,7 @@ const getTranslationArgs = ( }; } case Transaction.STRATEGIES_ALL_WITHDRAW: { - const [currency] = params.args; + const [, , currency] = params.args; return { key: isPast ? 'transaction.withdrew' : 'transaction.withdrawing', diff --git a/src/hooks/transaction/utils/fee.ts b/src/hooks/transaction/utils/fee.ts index c68ea72701..fdb3e311e8 100644 --- a/src/hooks/transaction/utils/fee.ts +++ b/src/hooks/transaction/utils/fee.ts @@ -12,6 +12,7 @@ import { MonetaryAmount } from '@interlay/monetary-js'; import { SubmittableExtrinsic } from '@polkadot/api/types'; import { GOVERNANCE_TOKEN } from '@/config/relay-chains'; +import { PROXY_ACCOUNT_RESERVE_AMOUNT } from '@/utils/constants/account'; import { getExtrinsic } from '../extrinsics'; import { Actions, Transaction } from '../types'; @@ -62,6 +63,7 @@ const getTxFeeSwapData = async ( reverseDirectionTrade.outputAmount.toString(true), baseExtrinsic ); + const withSwapTxFee = await window.bridge.transaction.getFeeEstimate(reverseDirectionExtrinsic); const { inputAmount, path } = getOptimalTradeForTxFeeSwap( withSwapTxFee.mul(OUTPUT_AMOUNT_SAFE_OFFSET_MULTIPLIER), @@ -158,8 +160,11 @@ const getAmount = (params: Actions): MonetaryAmount[] | undefined = } /* END - LOANS */ case Transaction.STRATEGIES_DEPOSIT: { - const [, amount] = params.args; - return [amount]; + const [, , isIdentitySet, , amount] = params.args; + if (isIdentitySet) { + return [amount]; + } + return [amount, PROXY_ACCOUNT_RESERVE_AMOUNT]; } case Transaction.VAULTS_REGISTER_NEW_COLLATERAL: { const [amount] = params.args; @@ -177,6 +182,7 @@ const getAmount = (params: Actions): MonetaryAmount[] | undefined = case Transaction.LOANS_DISABLE_COLLATERAL: case Transaction.STRATEGIES_ALL_WITHDRAW: case Transaction.STRATEGIES_WITHDRAW: + case Transaction.STRATEGIES_INITIALIZE_PROXY: case Transaction.AMM_CLAIM_REWARDS: return undefined; } diff --git a/src/hooks/use-all-cumulative-vault-collateral-volumes.ts b/src/hooks/use-all-cumulative-vault-collateral-volumes.ts new file mode 100644 index 0000000000..993a3725fe --- /dev/null +++ b/src/hooks/use-all-cumulative-vault-collateral-volumes.ts @@ -0,0 +1,25 @@ +import { TickerToData } from '@interlay/interbtc-api'; +import { useQuery, UseQueryResult } from 'react-query'; + +import cumulativeVaultCollateralVolumesFetcher, { + CUMULATIVE_VAULT_COLLATERALVOLUMES_FETCHER +} from '@/services/fetchers/cumulative-vault-collateral-volumes-fetcher'; +import { VolumeDataPoint } from '@/services/fetchers/cumulative-volumes-fetcher'; + +import { useGetCollateralCurrencies } from './api/use-get-collateral-currencies'; + +const useAllCumulativeVaultCollateralVolumes = ( + cutoffTimestamps: Array +): UseQueryResult>, Error> => { + const { data: collateralCurrencies } = useGetCollateralCurrencies(true); + + return useQuery>, Error>( + [CUMULATIVE_VAULT_COLLATERALVOLUMES_FETCHER, cutoffTimestamps, collateralCurrencies], + cumulativeVaultCollateralVolumesFetcher, + { + enabled: !!collateralCurrencies + } + ); +}; + +export default useAllCumulativeVaultCollateralVolumes; diff --git a/src/legacy-components/TestnetBanner/index.tsx b/src/legacy-components/TestnetBanner/index.tsx deleted file mode 100644 index abd3c37836..0000000000 --- a/src/legacy-components/TestnetBanner/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import clsx from 'clsx'; - -import WarningBanner from '../WarningBanner'; - -const TestnetBanner = (): JSX.Element => ( - -

- Thanks for trying out the testnet! The testnet might be reset at any point to make sure we can get the latest - version of our software to you. -

-
-); - -export default TestnetBanner; diff --git a/src/lib/form/schemas/strategies.ts b/src/lib/form/schemas/strategies.ts index 324546a18e..8bfe5e2faf 100644 --- a/src/lib/form/schemas/strategies.ts +++ b/src/lib/form/schemas/strategies.ts @@ -1,3 +1,10 @@ +import { CurrencyExt, newMonetaryAmount } from '@interlay/interbtc-api'; +import { MonetaryAmount } from '@interlay/monetary-js'; +import { TFunction } from 'i18next'; + +import { GOVERNANCE_TOKEN } from '@/config/relay-chains'; +import { PROXY_ACCOUNT_RESERVE_AMOUNT } from '@/utils/constants/account'; + import yup, { MaxAmountValidationParams, MinAmountValidationParams } from '../yup.custom'; const STRATEGY_DEPOSIT_AMOUNT_FIELD = 'strategy-deposit-amount'; @@ -8,11 +15,32 @@ type StrategyDepositFormData = { [STRATEGY_DEPOSIT_FEE_TOKEN_FIELD]?: string; }; -type StrategyDepositValidationParams = MaxAmountValidationParams & MinAmountValidationParams; +type StrategyDepositValidationParams = MaxAmountValidationParams & + MinAmountValidationParams & { + governanceBalance: MonetaryAmount; + requireProxyDeposit: boolean; + }; -const strategyDepositSchema = (action: string, params: StrategyDepositValidationParams): yup.ObjectSchema => { +const strategyDepositSchema = ( + action: string, + params: StrategyDepositValidationParams, + t: TFunction +): yup.ObjectSchema => { return yup.object().shape({ - [STRATEGY_DEPOSIT_AMOUNT_FIELD]: yup.string().requiredAmount(action).maxAmount(params).minAmount(params, action), + [STRATEGY_DEPOSIT_AMOUNT_FIELD]: yup + .string() + .requiredAmount(action) + .maxAmount(params) + .minAmount(params, action) + .fees( + { + transactionFee: params.requireProxyDeposit + ? PROXY_ACCOUNT_RESERVE_AMOUNT + : newMonetaryAmount(0, GOVERNANCE_TOKEN), + governanceBalance: params.governanceBalance + }, + t('strategies.proxy_deposit_insufficient_funds', { currency: GOVERNANCE_TOKEN.ticker }) + ), [STRATEGY_DEPOSIT_FEE_TOKEN_FIELD]: yup.string().required() }); }; diff --git a/src/pages/Dashboard/cards/OracleStatusCard/index.tsx b/src/pages/Dashboard/cards/OracleStatusCard/index.tsx index fcb27215e6..3d241d7c6c 100644 --- a/src/pages/Dashboard/cards/OracleStatusCard/index.tsx +++ b/src/pages/Dashboard/cards/OracleStatusCard/index.tsx @@ -1,9 +1,8 @@ -import { CurrencyExt } from '@interlay/interbtc-api'; -import { Bitcoin, ExchangeRate } from '@interlay/monetary-js'; import clsx from 'clsx'; import { withErrorBoundary } from 'react-error-boundary'; import { useTranslation } from 'react-i18next'; +import { formatNumber } from '@/common/utils/utils'; import { RELAY_CHAIN_NATIVE_TOKEN, RELAY_CHAIN_NATIVE_TOKEN_SYMBOL } from '@/config/relay-chains'; import { OracleStatus, useGetOracleStatus } from '@/hooks/api/oracle/use-get-oracle-status'; import { useGetExchangeRate } from '@/hooks/api/use-get-exchange-rate'; @@ -33,15 +32,11 @@ const OracleStatusCard = ({ hasLinks }: Props): JSX.Element => { return <>Loading...; } - const exchangeRate = relayChainExchangeRate - ? new ExchangeRate(Bitcoin, RELAY_CHAIN_NATIVE_TOKEN, relayChainExchangeRate.toBig(), 0, 0) - : 0; - const oracleOnline = oracleStatus && oracleStatus === OracleStatus.ONLINE; let statusText; let statusCircleText; - if (exchangeRate === undefined) { + if (relayChainExchangeRate === undefined) { statusText = t('dashboard.oracles.not_available'); statusCircleText = t('unavailable'); } else if (oracleOnline === true) { @@ -88,9 +83,13 @@ const OracleStatusCard = ({ hasLinks }: Props): JSX.Element => { > {statusCircleText} - {exchangeRate && ( + {relayChainExchangeRate && ( - {exchangeRate.toHuman(5)} {RELAY_CHAIN_NATIVE_TOKEN_SYMBOL} + {formatNumber(Number(relayChainExchangeRate.toHuman(5)), { + minimumFractionDigits: 5, + maximumFractionDigits: 5 + })}{' '} + {RELAY_CHAIN_NATIVE_TOKEN_SYMBOL} )} diff --git a/src/pages/Dashboard/cards/ParachainSecurityCard/index.tsx b/src/pages/Dashboard/cards/ParachainSecurityCard/index.tsx deleted file mode 100644 index c04d5f250e..0000000000 --- a/src/pages/Dashboard/cards/ParachainSecurityCard/index.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import clsx from 'clsx'; -import { useTranslation } from 'react-i18next'; - -import { WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains'; -import Ring64, { Ring64Title, Ring64Value } from '@/legacy-components/Ring64'; -import { PAGES } from '@/utils/constants/links'; -import { getColorShade } from '@/utils/helpers/colors'; -import { useGetParachainStatus } from '@/utils/hooks/api/system/use-get-parachain-status'; - -import Stats, { StatsRouterLink } from '../../Stats'; -import DashboardCard from '../DashboardCard'; - -interface Props { - hasLinks?: boolean; -} - -const ParachainSecurityCard = ({ hasLinks }: Props): JSX.Element => { - const { t } = useTranslation(); - - const { data: parachainStatus, isLoading } = useGetParachainStatus(); - - const getParachainStatusText = () => { - if (!parachainStatus && !isLoading) { - return t('no_data'); - } - - if (!parachainStatus || isLoading) { - return t('loading'); - } - - if (parachainStatus.isError || parachainStatus.isShutdown) { - return t('dashboard.parachain.halted'); - } - - return t('dashboard.parachain.secure'); - }; - - return ( - - {hasLinks && Status updates}} - /> - - - {getParachainStatusText()} - - - {t('dashboard.parachain.parachain_is', { - wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL - })} - - - - ); -}; - -export default ParachainSecurityCard; diff --git a/src/pages/Dashboard/index.tsx b/src/pages/Dashboard/index.tsx index 668d2d44b2..bf150e0f36 100644 --- a/src/pages/Dashboard/index.tsx +++ b/src/pages/Dashboard/index.tsx @@ -6,7 +6,6 @@ import { PAGES } from '@/utils/constants/links'; const Home = React.lazy(() => import(/* webpackChunkName: 'home' */ './sub-pages/Home')); const Vaults = React.lazy(() => import(/* webpackChunkName: 'vaults' */ './sub-pages/Vaults')); -const Parachain = React.lazy(() => import(/* webpackChunkName: 'parachain' */ './sub-pages/Parachain')); const Oracles = React.lazy(() => import(/* webpackChunkName: 'oracles' */ './sub-pages/Oracles')); const IssueRequests = React.lazy(() => import(/* webpackChunkName: 'issue-requests' */ './sub-pages/IssueRequests')); const RedeemRequests = React.lazy(() => import(/* webpackChunkName: 'redeem-requests' */ './sub-pages/RedeemRequests')); @@ -21,9 +20,6 @@ const Dashboard = (): JSX.Element => { - - - diff --git a/src/pages/Dashboard/sub-pages/Home/LockedCollateralsCard/index.tsx b/src/pages/Dashboard/sub-pages/Home/LockedCollateralsCard/index.tsx index c74c10068c..617d95f1f7 100644 --- a/src/pages/Dashboard/sub-pages/Home/LockedCollateralsCard/index.tsx +++ b/src/pages/Dashboard/sub-pages/Home/LockedCollateralsCard/index.tsx @@ -4,14 +4,8 @@ import { useTranslation } from 'react-i18next'; import { convertMonetaryAmountToValueInUSD, formatUSD, getLastMidnightTimestamps } from '@/common/utils/utils'; import { COUNT_OF_DATES_FOR_CHART } from '@/config/charts'; -import { - GOVERNANCE_TOKEN, - GOVERNANCE_TOKEN_SYMBOL, - RELAY_CHAIN_NATIVE_TOKEN, - RELAY_CHAIN_NATIVE_TOKEN_SYMBOL -} from '@/config/relay-chains'; import { useGetPrices } from '@/hooks/api/use-get-prices'; -import useCumulativeCollateralVolumes from '@/hooks/use-cumulative-collateral-volumes'; +import useAllCumulativeVaultCollateralVolumes from '@/hooks/use-all-cumulative-vault-collateral-volumes'; import ErrorFallback from '@/legacy-components/ErrorFallback'; import { INTERLAY_DENIM, KINTSUGI_SUPERNOVA } from '@/utils/constants/colors'; import { PAGES } from '@/utils/constants/links'; @@ -29,44 +23,23 @@ const LockedCollateralsCard = (): JSX.Element => { const prices = useGetPrices(); const { - isIdle: cumulativeRelayChainNativeTokenVolumesIdle, - isLoading: cumulativeRelayChainNativeTokenVolumesLoading, - data: cumulativeRelayChainNativeTokenVolumes, - error: cumulativeRelayChainNativeTokenVolumesError - } = useCumulativeCollateralVolumes(RELAY_CHAIN_NATIVE_TOKEN, cutoffTimestamps); - useErrorHandler(cumulativeRelayChainNativeTokenVolumesError); - - const { - isIdle: cumulativeGovernanceTokenVolumesIdle, - isLoading: cumulativeGovernanceTokenVolumesLoading, - data: cumulativeGovernanceTokenVolumes, - error: cumulativeGovernanceTokenVolumesError - } = useCumulativeCollateralVolumes(GOVERNANCE_TOKEN, cutoffTimestamps); - useErrorHandler(cumulativeGovernanceTokenVolumesError); - - const relayChainNativeTokenPriceInUSD = getTokenPrice(prices, RELAY_CHAIN_NATIVE_TOKEN_SYMBOL)?.usd; - const governanceTokenPriceInUSD = getTokenPrice(prices, GOVERNANCE_TOKEN_SYMBOL)?.usd; + data: allCumulativeVaultCollateralVolumes, + error: allCumulativeVaultCollateralVolumesError + } = useAllCumulativeVaultCollateralVolumes(cutoffTimestamps); + useErrorHandler(allCumulativeVaultCollateralVolumesError); const cumulativeUSDVolumes = React.useMemo(() => { - if (cumulativeRelayChainNativeTokenVolumes === undefined || cumulativeGovernanceTokenVolumes === undefined) return; + if (allCumulativeVaultCollateralVolumes === undefined) return; return Array(COUNT_OF_DATES_FOR_CHART) .fill(0) .map((_, index) => { - const collaterals = [ - { - cumulativeVolumes: cumulativeRelayChainNativeTokenVolumes, - tokenPriceInUSD: relayChainNativeTokenPriceInUSD - } - ]; - - // Includes governance token data only if the price is available. - if (governanceTokenPriceInUSD !== undefined) { - collaterals.push({ - cumulativeVolumes: cumulativeGovernanceTokenVolumes, - tokenPriceInUSD: governanceTokenPriceInUSD - }); - } + const collateralTickers = Object.keys(allCumulativeVaultCollateralVolumes); + + const collaterals = collateralTickers.map((ticker: string) => ({ + cumulativeVolumes: allCumulativeVaultCollateralVolumes[ticker], + tokenPriceInUSD: getTokenPrice(prices, ticker)?.usd + })); let sumValueInUSD = 0; for (const collateral of collaterals) { @@ -80,21 +53,11 @@ const LockedCollateralsCard = (): JSX.Element => { tillTimestamp: cutoffTimestamps[index] }; }); - }, [ - cumulativeRelayChainNativeTokenVolumes, - cumulativeGovernanceTokenVolumes, - relayChainNativeTokenPriceInUSD, - governanceTokenPriceInUSD - ]); + }, [allCumulativeVaultCollateralVolumes, prices]); const renderContent = () => { // TODO: should use skeleton loaders - if ( - cumulativeRelayChainNativeTokenVolumesIdle || - cumulativeRelayChainNativeTokenVolumesLoading || - cumulativeGovernanceTokenVolumesIdle || - cumulativeGovernanceTokenVolumesLoading - ) { + if (allCumulativeVaultCollateralVolumes === undefined) { return <>Loading...; } diff --git a/src/pages/Dashboard/sub-pages/Home/index.tsx b/src/pages/Dashboard/sub-pages/Home/index.tsx index 18108967b4..d44f14f398 100644 --- a/src/pages/Dashboard/sub-pages/Home/index.tsx +++ b/src/pages/Dashboard/sub-pages/Home/index.tsx @@ -8,7 +8,6 @@ import ActiveVaultsCard from '../../cards/ActiveVaultsCard'; import BTCRelayCard from '../../cards/BTCRelayCard'; import CollateralizationCard from '../../cards/CollateralizationCard'; import OracleStatusCard from '../../cards/OracleStatusCard'; -import ParachainSecurityCard from '../../cards/ParachainSecurityCard'; import ActiveCollatorsCard from './ActiveCollatorsCard'; import LockedCollateralsCard from './LockedCollateralsCard'; import WrappedTokenCard from './WrappedTokenCard'; @@ -23,7 +22,6 @@ const Home = (): JSX.Element => { - diff --git a/src/pages/Dashboard/sub-pages/Parachain/index.tsx b/src/pages/Dashboard/sub-pages/Parachain/index.tsx deleted file mode 100644 index 4b5a4a47ec..0000000000 --- a/src/pages/Dashboard/sub-pages/Parachain/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useTranslation } from 'react-i18next'; - -import Hr1 from '@/legacy-components/hrs/Hr1'; -import PageTitle from '@/legacy-components/PageTitle'; -import TimerIncrement from '@/legacy-components/TimerIncrement'; - -import ParachainSecurityCard from '../../cards/ParachainSecurityCard'; - -const Parachain = (): JSX.Element => { - const { t } = useTranslation(); - - return ( - <> -
- } /> - -
- - - ); -}; - -export default Parachain; diff --git a/src/pages/Dashboard/sub-pages/Vaults/LockedCollateralCard/index.tsx b/src/pages/Dashboard/sub-pages/Vaults/LockedCollateralCard/index.tsx index af5eea9735..982c6341ff 100644 --- a/src/pages/Dashboard/sub-pages/Vaults/LockedCollateralCard/index.tsx +++ b/src/pages/Dashboard/sub-pages/Vaults/LockedCollateralCard/index.tsx @@ -47,7 +47,7 @@ const LockedCollateralCard = ({ if (cumulativeVolumes === undefined) { throw new Error('Something went wrong!'); } - const totalLockedCollateralTokenAmount = cumulativeVolumes.slice(-1)[0].amount; + const totalLockedCollateralTokenAmount = cumulativeVolumes.slice(-1)[0]?.amount; let chartLineColor; if (process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT) { diff --git a/src/pages/Pools/components/DepositForm/DepositForm.tsx b/src/pages/Pools/components/DepositForm/DepositForm.tsx index 0673db1da6..26d0064bbe 100644 --- a/src/pages/Pools/components/DepositForm/DepositForm.tsx +++ b/src/pages/Pools/components/DepositForm/DepositForm.tsx @@ -28,11 +28,6 @@ import { PoolName } from '../PoolName'; import { StyledPlusDivider, StyledTokenInput } from './DepositForm.styles'; import { DepositOutputAssets } from './DepositOutputAssets'; -const isCustomAmountsMode = (form: ReturnType, pool: LiquidityPool) => - // If pool has no liquidity, the assets ratio is set by the user, - // therefore the value inputted is directly used. - (form.dirty && Object.values(form.touched).filter(Boolean).length > 0) || pool.isEmpty; - type DepositFormProps = { pool: LiquidityPool; overlappingModalRef: RefObject; @@ -45,8 +40,6 @@ const DepositForm = ({ pool, overlappingModalRef, onSuccess, onSigning }: Deposi const [slippage, setSlippage] = useState(0.1); - const [isCustomMode, setCustomMode] = useState(false); - const accountId = useAccountId(); const { t } = useTranslation(); const { getAvailableBalance } = useGetBalances(); @@ -64,7 +57,7 @@ const DepositForm = ({ pool, overlappingModalRef, onSuccess, onSigning }: Deposi if (feeCurrencyAmount && transaction.fee.data) { const newFeeCurrencyAmount = transaction.calculateAmountWithFeeDeducted(feeCurrencyAmount); - if (isCustomMode) { + if (pool.isEmpty) { return amounts.map((amount) => transaction.fee.isEqualFeeCurrency(amount.currency) ? newFeeCurrencyAmount : amount ); @@ -75,7 +68,7 @@ const DepositForm = ({ pool, overlappingModalRef, onSuccess, onSigning }: Deposi return amounts; }, - [isCustomMode, pool, transaction] + [pool, transaction] ); const getTransactionArgs = useCallback( @@ -148,11 +141,7 @@ const DepositForm = ({ pool, overlappingModalRef, onSuccess, onSigning }: Deposi }); const handleChange: ChangeEventHandler = async (e) => { - const isCustom = isCustomAmountsMode(form, pool); - - if (isCustom) { - return setCustomMode(true); - } + if (pool.isEmpty) return; if (!e.target.value || isNaN(Number(e.target.value))) { return form.setValues({ ...form.values, ...defaultValues }); diff --git a/src/pages/Strategies/Strategy/Strategy.tsx b/src/pages/Strategies/Strategy/Strategy.tsx index f3902782f4..85d527d353 100644 --- a/src/pages/Strategies/Strategy/Strategy.tsx +++ b/src/pages/Strategies/Strategy/Strategy.tsx @@ -1,7 +1,7 @@ import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router'; -import { Card, CoinIcon, Flex, H1, H2, P, TextLink } from '@/component-library'; +import { BreadcrumbItem, Breadcrumbs, Card, CoinIcon, Flex, H1, H2, P, TextLink } from '@/component-library'; import { MainContainer } from '@/components'; import FullLoadingSpinner from '@/legacy-components/FullLoadingSpinner'; import { PAGES, URL_PARAMETERS } from '@/utils/constants/links'; @@ -10,6 +10,7 @@ import { StrategyInfographics, StrategyInsights, StrategyTag } from '../componen import { getContent } from '../helpers/content'; import { useGetStrategies } from '../hooks/use-get-strategies'; import { useGetStrategyPosition } from '../hooks/use-get-strategy-position'; +import { useGetStrategyProxyAccount } from '../hooks/use-get-strategy-proxy-account'; import { StrategyRisk, StrategyType } from '../types'; import { StyledFlex, StyledInfoCards, StyledStrategyForm } from './Strategy.styles'; @@ -21,7 +22,9 @@ const Strategy = (): JSX.Element | null => { const strategy = getStrategy(strategyType); - const { data: position, isLoading: isPositionLoading } = useGetStrategyPosition(strategy); + const { account: proxyAccount } = useGetStrategyProxyAccount(strategyType); + + const { data: position, isLoading: isPositionLoading } = useGetStrategyPosition(strategy, proxyAccount); if (!strategies || isPositionLoading) { return ; @@ -39,6 +42,10 @@ const Strategy = (): JSX.Element | null => { return ( + + {t('navigation.strategies')} + {title} +

{title} diff --git a/src/pages/Strategies/components/StrategyForm/StrategyDepositForm.tsx b/src/pages/Strategies/components/StrategyForm/StrategyDepositForm.tsx index e08be71914..4783bebcec 100644 --- a/src/pages/Strategies/components/StrategyForm/StrategyDepositForm.tsx +++ b/src/pages/Strategies/components/StrategyForm/StrategyDepositForm.tsx @@ -5,8 +5,9 @@ import { useTranslation } from 'react-i18next'; import { convertMonetaryAmountToValueInUSD, newSafeMonetaryAmount } from '@/common/utils/utils'; import { Flex, TokenInput } from '@/component-library'; -import { AuthCTA, TransactionFeeDetails } from '@/components'; -import { WRAPPED_TOKEN, WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains'; +import { AuthCTA } from '@/components'; +import { GOVERNANCE_TOKEN, WRAPPED_TOKEN, WRAPPED_TOKEN_SYMBOL } from '@/config/relay-chains'; +import { useGetBalances } from '@/hooks/api/tokens/use-get-balances'; import { useGetPrices } from '@/hooks/api/use-get-prices'; import { Transaction, useTransaction } from '@/hooks/transaction'; import { @@ -21,7 +22,9 @@ import { import { StrategyData } from '../../hooks/use-get-strategies'; import { useGetStrategyAvailableAmounts } from '../../hooks/use-get-strategy-available-amounts'; import { StrategyPositionData } from '../../hooks/use-get-strategy-position'; +import { useGetStrategyProxyAccount } from '../../hooks/use-get-strategy-proxy-account'; import { StrategyFormType } from '../../types'; +import { StrategyFormTransactionFees } from './StrategyFormTransactionFees'; type StrategyDepositFormProps = { strategy: StrategyData; @@ -31,16 +34,29 @@ type StrategyDepositFormProps = { const StrategyDepositForm = ({ strategy, position }: StrategyDepositFormProps): JSX.Element => { const { t } = useTranslation(); const prices = useGetPrices(); - const transaction = useTransaction(Transaction.STRATEGIES_DEPOSIT, { + const transaction = useTransaction({ onSuccess: () => { - form.resetForm(); + if (proxyAccount) { + form.resetForm(); + } else { + refetchProxyAccount(); + } } }); + const { getAvailableBalance } = useGetBalances(); + + const { + isLoading: isLoadingProxyAccount, + account: proxyAccount, + isIdentitySet, + refetch: refetchProxyAccount + } = useGetStrategyProxyAccount(strategy.type); + const { data: { maxAmount, minAmount } - } = useGetStrategyAvailableAmounts(StrategyFormType.DEPOSIT, strategy, position); + } = useGetStrategyAvailableAmounts(StrategyFormType.DEPOSIT, strategy, proxyAccount, position); - const getTransactionArgs = useCallback( + const getDepositTransactionArgs = useCallback( (values: StrategyDepositFormData) => { const amount = values[STRATEGY_DEPOSIT_AMOUNT_FIELD] || 0; const monetaryAmount = newMonetaryAmount(amount, strategy.currency, true); @@ -51,13 +67,28 @@ const StrategyDepositForm = ({ strategy, position }: StrategyDepositFormProps): ); const handleSubmit = (values: StrategyDepositFormData) => { - const transactionData = getTransactionArgs(values); - - if (!transactionData) return; - - const { monetaryAmount } = transactionData; - - transaction.execute(WRAPPED_TOKEN, monetaryAmount); + if (proxyAccount) { + const depositTransactionData = getDepositTransactionArgs(values); + + if (!depositTransactionData) return; + + let { monetaryAmount } = depositTransactionData; + + if (transaction.fee.isEqualFeeCurrency(monetaryAmount.currency)) { + monetaryAmount = transaction.calculateAmountWithFeeDeducted(monetaryAmount); + } + + transaction.execute( + Transaction.STRATEGIES_DEPOSIT, + strategy.type, + proxyAccount, + !!isIdentitySet, + WRAPPED_TOKEN, + monetaryAmount + ); + } else { + transaction.execute(Transaction.STRATEGIES_INITIALIZE_PROXY, strategy.type); + } }; const form = useForm({ @@ -65,16 +96,36 @@ const StrategyDepositForm = ({ strategy, position }: StrategyDepositFormProps): [STRATEGY_DEPOSIT_AMOUNT_FIELD]: '', [STRATEGY_DEPOSIT_FEE_TOKEN_FIELD]: transaction.fee.defaultCurrency.ticker }, - validationSchema: strategyDepositSchema('deposit', { maxAmount, minAmount }), + validationSchema: strategyDepositSchema( + 'deposit', + { + maxAmount, + minAmount, + requireProxyDeposit: !isIdentitySet, + governanceBalance: getAvailableBalance(GOVERNANCE_TOKEN.ticker) || newMonetaryAmount(0, GOVERNANCE_TOKEN) + }, + t + ), onSubmit: handleSubmit, onComplete: (values: StrategyDepositFormData) => { - const transactionData = getTransactionArgs(values); - - if (!transactionData) return; - - const { monetaryAmount } = transactionData; - - transaction.fee.estimate(WRAPPED_TOKEN, monetaryAmount); + if (proxyAccount) { + const depositTransactionData = getDepositTransactionArgs(values); + + if (!depositTransactionData) return; + + const { monetaryAmount } = depositTransactionData; + + transaction.fee.estimate( + Transaction.STRATEGIES_DEPOSIT, + strategy.type, + proxyAccount, + !!isIdentitySet, + WRAPPED_TOKEN, + monetaryAmount + ); + } else { + transaction.fee.estimate(Transaction.STRATEGIES_INITIALIZE_PROXY, strategy.type); + } } }); @@ -84,7 +135,7 @@ const StrategyDepositForm = ({ strategy, position }: StrategyDepositFormProps): true ); const inputUSDValue = convertMonetaryAmountToValueInUSD(inputMonetaryAmount, prices?.[WRAPPED_TOKEN_SYMBOL].usd) || 0; - const isSubmitButtonDisabled = isFormDisabled(form); + const isSubmitButtonDisabled = isFormDisabled(form) || isLoadingProxyAccount; return (
@@ -99,12 +150,13 @@ const StrategyDepositForm = ({ strategy, position }: StrategyDepositFormProps): {...mergeProps(form.getFieldProps(STRATEGY_DEPOSIT_AMOUNT_FIELD))} /> - - {t('deposit')} + {proxyAccount ? t('deposit') : t('strategy.initialize')} diff --git a/src/pages/Strategies/components/StrategyForm/StrategyFormTransactionFees.tsx b/src/pages/Strategies/components/StrategyForm/StrategyFormTransactionFees.tsx new file mode 100644 index 0000000000..44f08a82b7 --- /dev/null +++ b/src/pages/Strategies/components/StrategyForm/StrategyFormTransactionFees.tsx @@ -0,0 +1,108 @@ +import { mergeProps, useId } from '@react-aria/utils'; +import { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { displayMonetaryAmountInUSDFormat, formatUSD } from '@/common/utils/utils'; +import { Alert, Flex } from '@/component-library'; +import { + TransactionDetails, + TransactionDetailsDd, + TransactionDetailsDt, + TransactionDetailsGroup, + TransactionDetailsProps, + TransactionSelectToken, + TransactionSelectTokenProps +} from '@/components'; +import { GOVERNANCE_TOKEN } from '@/config/relay-chains'; +import { useGetPrices } from '@/hooks/api/use-get-prices'; +import { UseFeeEstimateResult } from '@/hooks/transaction/types/hook'; +import { SelectCurrencyFilter, useSelectCurrency } from '@/hooks/use-select-currency'; +import { PROXY_ACCOUNT_RESERVE_AMOUNT } from '@/utils/constants/account'; +import { getTokenPrice } from '@/utils/helpers/prices'; + +type Props = { + label?: ReactNode; + tooltipLabel?: ReactNode; + selectProps?: TransactionSelectTokenProps; + fee?: UseFeeEstimateResult; + includeProxyAccountFee?: boolean; +}; + +type InheritAttrs = Omit; + +type StrategyFormTransactionFeesProps = Props & InheritAttrs; + +const StrategyFormTransactionFees = ({ + selectProps, + label, + tooltipLabel, + className, + fee, + includeProxyAccountFee, + ...props +}: StrategyFormTransactionFeesProps): JSX.Element => { + const { t } = useTranslation(); + const id = useId(); + const prices = useGetPrices(); + const selectCurrency = useSelectCurrency(SelectCurrencyFilter.TRADEABLE_FOR_NATIVE_CURRENCY); + + const { selectProps: feeSelectProps, data } = fee || {}; + const { amount, isValid } = data || {}; + + const amountLabel = amount + ? `${amount.toHuman()} ${amount.currency.ticker} (${displayMonetaryAmountInUSDFormat( + amount, + getTokenPrice(prices, amount.currency.ticker)?.usd + )})` + : `${0.0} ${selectProps?.value} (${formatUSD(0)})`; + + const proxyDepositAmonut = includeProxyAccountFee + ? `${PROXY_ACCOUNT_RESERVE_AMOUNT.toHuman()} ${ + PROXY_ACCOUNT_RESERVE_AMOUNT.currency.ticker + } (${displayMonetaryAmountInUSDFormat( + PROXY_ACCOUNT_RESERVE_AMOUNT, + getTokenPrice(prices, PROXY_ACCOUNT_RESERVE_AMOUNT.currency.ticker)?.usd + )})` + : `${0.0} ${selectProps?.value} (${formatUSD(0)})`; + + const errorMessage = + isValid === false && t('forms.ensure_adequate_amount_left_to_cover_action', { action: t('fees').toLowerCase() }); + + return ( + + + {selectProps && ( + + )} + + {label || t('tx_fees')} + {amountLabel} + + {includeProxyAccountFee && ( + + + {label || t('strategies.proxy_deposit', { currency: GOVERNANCE_TOKEN.ticker })} + + {proxyDepositAmonut} + + )} + + {errorMessage && ( + + {errorMessage} + + )} + + ); +}; + +export { StrategyFormTransactionFees }; +export type { StrategyFormTransactionFeesProps }; diff --git a/src/pages/Strategies/components/StrategyForm/StrategyWithdrawalForm.tsx b/src/pages/Strategies/components/StrategyForm/StrategyWithdrawalForm.tsx index 75929e4f28..e617ac726e 100644 --- a/src/pages/Strategies/components/StrategyForm/StrategyWithdrawalForm.tsx +++ b/src/pages/Strategies/components/StrategyForm/StrategyWithdrawalForm.tsx @@ -20,6 +20,7 @@ import { import { StrategyData } from '../../hooks/use-get-strategies'; import { useGetStrategyAvailableAmounts } from '../../hooks/use-get-strategy-available-amounts'; import { StrategyPositionData } from '../../hooks/use-get-strategy-position'; +import { useGetStrategyProxyAccount } from '../../hooks/use-get-strategy-proxy-account'; import { StrategyFormType } from '../../types'; type StrategyWithdrawalFormProps = { @@ -32,15 +33,26 @@ const StrategyWithdrawalForm = ({ strategy, position }: StrategyWithdrawalFormPr const prices = useGetPrices(); const transaction = useTransaction({ onSuccess: () => { - form.resetForm(); + if (proxyAccount) { + form.resetForm(); + } else { + refetchProxyAccount(); + } } }); + + const { + isLoading: isLoadingProxyAccount, + account: proxyAccount, + refetch: refetchProxyAccount + } = useGetStrategyProxyAccount(strategy.type); + const { data: { maxAmount, minAmount }, isMaxAmount - } = useGetStrategyAvailableAmounts(StrategyFormType.WITHDRAW, strategy, position); + } = useGetStrategyAvailableAmounts(StrategyFormType.WITHDRAW, strategy, proxyAccount, position); - const getTransactionArgs = useCallback( + const getWithdrawalTransactionArgs = useCallback( (values: StrategyWithdrawFormData) => { const amount = values[STRATEGY_WITHDRAW_AMOUNT_FIELD] || 0; const monetaryAmount = newMonetaryAmount(amount, strategy.currency, true); @@ -51,18 +63,38 @@ const StrategyWithdrawalForm = ({ strategy, position }: StrategyWithdrawalFormPr ); const handleSubmit = (values: StrategyWithdrawFormData) => { - const transactionData = getTransactionArgs(values); + if (proxyAccount) { + const transactionData = getWithdrawalTransactionArgs(values); - if (!transactionData) return; + if (!transactionData) return; - const { monetaryAmount } = transactionData; + let { monetaryAmount } = transactionData; - const isWithdrawAll = isMaxAmount(monetaryAmount); + if (transaction.fee.isEqualFeeCurrency(monetaryAmount.currency)) { + monetaryAmount = transaction.calculateAmountWithFeeDeducted(monetaryAmount); + } - if (isWithdrawAll) { - return transaction.execute(Transaction.STRATEGIES_ALL_WITHDRAW, monetaryAmount.currency); + const isWithdrawAll = isMaxAmount(monetaryAmount); + + if (isWithdrawAll) { + return transaction.execute( + Transaction.STRATEGIES_ALL_WITHDRAW, + strategy.type, + proxyAccount, + monetaryAmount.currency, + monetaryAmount + ); + } else { + return transaction.execute( + Transaction.STRATEGIES_WITHDRAW, + strategy.type, + proxyAccount, + monetaryAmount.currency, + monetaryAmount + ); + } } else { - return transaction.execute(Transaction.STRATEGIES_WITHDRAW, monetaryAmount.currency, monetaryAmount); + transaction.execute(Transaction.STRATEGIES_INITIALIZE_PROXY, strategy.type); } }; @@ -77,18 +109,34 @@ const StrategyWithdrawalForm = ({ strategy, position }: StrategyWithdrawalFormPr }), onSubmit: handleSubmit, onComplete: (values: StrategyWithdrawFormData) => { - const transactionData = getTransactionArgs(values); - - if (!transactionData) return; - - const { monetaryAmount } = transactionData; - - const isWithdrawAll = isMaxAmount(monetaryAmount); - - if (isWithdrawAll) { - return transaction.fee.estimate(Transaction.STRATEGIES_ALL_WITHDRAW, monetaryAmount.currency); + if (proxyAccount) { + const transactionData = getWithdrawalTransactionArgs(values); + + if (!transactionData) return; + + const { monetaryAmount } = transactionData; + + const isWithdrawAll = isMaxAmount(monetaryAmount); + + if (isWithdrawAll) { + return transaction.fee.estimate( + Transaction.STRATEGIES_ALL_WITHDRAW, + strategy.type, + proxyAccount, + monetaryAmount.currency, + monetaryAmount + ); + } else { + return transaction.fee.estimate( + Transaction.STRATEGIES_WITHDRAW, + strategy.type, + proxyAccount, + monetaryAmount.currency, + monetaryAmount + ); + } } else { - return transaction.fee.estimate(Transaction.STRATEGIES_WITHDRAW, monetaryAmount.currency, monetaryAmount); + transaction.fee.estimate(Transaction.STRATEGIES_INITIALIZE_PROXY, strategy.type); } } }); @@ -99,7 +147,7 @@ const StrategyWithdrawalForm = ({ strategy, position }: StrategyWithdrawalFormPr true ); const inputUSDValue = convertMonetaryAmountToValueInUSD(inputMonetaryAmount, prices?.[WRAPPED_TOKEN_SYMBOL].usd) || 0; - const isSubmitButtonDisabled = isFormDisabled(form); + const isSubmitButtonDisabled = isFormDisabled(form) || isLoadingProxyAccount; return ( @@ -120,7 +168,7 @@ const StrategyWithdrawalForm = ({ strategy, position }: StrategyWithdrawalFormPr selectProps={{ ...form.getSelectFieldProps(STRATEGY_WITHDRAW_FEE_TOKEN_FIELD) }} /> - {t('withdraw')} + {proxyAccount ? t('withdraw') : t('strategy.initialize')} diff --git a/src/pages/Strategies/hooks/use-get-strategy-available-amounts.ts b/src/pages/Strategies/hooks/use-get-strategy-available-amounts.ts index 0df60edb21..792881ca42 100644 --- a/src/pages/Strategies/hooks/use-get-strategy-available-amounts.ts +++ b/src/pages/Strategies/hooks/use-get-strategy-available-amounts.ts @@ -1,3 +1,5 @@ +import { AccountId } from '@polkadot/types/interfaces'; + import { useGetLoanAvailableAmounts, UseGetLoanAvailableAmountsResult @@ -12,12 +14,14 @@ type useGetStrategyAvailableAmountsResult = UseGetLoanAvailableAmountsResult; const useGetStrategyAvailableAmounts = ( type: StrategyFormType, strategy: StrategyData, + proxyAccount: AccountId | undefined, position?: StrategyPositionData ): useGetStrategyAvailableAmountsResult => { const loanAvailableAmounts = useGetLoanAvailableAmounts( type === StrategyFormType.DEPOSIT ? 'lend' : 'withdraw', strategy.loanAsset, - position?.loanPosition + position?.loanPosition, + proxyAccount ); switch (strategy.type) { diff --git a/src/pages/Strategies/hooks/use-get-strategy-position.ts b/src/pages/Strategies/hooks/use-get-strategy-position.ts index 88dc11bbe2..734b4f0180 100644 --- a/src/pages/Strategies/hooks/use-get-strategy-position.ts +++ b/src/pages/Strategies/hooks/use-get-strategy-position.ts @@ -1,5 +1,6 @@ import { CurrencyExt } from '@interlay/interbtc-api'; import { MonetaryAmount } from '@interlay/monetary-js'; +import { AccountId } from '@polkadot/types/interfaces'; import { useGetAccountPositions } from '@/hooks/api/loans/use-get-account-positions'; import { CollateralPosition } from '@/types/loans'; @@ -18,8 +19,11 @@ type UseGetStrategyPositionResult = { data: StrategyPositionData | undefined; }; -const useGetStrategyPosition = (strategy: StrategyData | undefined): UseGetStrategyPositionResult => { - const { getLendPosition, isLoading: isAccountPositionsLoading } = useGetAccountPositions(); +const useGetStrategyPosition = ( + strategy: StrategyData | undefined, + proxyAccount: AccountId | undefined +): UseGetStrategyPositionResult => { + const { getLendPosition, isLoading: isAccountPositionsLoading } = useGetAccountPositions(proxyAccount); if (!strategy) { return { diff --git a/src/pages/Strategies/hooks/use-get-strategy-proxy-account.ts b/src/pages/Strategies/hooks/use-get-strategy-proxy-account.ts new file mode 100644 index 0000000000..3ffae84c6b --- /dev/null +++ b/src/pages/Strategies/hooks/use-get-strategy-proxy-account.ts @@ -0,0 +1,92 @@ +import { storageKeyToNthInner } from '@interlay/interbtc-api'; +import { AccountId } from '@polkadot/types/interfaces'; +import { useErrorHandler } from 'react-error-boundary'; +import { useQuery } from 'react-query'; + +import useAccountId from '@/hooks/use-account-id'; + +import { StrategyType } from '../types'; + +interface UseGetStrategyProxyAccountResult { + account: AccountId | undefined; + isIdentitySet: boolean | undefined; + isLoading: boolean; + refetch: () => void; +} + +const getProxyIdentities = (proxyAccounts: Array) => + new Promise>((resolve) => + window.bridge.api.query.identity.identityOf.multi(proxyAccounts, (identities) => { + const accountIdentities = identities.map((identity) => { + if (identity.isNone) { + return undefined; + } + return identity.unwrap().info.additional[0][1].asRaw.toHuman() as StrategyType; + }); + + const accountsWithStrategies = proxyAccounts.map( + (account, index) => [account, accountIdentities[index]] as [AccountId, StrategyType | undefined] + ); + + resolve(accountsWithStrategies); + }) + ); + +const getStrategyProxyAccount = async ( + strategyType: StrategyType, + primaryAccount: AccountId | undefined +): Promise<{ account: AccountId; isIdentitySet: boolean } | undefined> => { + if (!primaryAccount) { + return undefined; + } + + // MEMO: Not possible to query proxy accounts by delegate, + // therefore all are fetched and then filtered. + + const allProxies = await window.bridge.api.query.proxy.proxies.entries(); + const proxiesOfUserAccount = allProxies + .filter((proxy) => proxy[1][0][0].delegate.toString() === primaryAccount.toString()) + .map((proxy) => storageKeyToNthInner(proxy[0])); + + if (proxiesOfUserAccount.length === 0) { + return undefined; + } + + const accountToStrategy = await getProxyIdentities(proxiesOfUserAccount); + + const strategyAccount = accountToStrategy.find( + ([, accountStrategyType]) => accountStrategyType === strategyType + )?.[0]; + + if (strategyAccount) { + return { account: strategyAccount, isIdentitySet: true }; + } + // If strategy is not connected with any proxy account return first unused proxy account + // that will be assigned with first strategy deposit. + + const firstUnusedProxyAccount = accountToStrategy.find( + ([, accountStrategyType]) => accountStrategyType === undefined + )?.[0]; + + if (!firstUnusedProxyAccount) { + throw new Error('Proxy account limit was exceeded.'); + } + + return { account: firstUnusedProxyAccount, isIdentitySet: false }; +}; + +const useGetStrategyProxyAccount = (strategyType: StrategyType): UseGetStrategyProxyAccountResult => { + const primaryAccount = useAccountId(); + + const { data, error, refetch, isLoading } = useQuery({ + queryKey: ['strategy-proxy-account', strategyType, primaryAccount?.toString()], + queryFn: () => getStrategyProxyAccount(strategyType, primaryAccount), + enabled: !!primaryAccount + }); + + useErrorHandler(error); + + return { account: data?.account, isIdentitySet: data?.isIdentitySet, isLoading, refetch }; +}; + +export { useGetStrategyProxyAccount }; diff --git a/src/pages/Vaults/Vault/components/IssueRedeemForm/IssueRedeemForm.tsx b/src/pages/Vaults/Vault/components/IssueRedeemForm/IssueRedeemForm.tsx index f78fce7bf3..e2af9bab22 100644 --- a/src/pages/Vaults/Vault/components/IssueRedeemForm/IssueRedeemForm.tsx +++ b/src/pages/Vaults/Vault/components/IssueRedeemForm/IssueRedeemForm.tsx @@ -165,10 +165,7 @@ const IssueRedeemForm = ({ const label = isIssueModal ? 'Issue amount' : 'Reddem amount'; const highlightTerm = isIssueModal ? 'Maximum vault capacity:' : 'Locked:'; - const handleSubmit = (data: IssueRedeemFormData) => { - onSubmit?.(); - console.log(data); - }; + const handleSubmit = () => onSubmit?.(); const parsedBTCAmount = new BitcoinAmount(inputBTCAmount); const bridgeFee = parsedBTCAmount.mul(issueFeeRate); diff --git a/src/services/fetchers/cumulative-vault-collateral-volumes-fetcher.ts b/src/services/fetchers/cumulative-vault-collateral-volumes-fetcher.ts new file mode 100644 index 0000000000..23f2b31010 --- /dev/null +++ b/src/services/fetchers/cumulative-vault-collateral-volumes-fetcher.ts @@ -0,0 +1,80 @@ +import { CollateralCurrencyExt, CurrencyExt, newMonetaryAmount, TickerToData } from '@interlay/interbtc-api'; + +import { WRAPPED_TOKEN } from '@/config/relay-chains'; +import graphqlFetcher, { GRAPHQL_FETCHER } from '@/services/fetchers/graphql-fetcher'; +import { getCurrencyEqualityCondition } from '@/utils/helpers/currencies'; + +import { VolumeDataPoint } from './cumulative-volumes-fetcher'; + +const CUMULATIVE_VAULT_COLLATERALVOLUMES_FETCHER = 'cumulative-vault-collateral-volumes-fetcher'; + +const cumulativeVaultCollateralVolumesFetcher = async ( + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + { queryKey }: any +): Promise>> => { + const [key, cutoffTimestamps, collateralCurrencies] = queryKey as CumulativeVaultCollateralVolumesFetcherParams; + + if (key !== CUMULATIVE_VAULT_COLLATERALVOLUMES_FETCHER) throw new Error('Invalid key!'); + + const queryFragment = (date: Date, collateralCurrency: CurrencyExt) => { + const where = `{ + tillTimestamp_lte: "${date.toISOString()}", + type_eq: Collateral, + ${`collateralCurrency: { + ${getCurrencyEqualityCondition(collateralCurrency)}}`}, + ${`wrappedCurrency: { + ${getCurrencyEqualityCondition(WRAPPED_TOKEN)}}`}, + }`; + + return ` + ts${date.getTime()}_${ + collateralCurrency?.ticker + }: cumulativeVolumePerCurrencyPairs (where: ${where}, orderBy: tillTimestamp_DESC, limit: 1) { + amount + tillTimestamp + } + `; + }; + + const query = ` + { + ${cutoffTimestamps.map((date) => collateralCurrencies.map((currency) => queryFragment(date, currency)))} + } + `; + + // TODO: should type properly (`Relay`) + const volumesData = await graphqlFetcher>()({ + queryKey: [GRAPHQL_FETCHER, query] + }); + + const volumes = volumesData?.data || {}; + + return Object.entries(volumes).reduce((result, [key, [volumeData]]) => { + const [rawTimestamp, ticker] = key.split('_'); + const timestamp = rawTimestamp.slice(2); + const currency = collateralCurrencies.find((collateralCurrency) => collateralCurrency.ticker === ticker); + if (currency === undefined) { + throw new Error('Ivalid query.'); + } + return { + ...result, + [ticker]: [ + ...(result[ticker] || []), + { + amount: newMonetaryAmount(volumeData.amount || 0, currency), + tillTimestamp: cutoffTimestamps.find((cutoffTimestamp) => cutoffTimestamp.getTime().toString() === timestamp) + } + ] + }; + }, {} as any); +}; + +type CumulativeVaultCollateralVolumesFetcherParams = [ + queryKey: string, + cutoffTimestamp: Array, + collateralCurrency: Array +]; + +export { CUMULATIVE_VAULT_COLLATERALVOLUMES_FETCHER }; + +export default cumulativeVaultCollateralVolumesFetcher; diff --git a/src/services/fetchers/cumulative-volumes-fetcher.ts b/src/services/fetchers/cumulative-volumes-fetcher.ts index 10609caa9b..5589771170 100644 --- a/src/services/fetchers/cumulative-volumes-fetcher.ts +++ b/src/services/fetchers/cumulative-volumes-fetcher.ts @@ -2,6 +2,7 @@ import { CollateralCurrencyExt, CurrencyExt, newMonetaryAmount, WrappedCurrency import { MonetaryAmount } from '@interlay/monetary-js'; import graphqlFetcher, { GRAPHQL_FETCHER } from '@/services/fetchers/graphql-fetcher'; +import { getCurrencyEqualityCondition } from '@/utils/helpers/currencies'; const CUMULATIVE_VOLUMES_FETCHER = 'cumulative-volumes-fetcher'; @@ -35,41 +36,26 @@ const cumulativeVolumesFetcher = async ( const queryFragment = ( type: VolumeType, date: Date, - collateralCurrencyIdLiteral?: string | number, - wrappedCurrencyIdLiteral?: string | number + collateralCurrency?: CurrencyExt, + wrappedCurrency?: CurrencyExt ) => { - let colCurrCond = ''; - if (collateralCurrencyIdLiteral !== undefined) { - colCurrCond = - (typeof collateralCurrencyIdLiteral === 'number' ? 'asset_eq: ' : 'token_eq: ') + - collateralCurrencyIdLiteral.toString(); - } - let wrapCurrCond = ''; - if (wrappedCurrencyIdLiteral !== undefined) { - wrapCurrCond = - (typeof wrappedCurrencyIdLiteral === 'number' ? 'asset_eq: ' : 'token_eq: ') + - wrappedCurrencyIdLiteral.toString(); - } const where = `{ tillTimestamp_lte: "${date.toISOString()}", type_eq: ${type}, ${ - collateralCurrencyIdLiteral + collateralCurrency ? `collateralCurrency: { - ${colCurrCond}}` + ${getCurrencyEqualityCondition(collateralCurrency)}}` : `` }, ${ - wrappedCurrencyIdLiteral + wrappedCurrency ? `wrappedCurrency: { - ${wrapCurrCond}}` + ${getCurrencyEqualityCondition(wrappedCurrency)}}` : `` }, }`; - const entityName = - collateralCurrencyIdLiteral || wrappedCurrencyIdLiteral - ? `cumulativeVolumePerCurrencyPairs` - : `cumulativeVolumes`; + const entityName = collateralCurrency || wrappedCurrency ? `cumulativeVolumePerCurrencyPairs` : `cumulativeVolumes`; return ` ts${date.getTime()}: ${entityName} (where: ${where}, orderBy: tillTimestamp_DESC, limit: 1) { amount @@ -80,14 +66,7 @@ const cumulativeVolumesFetcher = async ( const query = ` { - ${cutoffTimestamps.map((date) => { - let col; - // TODO: Need to refactor when we want to support lend tokens as collateral for vaults. - if (collateralCurrency) { - col = 'foreignAsset' in collateralCurrency ? collateralCurrency.foreignAsset.id : collateralCurrency.ticker; - } - return queryFragment(type, date, col, wrappedCurrency?.ticker); - })} + ${cutoffTimestamps.map((date) => queryFragment(type, date, collateralCurrency, wrappedCurrency))} } `; diff --git a/src/test/mocks/@interlay/interbtc-api/parachain/system.ts b/src/test/mocks/@interlay/interbtc-api/parachain/system.ts index 457501ec8f..66a3c8c7db 100644 --- a/src/test/mocks/@interlay/interbtc-api/parachain/system.ts +++ b/src/test/mocks/@interlay/interbtc-api/parachain/system.ts @@ -22,7 +22,6 @@ const MODULE: Record> = { getCurrentActiveBlockNumber: jest.fn().mockResolvedValue(CURRENT_ACTIVE_BLOCK_NUMBER), getCurrentBlockNumber: jest.fn().mockResolvedValue(CURRENT_BLOCK_NUMBER), getFutureBlockNumber: jest.fn().mockResolvedValue(FUTURE_BLOCK_NUMBER), - getStatusCode: jest.fn().mockResolvedValue(STATUS_CODE), getBlockHash: jest.fn(), setCode: jest.fn(), subscribeToCurrentBlockHeads: jest.fn(), diff --git a/src/test/pages/Pools.test.tsx b/src/test/pages/Pools.test.tsx index 9280f99d9e..a7b4808b6f 100644 --- a/src/test/pages/Pools.test.tsx +++ b/src/test/pages/Pools.test.tsx @@ -206,52 +206,6 @@ describe('Pools Page', () => { getClaimableFarmingRewards.mockReturnValue(CLAIMABLE_REWARDS); }); - it('should be able to enter customisable input amounts mode', async () => { - jest - .spyOn(LIQUIDITY_POOLS.ONE, 'getLiquidityDepositInputAmounts') - .mockReturnValue(LIQUIDITY_POOLS.ONE.pooledCurrencies); - - const [DEFAULT_CURRENCY_1, DEFAULT_CURRENCY_2] = LIQUIDITY_POOLS.ONE.pooledCurrencies; - - await render(, { path }); - - const tabPanel = await withinModalTabPanel(TABLES.AVAILABLE_POOLS, LP_TOKEN_A.ticker, TABS.DEPOSIT); - - await userEvent.type( - tabPanel.getByRole('textbox', { - name: new RegExp(`${DEFAULT_CURRENCY_1.currency.ticker} deposit amount`, 'i') - }), - DEFAULT_CURRENCY_1.toString(), - { delay: 1 } - ); - - expect(LIQUIDITY_POOLS.ONE.getLiquidityDepositInputAmounts).toHaveBeenCalledWith(DEFAULT_CURRENCY_1); - - await waitFor(() => { - expect( - tabPanel.getByRole('textbox', { - name: new RegExp(`${DEFAULT_CURRENCY_2.currency.ticker} deposit amount`, 'i') - }) - ).toHaveValue(DEFAULT_CURRENCY_2.toString()); - }); - - await userEvent.type( - tabPanel.getByRole('textbox', { - name: new RegExp(`${DEFAULT_CURRENCY_2.currency.ticker} deposit amount`, 'i') - }), - '10', - { delay: 1 } - ); - - await waitFor(() => { - expect( - tabPanel.getByRole('textbox', { - name: new RegExp(`${DEFAULT_CURRENCY_1.currency.ticker} deposit amount`, 'i') - }) - ).toHaveValue(DEFAULT_CURRENCY_1.toString()); - }); - }); - it('should render illiquid pool and deposit with custom ratio', async () => { jest .spyOn(LIQUIDITY_POOLS.EMPTY, 'getLiquidityDepositInputAmounts') diff --git a/src/utils/constants/account.ts b/src/utils/constants/account.ts index 467df8b303..a9c88d129d 100644 --- a/src/utils/constants/account.ts +++ b/src/utils/constants/account.ts @@ -1,5 +1,11 @@ -import { APP_NAME } from '@/config/relay-chains'; +import { MonetaryAmount } from '@interlay/monetary-js'; + +import { APP_NAME, GOVERNANCE_TOKEN } from '@/config/relay-chains'; const SELECTED_ACCOUNT_LOCAL_STORAGE_KEY = `${APP_NAME}-selected-account`; -export { SELECTED_ACCOUNT_LOCAL_STORAGE_KEY }; +const DEFAULT_PROXY_ACCOUNT_AMOUNT = 10; + +const PROXY_ACCOUNT_RESERVE_AMOUNT = new MonetaryAmount(GOVERNANCE_TOKEN, 26); + +export { DEFAULT_PROXY_ACCOUNT_AMOUNT, PROXY_ACCOUNT_RESERVE_AMOUNT, SELECTED_ACCOUNT_LOCAL_STORAGE_KEY }; diff --git a/src/utils/helpers/extrinsic.ts b/src/utils/helpers/extrinsic.ts new file mode 100644 index 0000000000..acc58dfe86 --- /dev/null +++ b/src/utils/helpers/extrinsic.ts @@ -0,0 +1,9 @@ +import { SubmittableExtrinsic } from '@polkadot/api/types'; +import { AccountId } from '@polkadot/types/interfaces'; + +const proxifyExtrinsic = ( + proxyAccount: AccountId, + extrinsic: SubmittableExtrinsic<'promise'> +): SubmittableExtrinsic<'promise'> => window.bridge.api.tx.proxy.proxy(proxyAccount, 'Any', extrinsic); + +export { proxifyExtrinsic }; diff --git a/src/utils/hooks/api/system/use-get-parachain-status.tsx b/src/utils/hooks/api/system/use-get-parachain-status.tsx deleted file mode 100644 index 9a3f54fe6c..0000000000 --- a/src/utils/hooks/api/system/use-get-parachain-status.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useErrorHandler } from 'react-error-boundary'; -import { useQuery, UseQueryResult } from 'react-query'; - -import { REFETCH_INTERVAL } from '@/utils/constants/api'; - -type ParachainStatusData = { - isRunning: boolean; - isShutdown: boolean; - isError: boolean; -}; - -type UseGetParachainStatusResult = UseQueryResult; - -const getStatus = async (): Promise => { - const statusCode = await window.bridge.system.getStatusCode(); - - return { - isRunning: Boolean(statusCode.isRunning), - isError: Boolean(statusCode.isError), - isShutdown: Boolean(statusCode.isShutdown) - }; -}; - -const useGetParachainStatus = (): UseGetParachainStatusResult => { - const queryResult = useQuery({ - queryKey: 'parachain-status', - queryFn: getStatus, - refetchInterval: REFETCH_INTERVAL.MINUTE - }); - - useErrorHandler(queryResult.error); - - return queryResult; -}; - -export { useGetParachainStatus }; -export type { ParachainStatusData, UseGetParachainStatusResult }; diff --git a/yarn.lock b/yarn.lock index 148843ae1e..3d55a76db9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2125,13 +2125,13 @@ dependencies: axios "^0.21.1" -"@interlay/interbtc-api@2.3.7": - version "2.3.7" - resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-2.3.7.tgz#26d4fa574531fe9eea3f0d49364f7476da9713cf" - integrity sha512-w9xPaUa3wTTXOb83pHbSqlE3E8V2iA4WE4IlOu23Zqth4hnG0h819WynlfzUsAPGug6RkZkHWIKnu+85V95g5A== +"@interlay/interbtc-api@2.4.3": + version "2.4.3" + resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-2.4.3.tgz#1b1b953d792a168f7a3d20014c8255b78cf08119" + integrity sha512-0j8sPekmyhf6m8Qa4gnYndUx5Uu8XB+4E0nxhrUsVN62zSNrxSu8Ubn6sjlwOXUmgqB1BJRZe/PV3hj2OiWgUw== dependencies: "@interlay/esplora-btc-api" "0.4.0" - "@interlay/interbtc-types" "1.12.0" + "@interlay/interbtc-types" "1.13.0" "@interlay/monetary-js" "0.7.3" "@polkadot/api" "9.14.2" big.js "6.1.1" @@ -2147,10 +2147,10 @@ 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.13.0": + version "1.13.0" + resolved "https://registry.yarnpkg.com/@interlay/interbtc-types/-/interbtc-types-1.13.0.tgz#0e09badf10861b4c8a5087b4358da317e464a14a" + integrity sha512-oUjavcfnX7lxlMd10qGc48/MoATX37TQcuSAZBIUmpCRiJ15hZbQoTAKGgWMPsla3+3YqUAzkWUEVMwUvM1U+w== "@interlay/monetary-js@0.7.3": version "0.7.3" @@ -3474,6 +3474,19 @@ "@react-types/shared" "^3.16.0" "@swc/helpers" "^0.4.14" +"@react-aria/breadcrumbs@^3.5.3": + version "3.5.3" + resolved "https://registry.yarnpkg.com/@react-aria/breadcrumbs/-/breadcrumbs-3.5.3.tgz#05d4d811d7a665ccf6b0b411a2c0ab0f4fb4638e" + integrity sha512-rmkApAflZm7Finn3vxLGv7MbsMaPo5Bn7/lf8GBztNfzmLWP/dAA5bgvi1sj1T6sWJOuFJT8u04ImUwBCLh8cQ== + dependencies: + "@react-aria/i18n" "^3.8.0" + "@react-aria/interactions" "^3.16.0" + "@react-aria/link" "^3.5.2" + "@react-aria/utils" "^3.18.0" + "@react-types/breadcrumbs" "^3.6.0" + "@react-types/shared" "^3.18.1" + "@swc/helpers" "^0.5.0" + "@react-aria/button@^3.6.4": version "3.6.4" resolved "https://registry.yarnpkg.com/@react-aria/button/-/button-3.6.4.tgz#51927c9d968d0c1f741ee2081ca7f2e244abbc12" @@ -3905,6 +3918,18 @@ "@react-types/shared" "^3.17.0" "@swc/helpers" "^0.4.14" +"@react-aria/link@^3.5.2": + version "3.5.2" + resolved "https://registry.yarnpkg.com/@react-aria/link/-/link-3.5.2.tgz#68b99721eeddffb87c42541419f08333eada37d9" + integrity sha512-CCFP11Uietro6TUZpWBoq3Ql/6qss/ODC5XM6oNxckj72IHruFIj8V7Y0tL5x0aE6h38hlKcDf8NCxkQqz2edg== + dependencies: + "@react-aria/focus" "^3.13.0" + "@react-aria/interactions" "^3.16.0" + "@react-aria/utils" "^3.18.0" + "@react-types/link" "^3.4.3" + "@react-types/shared" "^3.18.1" + "@swc/helpers" "^0.5.0" + "@react-aria/listbox@^3.8.0": version "3.9.0" resolved "https://registry.yarnpkg.com/@react-aria/listbox/-/listbox-3.9.0.tgz#243d9a863d2592f003aa2c7604962e4db1d57dee" @@ -4680,6 +4705,14 @@ dependencies: "@react-types/shared" "^3.16.0" +"@react-types/breadcrumbs@^3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@react-types/breadcrumbs/-/breadcrumbs-3.6.0.tgz#6a5b5e459597172d7f23f2ecbc9e11c94d2a3f2a" + integrity sha512-EnZk/f59yMQUmH2DW21uo3ajQ7nLEZ/sIMSfEZYP69CFe1by0RKi9aFRjJSrYjxRC0PSHTVPTjIG72KeBSsUGA== + dependencies: + "@react-types/link" "^3.4.3" + "@react-types/shared" "^3.18.1" + "@react-types/button@^3.7.0": version "3.7.0" resolved "https://registry.yarnpkg.com/@react-types/button/-/button-3.7.0.tgz#774c043d8090a505e60fdf26f026d5f0cc968f0f" @@ -4766,6 +4799,14 @@ "@react-aria/interactions" "^3.14.0" "@react-types/shared" "^3.17.0" +"@react-types/link@^3.4.3": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@react-types/link/-/link-3.4.3.tgz#51534673ea35cf6583b950319bafd16ff76296dc" + integrity sha512-opKfkcaeV0cir64jPcy7DS0BrmdfuWMjua+MSeNv7FfT/b65rFgPfAOKZcvLWDsaxT5HYb7pivYPBfjKqHsQKw== + dependencies: + "@react-aria/interactions" "^3.16.0" + "@react-types/shared" "^3.18.1" + "@react-types/listbox@^3.4.0", "@react-types/listbox@^3.4.1": version "3.4.1" resolved "https://registry.yarnpkg.com/@react-types/listbox/-/listbox-3.4.1.tgz#3d9f5859ad4eb550a6c1c532042316b32e43b606"