diff --git a/package.json b/package.json index c166de9e4e..a2fa1b8a4b 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@react-stately/tooltip": "^3.2.3", "@react-stately/tree": "^3.4.1", "@tailwindcss/forms": "^0.3.2", + "@talismn/connect-wallets": "^1.2.3", "big.js": "^6.1.1", "chart.js": "^2.9.4", "clsx": "^1.1.1", diff --git a/src/App.tsx b/src/App.tsx index a12f6f1b7c..07538d652f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,6 +19,7 @@ import { PAGES } from '@/utils/constants/links'; import { Layout, TransactionModal } from './components'; import * as constants from './constants'; +import { useApi } from './hooks/api/use-api'; import { FeatureFlags, useFeatureFlag } from './hooks/use-feature-flag'; import TestnetBanner from './legacy-components/TestnetBanner'; @@ -41,6 +42,8 @@ const Actions = React.lazy(() => import(/* webpackChunkName: 'actions' */ '@/pag const NoMatch = React.lazy(() => import(/* webpackChunkName: 'no-match' */ '@/pages/NoMatch')); const App = (): JSX.Element => { + useApi(); + const { selectedAccount, extensions } = useSubstrateSecureState(); const { setSelectedAccount } = useSubstrate(); diff --git a/src/components/AuthModal/AuthModal.tsx b/src/components/AuthModal/AuthModal.tsx deleted file mode 100644 index 89d8eb0904..0000000000 --- a/src/components/AuthModal/AuthModal.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { InjectedAccountWithMeta, InjectedExtension } from '@polkadot/extension-inject/types'; -import { useEffect, useMemo, useState } from 'react'; -import { TFunction, useTranslation } from 'react-i18next'; - -import { CTA, Modal, ModalBody, ModalFooter, ModalHeader, ModalProps } from '@/component-library'; -import { useSubstrateSecureState } from '@/lib/substrate'; -import { WalletData } from '@/utils/constants/wallets'; -import { findWallet } from '@/utils/helpers/wallet'; - -import { AccountStep } from './AccountStep'; -import { Disclaimer } from './Disclaimer'; -import { AuthModalSteps } from './types'; -import { WalletStep } from './WalletStep'; - -const getTitle = (t: TFunction, step: AuthModalSteps, extensions: InjectedExtension[]) => { - if (step === AuthModalSteps.ACCOUNT) { - return t('account_modal.please_select_an_account'); - } - - if (!extensions.length) { - return t('account_modal.please_install_supported_wallet'); - } - - return t('account_modal.please_select_a_wallet'); -}; - -type Props = { - onAccountSelect?: (account: InjectedAccountWithMeta) => void; - onDisconnect?: () => void; -}; - -type InheritAttrs = Omit; - -type AuthModalProps = Props & InheritAttrs; - -const AuthModal = ({ onAccountSelect, onDisconnect, isOpen, ...props }: AuthModalProps): JSX.Element => { - const { t } = useTranslation(); - - const { extensions, selectedAccount, accounts } = useSubstrateSecureState(); - - const [step, setStep] = useState(AuthModalSteps.ACCOUNT); - const [wallet, setWallet] = useState(); - - useEffect(() => { - if (!isOpen) return; - - setStep(selectedAccount ? AuthModalSteps.ACCOUNT : AuthModalSteps.WALLET); - setWallet(undefined); - }, [isOpen, selectedAccount]); - - const handleWalletSelect = (wallet: WalletData) => { - setStep(AuthModalSteps.ACCOUNT); - setWallet(wallet); - }; - - const handleChangeWallet = () => { - setStep(AuthModalSteps.WALLET); - setWallet(undefined); - }; - - const handleAccountSelection = (account: InjectedAccountWithMeta) => onAccountSelect?.(account); - - const handleDisconnect = () => onDisconnect?.(); - - const currentWallet = useMemo(() => wallet || (selectedAccount && findWallet(selectedAccount?.meta.source)), [ - selectedAccount, - wallet - ]); - - const title = getTitle(t, step, extensions); - - const hasDisconnect = step === AuthModalSteps.ACCOUNT && selectedAccount; - - return ( - - {title} - - - - {currentWallet && ( - - )} - - {hasDisconnect && ( - - - {t('account_modal.disconnect')} - - - )} - - ); -}; - -export { AuthModal, AuthModalSteps }; -export type { AuthModalProps }; diff --git a/src/components/AuthModal/AccountStep.tsx b/src/components/WalletModalTrigger/AccountStep.tsx similarity index 79% rename from src/components/AuthModal/AccountStep.tsx rename to src/components/WalletModalTrigger/AccountStep.tsx index 7a28cfabbe..c6241db1d2 100644 --- a/src/components/AuthModal/AccountStep.tsx +++ b/src/components/WalletModalTrigger/AccountStep.tsx @@ -1,4 +1,3 @@ -import { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; import { mergeProps } from '@react-aria/utils'; import { useTranslation } from 'react-i18next'; import { useCopyToClipboard } from 'react-use'; @@ -7,14 +6,13 @@ import { DocumentDuplicate } from '@/assets/icons'; import { shortAddress } from '@/common/utils/utils'; import { CTA, Divider, Flex, P, Span, Tooltip, WalletIcon } from '@/component-library'; import { useCopyTooltip } from '@/hooks/use-copy-tooltip'; -import { KeyringPair } from '@/lib/substrate'; -import { WalletData } from '@/utils/constants/wallets'; +import { WalletAccountData, WalletData } from '@/lib/wallet/types'; import { StepComponentProps, withStep } from '@/utils/hocs/step'; import { StyledAccountItem, StyledCopyItem, StyledP } from './AuthModal.style'; import { AuthModalSteps } from './types'; -type CopyAddressProps = { account: InjectedAccountWithMeta }; +type CopyAddressProps = { account: WalletAccountData }; const CopyAddress = ({ account }: CopyAddressProps) => { const [, copy] = useCopyToClipboard(); @@ -26,7 +24,7 @@ const CopyAddress = ({ account }: CopyAddressProps) => { @@ -36,24 +34,22 @@ const CopyAddress = ({ account }: CopyAddressProps) => { }; type AccountStepProps = { - accounts: InjectedAccountWithMeta[]; + value?: WalletAccountData; + accounts: WalletAccountData[]; wallet: WalletData; - selectedAccount?: KeyringPair; onChangeWallet?: () => void; - onSelectionChange: (account: InjectedAccountWithMeta) => void; + onSelectionChange: (account: WalletAccountData) => void; } & StepComponentProps; const AccountComponent = ({ + value, accounts, wallet, - selectedAccount, onChangeWallet, onSelectionChange }: AccountStepProps): JSX.Element | null => { const { t } = useTranslation(); - const walletAccounts = accounts.filter(({ meta: { source } }) => source === wallet.extensionName); - return ( @@ -70,8 +66,8 @@ const AccountComponent = ({ - {walletAccounts.map((account) => { - const isSelected = selectedAccount?.address === account.address; + {accounts.map((account) => { + const isSelected = value?.address === account.address; return ( @@ -84,7 +80,7 @@ const AccountComponent = ({ isSelected={isSelected} > - {account.meta.name} + {account.name} ({shortAddress(account.address)}) diff --git a/src/components/AuthModal/AuthListItem.tsx b/src/components/WalletModalTrigger/AuthListItem.tsx similarity index 100% rename from src/components/AuthModal/AuthListItem.tsx rename to src/components/WalletModalTrigger/AuthListItem.tsx diff --git a/src/components/AuthModal/AuthModal.style.tsx b/src/components/WalletModalTrigger/AuthModal.style.tsx similarity index 85% rename from src/components/AuthModal/AuthModal.style.tsx rename to src/components/WalletModalTrigger/AuthModal.style.tsx index 2aba8d2568..3a830e2f70 100644 --- a/src/components/AuthModal/AuthModal.style.tsx +++ b/src/components/WalletModalTrigger/AuthModal.style.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; import { ArrowRight } from '@/assets/icons'; -import { Flex, P, theme } from '@/component-library'; +import { CTA, Flex, P, theme } from '@/component-library'; import { AuthListItem } from './AuthListItem'; @@ -47,4 +47,9 @@ const StyledP = styled(P)` color: ${({ $isSelected }) => $isSelected && theme.list.text}; `; -export { StyledAccountItem, StyledArrowRight, StyledCopyItem, StyledItem, StyledP, StyledWalletItem }; +const StyledCTA = styled(CTA)` + padding: ${theme.spacing.spacing3}; + border: ${theme.border.default}; +`; + +export { StyledAccountItem, StyledArrowRight, StyledCopyItem, StyledCTA, StyledItem, StyledP, StyledWalletItem }; diff --git a/src/components/AuthModal/AuthModal.test.tsx b/src/components/WalletModalTrigger/AuthModal.test.tsx similarity index 100% rename from src/components/AuthModal/AuthModal.test.tsx rename to src/components/WalletModalTrigger/AuthModal.test.tsx diff --git a/src/components/WalletModalTrigger/AuthModal.tsx b/src/components/WalletModalTrigger/AuthModal.tsx new file mode 100644 index 0000000000..1568491ea6 --- /dev/null +++ b/src/components/WalletModalTrigger/AuthModal.tsx @@ -0,0 +1,126 @@ +import { useCallback, useEffect, useState } from 'react'; +import { TFunction, useTranslation } from 'react-i18next'; + +import { CTA, Modal, ModalBody, ModalFooter, ModalHeader, ModalProps } from '@/component-library'; +import { useSignMessage } from '@/hooks/use-sign-message'; +import { WalletAccountData, WalletData } from '@/lib/wallet/types'; +import { useEnableWallet } from '@/lib/wallet/use-enable-wallet'; +import { useGetWalletAccounts } from '@/lib/wallet/use-get-wallet-accounts'; +import { useGetWallets } from '@/lib/wallet/use-get-wallets'; +import { useWallet } from '@/lib/wallet/WalletProvider'; + +import { AccountStep } from './AccountStep'; +import { Disclaimer } from './Disclaimer'; +import { AuthModalSteps } from './types'; +import { WalletStep } from './WalletStep'; + +const getTitle = (t: TFunction, step: AuthModalSteps, hasNoInstalledWallet: boolean) => { + if (step === AuthModalSteps.ACCOUNT) { + return t('account_modal.please_select_an_account'); + } + + if (hasNoInstalledWallet) { + return t('account_modal.please_install_supported_wallet'); + } + + return t('account_modal.please_select_a_wallet'); +}; + +type InheritAttrs = Omit; + +type AuthModalProps = InheritAttrs; + +// TODO: handle better +const AuthModal = ({ isOpen, onClose, ...props }: AuthModalProps): JSX.Element => { + const { t } = useTranslation(); + + const { account, setAccount, disconnect } = useWallet(); + + const { data: wallets } = useGetWallets(); + + const { selectProps } = useSignMessage(); + + const [step, setStep] = useState(AuthModalSteps.ACCOUNT); + const [wallet, setWallet] = useState(); + + const { mutateAsync: enableWalletAsync } = useEnableWallet(wallet?.extensionName); + + const { data: accounts, mutateAsync: mutateAccountsAsync } = useGetWalletAccounts(wallet?.extensionName); + + const handleWalletSelect = useCallback( + async (wallet: WalletData) => { + setWallet(wallet); + await enableWalletAsync(wallet); + await mutateAccountsAsync(wallet); + }, + [enableWalletAsync, mutateAccountsAsync] + ); + + useEffect(() => { + if (!isOpen) return; + + setStep(account ? AuthModalSteps.ACCOUNT : AuthModalSteps.WALLET); + setWallet(account?.wallet); + }, [isOpen, account]); + + useEffect(() => { + if (!isOpen || !wallet) return; + + handleWalletSelect(wallet); + }, [isOpen, wallet, handleWalletSelect]); + + useEffect(() => { + if (!accounts) return; + + setStep(AuthModalSteps.ACCOUNT); + }, [accounts]); + + const handleChangeWallet = () => { + setStep(AuthModalSteps.WALLET); + }; + + const handleAccountSelection = (account: WalletAccountData) => { + setAccount(account); + selectProps.onSelectionChange(account); + onClose?.(); + }; + + const handleDisconnect = () => { + onClose?.(); + disconnect(); + }; + + const title = getTitle(t, step, wallets.hasInstalled); + + const hasDisconnect = step === AuthModalSteps.ACCOUNT && account; + + return ( + + {title} + + + + {wallet && accounts && ( + + )} + + {hasDisconnect && ( + + + {t('account_modal.disconnect')} + + + )} + + ); +}; + +export { AuthModal, AuthModalSteps }; +export type { AuthModalProps }; diff --git a/src/components/AuthModal/Disclaimer.tsx b/src/components/WalletModalTrigger/Disclaimer.tsx similarity index 100% rename from src/components/AuthModal/Disclaimer.tsx rename to src/components/WalletModalTrigger/Disclaimer.tsx diff --git a/src/components/AuthModal/SignTermsModal.tsx b/src/components/WalletModalTrigger/SignTermsModal.tsx similarity index 100% rename from src/components/AuthModal/SignTermsModal.tsx rename to src/components/WalletModalTrigger/SignTermsModal.tsx diff --git a/src/components/WalletModalTrigger/WalletModalTrigger.tsx b/src/components/WalletModalTrigger/WalletModalTrigger.tsx new file mode 100644 index 0000000000..466f560776 --- /dev/null +++ b/src/components/WalletModalTrigger/WalletModalTrigger.tsx @@ -0,0 +1,32 @@ +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; + +import { showAccountModalAction } from '@/common/actions/general.actions'; +import { StoreType } from '@/common/types/util.types'; +import { useWallet } from '@/lib/wallet/WalletProvider'; + +import { AuthModal } from './AuthModal'; +import { StyledCTA } from './AuthModal.style'; + +const WalletModalTrigger = (): JSX.Element => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const { account } = useWallet(); + + const { showAccountModal } = useSelector((state: StoreType) => state.general); + + const handleModalOpen = () => dispatch(showAccountModalAction(true)); + + const handleModalClose = () => dispatch(showAccountModalAction(false)); + + return ( + <> + + {account ? account?.name || 'Wallet' : t('connect_wallet')} + + + + ); +}; + +export { WalletModalTrigger }; diff --git a/src/components/AuthModal/WalletStep.tsx b/src/components/WalletModalTrigger/WalletStep.tsx similarity index 54% rename from src/components/AuthModal/WalletStep.tsx rename to src/components/WalletModalTrigger/WalletStep.tsx index 0df7c88701..f3e4950671 100644 --- a/src/components/AuthModal/WalletStep.tsx +++ b/src/components/WalletModalTrigger/WalletStep.tsx @@ -1,8 +1,6 @@ -import { InjectedExtension } from '@polkadot/extension-inject/types'; - import { ArrowTopRightOnSquare } from '@/assets/icons'; import { Flex, WalletIcon } from '@/component-library'; -import { WalletData, WALLETS } from '@/utils/constants/wallets'; +import { WalletData } from '@/lib/wallet/types'; import { StepComponentProps, withStep } from '@/utils/hocs/step'; import { StyledArrowRight, StyledWalletItem } from './AuthModal.style'; @@ -10,28 +8,27 @@ import { AuthModalSteps } from './types'; type WalletStepProps = { onSelectionChange?: (wallet: WalletData) => void; - extensions: InjectedExtension[]; - selectedWallet?: WalletData; + wallets: WalletData[]; + value?: WalletData; } & StepComponentProps; -const WalletComponent = ({ extensions, selectedWallet, onSelectionChange }: WalletStepProps): JSX.Element => { - const wallets = WALLETS.map((wallet) => ({ - isInstalled: extensions.find((extension) => extension.name === wallet.extensionName), - data: wallet - })).sort((a, b) => (a.isInstalled === b.isInstalled ? 0 : b.isInstalled ? 1 : -1)); +const WalletComponent = ({ wallets, value, onSelectionChange }: WalletStepProps): JSX.Element => { + const sortedWallets = wallets.sort((a, b) => (a.installed === b.installed ? 0 : b.installed ? 1 : -1)); return ( - {wallets.map(({ data, isInstalled }) => { + {sortedWallets.map((wallet) => { + const isInstalled = wallet.installed; + const handlePress = () => { if (isInstalled) { - onSelectionChange?.(data); + onSelectionChange?.(wallet); } else { - window.open(data.url, '_blank', 'noopener'); + window.open(wallet.installUrl, '_blank', 'noopener'); } }; - const isSelected = selectedWallet?.extensionName === data.extensionName; + const isSelected = value?.extensionName === wallet.extensionName; return ( - - {data.title} + + {wallet.title} {isInstalled ? : } diff --git a/src/components/AuthModal/index.tsx b/src/components/WalletModalTrigger/index.tsx similarity index 77% rename from src/components/AuthModal/index.tsx rename to src/components/WalletModalTrigger/index.tsx index 50ca3b94b9..d291e8e538 100644 --- a/src/components/AuthModal/index.tsx +++ b/src/components/WalletModalTrigger/index.tsx @@ -2,3 +2,4 @@ export type { AuthModalProps } from './AuthModal'; export { AuthModal } from './AuthModal'; export type { SignTermsModalProps } from './SignTermsModal'; export { SignTermsModal } from './SignTermsModal'; +export { WalletModalTrigger } from './WalletModalTrigger'; diff --git a/src/components/AuthModal/types.ts b/src/components/WalletModalTrigger/types.ts similarity index 100% rename from src/components/AuthModal/types.ts rename to src/components/WalletModalTrigger/types.ts diff --git a/src/components/index.tsx b/src/components/index.tsx index 6069e639c8..6e2e2a7ed0 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -4,8 +4,6 @@ export type { ApyDetailsGroupItemProps, ApyDetailsGroupProps, ApyDetailsProps } export { ApyDetails, ApyDetailsGroup, ApyDetailsGroupItem } from './ApyDetails'; export type { AuthCTAProps } from './AuthCTA'; export { AuthCTA } from './AuthCTA'; -export type { AuthModalProps, SignTermsModalProps } from './AuthModal'; -export { AuthModal, SignTermsModal } from './AuthModal'; export type { AssetCellProps, BalanceCellProps, CellProps, TableProps } from './DataGrid'; export { AssetCell, BalanceCell, Cell, Table } from './DataGrid'; export type { FundWalletProps } from './FundWallet'; @@ -35,3 +33,5 @@ export * from './TransactionDetails'; export type { TransactionFeeDetailsProps } from './TransactionFeeDetails'; export { TransactionFeeDetails } from './TransactionFeeDetails'; export { TransactionModal } from './TransactionModal'; +export type { AuthModalProps, SignTermsModalProps } from './WalletModalTrigger'; +export { AuthModal, SignTermsModal } from './WalletModalTrigger'; diff --git a/src/hooks/api/amm/use-get-account-pools.tsx b/src/hooks/api/amm/use-get-account-pools.tsx index 41687136e6..3c82881c2c 100644 --- a/src/hooks/api/amm/use-get-account-pools.tsx +++ b/src/hooks/api/amm/use-get-account-pools.tsx @@ -6,10 +6,10 @@ import { useErrorHandler } from 'react-error-boundary'; import { useQuery } from 'react-query'; import { Prices, useGetPrices } from '@/hooks/api/use-get-prices'; +import { useWallet } from '@/lib/wallet'; import { BLOCKTIME_REFETCH_INTERVAL } from '@/utils/constants/api'; import { calculateAccountLiquidityUSD, calculateTotalLiquidityUSD } from '@/utils/helpers/pool'; -import useAccountId from '../../use-account-id'; import { useGetLiquidityPools } from './use-get-liquidity-pools'; type AccountLiquidityPool = { data: LiquidityPool; amount: MonetaryAmount }; @@ -60,15 +60,16 @@ interface UseGetAccountProvidedLiquidity { // Mixes current pools with liquidity provided by the account const useGetAccountPools = (): UseGetAccountProvidedLiquidity => { - const accountId = useAccountId(); + const { account } = useWallet(); const prices = useGetPrices(); - const { data: liquidityPools, refetch: refetchLiquidityPools } = useGetLiquidityPools(); - const queryKey = ['account-pools', accountId]; + + const queryKey = ['account-pools', account?.address]; + const { data, error, refetch: refetchQuery } = useQuery({ - queryKey: ['account-pools', accountId], - queryFn: () => accountId && liquidityPools && prices && getAccountLiquidityPools(accountId, liquidityPools, prices), - enabled: !!accountId && !!liquidityPools && !!prices, + queryKey, + queryFn: () => account && liquidityPools && prices && getAccountLiquidityPools(account.id, liquidityPools, prices), + enabled: !!account && !!liquidityPools && !!prices, refetchInterval: BLOCKTIME_REFETCH_INTERVAL }); diff --git a/src/hooks/api/bridge/use-get-max-burnable-tokens.tsx b/src/hooks/api/bridge/use-get-max-burnable-tokens.tsx index 22c3848929..1fa04f87d4 100644 --- a/src/hooks/api/bridge/use-get-max-burnable-tokens.tsx +++ b/src/hooks/api/bridge/use-get-max-burnable-tokens.tsx @@ -4,9 +4,9 @@ import { useCallback } from 'react'; import { useErrorHandler } from 'react-error-boundary'; import { useQuery } from 'react-query'; +import { useWallet } from '@/lib/wallet'; import { BLOCKTIME_REFETCH_INTERVAL } from '@/utils/constants/api'; -import useAccountId from '../../use-account-id'; import { useGetCollateralCurrencies } from '../use-get-collateral-currencies'; type MaxBurnableTokensData = { @@ -31,14 +31,13 @@ type UseGetMaxBurnableTokensResult = { const useGetMaxBurnableTokens = (): UseGetMaxBurnableTokensResult => { const { data: collateralCurrencies } = useGetCollateralCurrencies(true); - - const accountId = useAccountId(); + const { account } = useWallet(); const { data, error, refetch } = useQuery({ - queryKey: ['max-burnable-tokens', accountId?.toString()], + queryKey: ['max-burnable-tokens', account?.address], queryFn: () => collateralCurrencies && getMaxBurnableTokensData(collateralCurrencies), refetchInterval: BLOCKTIME_REFETCH_INTERVAL, - enabled: !!accountId && !!collateralCurrencies + enabled: !!account && !!collateralCurrencies }); useErrorHandler(error); 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..e918b84981 100644 --- a/src/hooks/api/loans/use-get-account-positions-earnings.tsx +++ b/src/hooks/api/loans/use-get-account-positions-earnings.tsx @@ -7,7 +7,7 @@ import { useErrorHandler } from 'react-error-boundary'; import { useQuery } from 'react-query'; import { SQUID_URL } from '@/constants'; -import { useWallet } from '@/hooks/use-wallet'; +import { useWallet } from '@/lib/wallet'; import { CollateralPosition } from '@/types/loans'; import { REFETCH_INTERVAL } from '@/utils/constants/api'; @@ -73,8 +73,8 @@ const useGetAccountPositionsEarnings = ( const { account } = useWallet(); const { refetch, isLoading, data, error } = useQuery({ - queryKey: ['loan-earnings', account], - queryFn: () => lendPositions && account && getEarnedAmountByTicker(account.toString(), lendPositions), + queryKey: ['loan-earnings', account?.address], + queryFn: () => lendPositions && account && getEarnedAmountByTicker(account.address, lendPositions), enabled: !!lendPositions && !!account, refetchOnWindowFocus: false, refetchInterval: REFETCH_INTERVAL.MINUTE diff --git a/src/hooks/api/loans/use-get-account-positions.tsx b/src/hooks/api/loans/use-get-account-positions.tsx index 6b745db598..a19214e571 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 '@/lib/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> => @@ -20,12 +20,12 @@ interface UseGetLendPositionsOfAccountResult { } const useGetLendPositionsOfAccount = (): UseGetLendPositionsOfAccountResult => { - const accountId = useAccountId(); + const { account } = useWallet(); const { data, error, refetch, isLoading } = useQuery({ - queryKey: ['getLendPositionsOfAccount', accountId], - queryFn: () => accountId && getLendPositionsOfAccount(accountId), - enabled: !!accountId, + queryKey: ['getLendPositionsOfAccount', account?.address], + queryFn: () => account && getLendPositionsOfAccount(account.id), + enabled: !!account, refetchInterval: BLOCKTIME_REFETCH_INTERVAL }); @@ -41,18 +41,12 @@ interface UseGetBorrowPositionsOfAccountResult { } const useGetBorrowPositionsOfAccount = (): UseGetBorrowPositionsOfAccountResult => { - const accountId = useAccountId(); + const { account } = useWallet(); const { data, error, refetch, isLoading } = useQuery({ - queryKey: ['getBorrowPositionsOfAccount', accountId], - queryFn: async () => { - if (!accountId) { - throw new Error('Something went wrong!'); - } - - return await window.bridge.loans.getBorrowPositionsOfAccount(accountId); - }, - enabled: !!accountId, + queryKey: ['getBorrowPositionsOfAccount', account?.address], + queryFn: () => account && window.bridge.loans.getBorrowPositionsOfAccount(account?.id), + enabled: !!account, refetchInterval: BLOCKTIME_REFETCH_INTERVAL }); diff --git a/src/hooks/api/loans/use-get-loan-assets.tsx b/src/hooks/api/loans/use-get-loan-assets.tsx index 1eb7247265..5c0a77e2cf 100644 --- a/src/hooks/api/loans/use-get-loan-assets.tsx +++ b/src/hooks/api/loans/use-get-loan-assets.tsx @@ -4,6 +4,8 @@ import { useQuery } from 'react-query'; import { BLOCKTIME_REFETCH_INTERVAL } from '@/utils/constants/api'; +import { useApi } from '../use-api'; + interface UseGetLoansAssets { isLoading: boolean; data: TickerToData | undefined; @@ -11,9 +13,11 @@ interface UseGetLoansAssets { } const useGetLoanAssets = (): UseGetLoansAssets => { + const { data: api } = useApi(); + const { data, error, refetch, isLoading } = useQuery({ queryKey: ['loan-assets'], - queryFn: (): Promise> => window.bridge.loans.getLoanAssets(), + queryFn: (): Promise> => api.loans.getLoanAssets(), refetchInterval: BLOCKTIME_REFETCH_INTERVAL }); diff --git a/src/hooks/api/use-api.tsx b/src/hooks/api/use-api.tsx new file mode 100644 index 0000000000..6e102c020a --- /dev/null +++ b/src/hooks/api/use-api.tsx @@ -0,0 +1,26 @@ +import { createInterBtcApi, InterBtcApi } from '@interlay/interbtc-api'; +import { useErrorHandler } from 'react-error-boundary'; +import { useQuery, UseQueryResult } from 'react-query'; + +import * as constants from '@/constants'; + +type UseGetExchangeRateResult = UseQueryResult; + +const useApi = (): UseGetExchangeRateResult => { + const queryResult = useQuery({ + queryKey: ['api'], + queryFn: () => createInterBtcApi(constants.PARACHAIN_URL, constants.BITCOIN_NETWORK), + refetchOnMount: false, + refetchIntervalInBackground: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + cacheTime: Infinity + }); + + useErrorHandler(queryResult.error); + + return queryResult; +}; + +export { useApi }; +export type { UseGetExchangeRateResult }; diff --git a/src/hooks/use-local-storage.ts b/src/hooks/use-local-storage.ts index 512bff8496..ff12bcf775 100644 --- a/src/hooks/use-local-storage.ts +++ b/src/hooks/use-local-storage.ts @@ -1,13 +1,17 @@ import { useLocalStorage as useLibLocalStorage } from 'react-use'; +import { WalletAccountData } from '@/lib/wallet/types'; + enum LocalStorageKey { TC_SIGNATURES = 'TC_SIGNATURES', - WALLET_WELCOME_BANNER = 'WALLET_WELCOME_BANNER' + WALLET_WELCOME_BANNER = 'WALLET_WELCOME_BANNER', + WALLET_ACCOUNT = 'WALLET_ACCOUNT' } type LocalStorageValueTypes = { [LocalStorageKey.TC_SIGNATURES]: { [account: string]: { version: string; isSigned: boolean } | boolean }; [LocalStorageKey.WALLET_WELCOME_BANNER]: boolean; + [LocalStorageKey.WALLET_ACCOUNT]: WalletAccountData; }; type Options = diff --git a/src/hooks/use-sign-message.ts b/src/hooks/use-sign-message.ts index cfaad90563..3b077b9a03 100644 --- a/src/hooks/use-sign-message.ts +++ b/src/hooks/use-sign-message.ts @@ -8,6 +8,7 @@ import { showSignTermsModalAction } from '@/common/actions/general.actions'; import { TERMS_AND_CONDITIONS_LINK } from '@/config/relay-chains'; import { SIGNER_API_URL, TC_VERSION } from '@/constants'; import { KeyringPair, useSubstrateSecureState } from '@/lib/substrate'; +import { WalletAccountData } from '@/lib/wallet/types'; import { NotificationToastType, useNotifications } from '../utils/context/Notifications'; import { signMessage } from '../utils/helpers/wallet'; @@ -41,7 +42,7 @@ type UseSignMessageResult = { onPress: (e: PressEvent) => void; loading: boolean; }; - selectProps: { onSelectionChange: (account: KeyringPair) => void }; + selectProps: { onSelectionChange: (account: WalletAccountData) => void }; modal: { buttonProps: { onPress: (e: PressEvent) => void; @@ -142,7 +143,7 @@ const useSignMessage = (): UseSignMessageResult => { signMessageMutation.mutate(account); }; - const handleOpenSignTermModal = async (account: KeyringPair) => { + const handleOpenSignTermModal = async (account: WalletAccountData) => { if (!SIGNER_API_URL || !shouldCheckSignature) return; // Cancel possible ongoing unwanted account diff --git a/src/index.tsx b/src/index.tsx index c10a8de3b7..436e021152 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,6 +19,7 @@ import { SubstrateLoadingAndErrorHandlingWrapper, SubstrateProvider } from '@/li import App from './App'; import { GeoblockingWrapper } from './components/Geoblock/Geoblock'; +import { WalletProvider } from './lib/wallet/WalletProvider'; import reportWebVitals from './reportWebVitals'; import { store } from './store'; import { NotificationsProvider } from './utils/context/Notifications'; @@ -39,15 +40,17 @@ ReactDOM.render( - - - - - - - - - + + + + + + + + + + + diff --git a/src/legacy-components/Topbar/index.tsx b/src/legacy-components/Topbar/index.tsx index 3c66aa8735..37ee6f72e2 100644 --- a/src/legacy-components/Topbar/index.tsx +++ b/src/legacy-components/Topbar/index.tsx @@ -1,22 +1,17 @@ import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'; -import { Keyring } from '@polkadot/api'; -import { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; -import { showAccountModalAction, showSignTermsModalAction } from '@/common/actions/general.actions'; +import { showSignTermsModalAction } from '@/common/actions/general.actions'; import { StoreType } from '@/common/types/util.types'; import { FundWallet, NotificationsPopover } from '@/components'; -import { AuthModal, SignTermsModal } from '@/components/AuthModal'; -import { SS58_FORMAT } from '@/constants'; -import { useSignMessage } from '@/hooks/use-sign-message'; +import { SignTermsModal, WalletModalTrigger } from '@/components/WalletModalTrigger'; import InterlayCaliforniaOutlinedButton from '@/legacy-components/buttons/InterlayCaliforniaOutlinedButton'; -import InterlayDefaultContainedButton from '@/legacy-components/buttons/InterlayDefaultContainedButton'; import InterlayDenimOrKintsugiMidnightOutlinedButton from '@/legacy-components/buttons/InterlayDenimOrKintsugiMidnightOutlinedButton'; import Tokens from '@/legacy-components/Tokens'; import InterlayLink from '@/legacy-components/UI/InterlayLink'; -import { KeyringPair, useSubstrate, useSubstrateSecureState } from '@/lib/substrate'; +import { useWallet } from '@/lib/wallet/WalletProvider'; import { BitcoinNetwork } from '@/types/bitcoin'; import { POLKADOT } from '@/utils/constants/relay-chain-names'; import { useNotifications } from '@/utils/context/Notifications'; @@ -27,45 +22,19 @@ import ManualIssueExecutionActionsBadge from './ManualIssueExecutionActionsBadge const SMALL_SIZE_BUTTON_CLASSES = clsx('leading-7', '!px-3'); const Topbar = (): JSX.Element => { - const { showAccountModal, isSignTermsModalOpen } = useSelector((state: StoreType) => state.general); + const { isSignTermsModalOpen } = useSelector((state: StoreType) => state.general); const dispatch = useDispatch(); const { t } = useTranslation(); - const { setSelectedAccount, removeSelectedAccount } = useSubstrate(); - const { selectProps } = useSignMessage(); + // const { removeSelectedAccount } = useSubstrate(); + // const { selectProps } = useSignMessage(); const notifications = useNotifications(); const { buttonProps, isAvailable } = useFaucet(); - const { extensions, selectedAccount } = useSubstrateSecureState(); - - const handleAccountModalOpen = () => dispatch(showAccountModalAction(true)); - - const handleAccountModalClose = () => dispatch(showAccountModalAction(false)); - - const handleAccountSelect = (account: InjectedAccountWithMeta) => { - const keyring = new Keyring({ type: 'sr25519', ss58Format: SS58_FORMAT }); - const keyringAccount = keyring.addFromAddress(account.address, account.meta); - setSelectedAccount(keyringAccount); - selectProps.onSelectionChange(keyringAccount as KeyringPair); - handleAccountModalClose(); - }; - - const handleDisconnect = () => { - removeSelectedAccount(); - handleAccountModalClose(); - }; + const { account } = useWallet(); const handleCloseSignTermsModal = () => dispatch(showSignTermsModalAction(false)); - let accountLabel; - if (!extensions.length) { - accountLabel = t('connect_wallet'); - } else if (selectedAccount) { - accountLabel = selectedAccount.meta.name; - } else { - accountLabel = 'Select Wallet'; - } - return ( <>
@@ -76,7 +45,7 @@ const Topbar = (): JSX.Element => { {t('request_funds')} )} - {selectedAccount !== undefined && ( + {account !== undefined && ( <> {process.env.REACT_APP_BITCOIN_NETWORK !== BitcoinNetwork.Mainnet && ( <> @@ -103,18 +72,11 @@ const Topbar = (): JSX.Element => { 'bg-white': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT })} > - + - - {accountLabel} - +
- + ); diff --git a/src/lib/substrate/context/provider.tsx b/src/lib/substrate/context/provider.tsx index dc08b624b4..07537b7244 100644 --- a/src/lib/substrate/context/provider.tsx +++ b/src/lib/substrate/context/provider.tsx @@ -1,6 +1,6 @@ import { createInterBtcApi } from '@interlay/interbtc-api'; import { ApiPromise } from '@polkadot/api'; -import { web3Accounts, web3Enable, web3FromAddress } from '@polkadot/extension-dapp'; +import { web3Accounts, web3Enable } from '@polkadot/extension-dapp'; import { TypeRegistry } from '@polkadot/types/create'; import jsonrpc from '@polkadot/types/interfaces/jsonrpc'; import { keyring } from '@polkadot/ui-keyring'; @@ -187,22 +187,22 @@ const SubstrateProvider = ({ children, socket }: SubstrateProviderProps): JSX.El const keyringStatus = state.keyringStatus; const accounts = state.accounts; - React.useEffect(() => { - if (keyringStatus !== KeyringStatus.Ready) return; - - if (selectedAccountAddress) { - (async () => { - try { - const { signer } = await web3FromAddress(selectedAccountAddress); - window.bridge.setAccount(selectedAccountAddress, signer); - } catch (error) { - console.error('[SubstrateProvider] error => ', error); - } - })(); - } else { - window.bridge.removeAccount(); - } - }, [selectedAccountAddress, keyringStatus]); + // React.useEffect(() => { + // if (keyringStatus !== KeyringStatus.Ready) return; + + // if (selectedAccountAddress) { + // (async () => { + // try { + // const { signer } = await web3FromAddress(selectedAccountAddress); + // window.bridge.setAccount(selectedAccountAddress, signer); + // } catch (error) { + // console.error('[SubstrateProvider] error => ', error); + // } + // })(); + // } else { + // window.bridge.removeAccount(); + // } + // }, [selectedAccountAddress, keyringStatus]); const removeSelectedAccount = React.useCallback(() => { if (!removeLS) return; diff --git a/src/lib/wallet/WalletProvider.tsx b/src/lib/wallet/WalletProvider.tsx new file mode 100644 index 0000000000..97d9ceece2 --- /dev/null +++ b/src/lib/wallet/WalletProvider.tsx @@ -0,0 +1,60 @@ +import { newAccountId } from '@interlay/interbtc-api'; +import { AccountId } from '@polkadot/types/interfaces'; +import { Signer } from '@polkadot/types/types'; +import React, { useCallback, useMemo } from 'react'; + +import { LocalStorageKey, useLocalStorage } from '@/hooks/use-local-storage'; + +import { WalletAccountData, WalletData } from './types'; +import { useGetWallets } from './use-get-wallets'; + +type WalletConfig = { + account?: WalletAccountData & { wallet: WalletData; id: AccountId }; + setAccount: (account: WalletAccountData) => void; + disconnect: () => void; +}; + +const defaultContext: WalletConfig = {} as WalletConfig; + +const WalletContext = React.createContext(defaultContext); + +const useWallet = (): WalletConfig => React.useContext(WalletContext); + +const WalletProvider: React.FC = ({ children }) => { + const [account, setAccount, clearAccount] = useLocalStorage(LocalStorageKey.WALLET_ACCOUNT); + const { data: wallets } = useGetWallets(); + + const wallet = account?.wallet?.extensionName + ? wallets.available.find((wallet) => wallet.extensionName === account.wallet?.extensionName) + : undefined; + + const handleSetAccount = useCallback( + (account: WalletAccountData) => { + setAccount(account); + + if (account.signer) { + window.bridge.setAccount(account.address, account.signer as Signer); + } + }, + [setAccount] + ); + + const accountId = useMemo( + () => (account && window.bridge ? newAccountId(window.bridge.api, account.address) : undefined), + [window.bridge, account] + ); + + return ( + + {children} + + ); +}; + +export { useWallet, WalletContext, WalletProvider }; diff --git a/src/lib/wallet/index.ts b/src/lib/wallet/index.ts new file mode 100644 index 0000000000..21924f6e47 --- /dev/null +++ b/src/lib/wallet/index.ts @@ -0,0 +1 @@ +export { useWallet, WalletContext, WalletProvider } from './WalletProvider'; diff --git a/src/lib/wallet/types.ts b/src/lib/wallet/types.ts new file mode 100644 index 0000000000..084d5a5dbe --- /dev/null +++ b/src/lib/wallet/types.ts @@ -0,0 +1,3 @@ +import { Wallet as WalletData, WalletAccount as WalletAccountData } from '@talismn/connect-wallets'; + +export type { WalletAccountData, WalletData }; diff --git a/src/lib/wallet/use-enable-wallet.tsx b/src/lib/wallet/use-enable-wallet.tsx new file mode 100644 index 0000000000..779f3eb37e --- /dev/null +++ b/src/lib/wallet/use-enable-wallet.tsx @@ -0,0 +1,24 @@ +import { useMutation, UseMutationOptions, UseMutationResult } from 'react-query'; + +import { APP_NAME } from '@/config/relay-chains'; + +import { WalletData } from './types'; + +const enableFn = async (wallet: WalletData): Promise => { + await wallet.enable(APP_NAME); + + return wallet; +}; + +type UseEnableWalletOptions = Omit< + UseMutationOptions, + 'mutationKey' | 'mutationFn' +>; + +type UseEnableWalletResult = UseMutationResult; + +const useEnableWallet = (provider?: string, options?: UseEnableWalletOptions): UseEnableWalletResult => + useMutation(['web3Enable', provider], enableFn, options); + +export { useEnableWallet }; +export type { UseEnableWalletOptions, UseEnableWalletResult }; diff --git a/src/lib/wallet/use-get-wallet-accounts.tsx b/src/lib/wallet/use-get-wallet-accounts.tsx new file mode 100644 index 0000000000..d776e30615 --- /dev/null +++ b/src/lib/wallet/use-get-wallet-accounts.tsx @@ -0,0 +1,18 @@ +import { useMutation, UseMutationOptions, UseMutationResult } from 'react-query'; + +import { WalletAccountData, WalletData } from './types'; + +const getAccountsFn = async (wallet: WalletData) => wallet.getAccounts(); + +type UseGetWalletAccountsOptions = Omit< + UseMutationOptions, + 'mutationKey' | 'mutationFn' +>; + +type UseGetWalletAccountsResult = UseMutationResult; + +const useGetWalletAccounts = (provider?: string, options?: UseGetWalletAccountsOptions): UseGetWalletAccountsResult => + useMutation(['accounts', provider], getAccountsFn, options); + +export { useGetWalletAccounts }; +export type { UseGetWalletAccountsOptions, UseGetWalletAccountsResult }; diff --git a/src/lib/wallet/use-get-wallets.tsx b/src/lib/wallet/use-get-wallets.tsx new file mode 100644 index 0000000000..87efef40f5 --- /dev/null +++ b/src/lib/wallet/use-get-wallets.tsx @@ -0,0 +1,30 @@ +import { getWallets } from '@talismn/connect-wallets'; +import { useMemo } from 'react'; + +import { WalletData } from './types'; + +type UseGetWalletResult = { + data: { + available: WalletData[]; + installed: WalletData[]; + hasInstalled: boolean; + }; +}; + +const useGetWallets = (): UseGetWalletResult => { + return useMemo(() => { + const available = getWallets(); + const installed = available.filter((wallet) => wallet.installed); + + return { + data: { + available, + installed, + hasInstalled: !!installed.length + } + }; + }, []); +}; + +export { useGetWallets }; +export type { UseGetWalletResult }; diff --git a/src/pages/Onboarding/Onboarding.tsx b/src/pages/Onboarding/Onboarding.tsx index 2df83f77e5..0d872fd32a 100644 --- a/src/pages/Onboarding/Onboarding.tsx +++ b/src/pages/Onboarding/Onboarding.tsx @@ -1,5 +1,3 @@ -import { Keyring } from '@polkadot/api'; -import { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; import { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; @@ -10,10 +8,9 @@ import { Card, CTA, CTALink, Flex, H1, H2, P, Strong } from '@/component-library import { AuthModal, MainContainer, SignTermsModal } from '@/components'; import { INTERLAY_DISCORD_LINK } from '@/config/links'; import { GOVERNANCE_TOKEN } from '@/config/relay-chains'; -import { SS58_FORMAT } from '@/constants'; import { useGetBalances } from '@/hooks/api/tokens/use-get-balances'; import { useSignMessage } from '@/hooks/use-sign-message'; -import { KeyringPair, useSubstrate, useSubstrateSecureState } from '@/lib/substrate'; +import { useSubstrateSecureState } from '@/lib/substrate'; import { Tutorial } from './components'; import { StyledWrapper } from './Onboarding.style'; @@ -34,8 +31,8 @@ const Onboarding = (): JSX.Element => { const dispatch = useDispatch(); const { t } = useTranslation(); const { getAvailableBalance } = useGetBalances(); - const { setSelectedAccount, removeSelectedAccount } = useSubstrate(); - const { selectProps } = useSignMessage(); + // const { setSelectedAccount, removeSelectedAccount } = useSubstrate(); + // const { selectProps } = useSignMessage(); const { extensions, selectedAccount } = useSubstrateSecureState(); const { hasSignature } = useSignMessage(); @@ -45,18 +42,18 @@ const Onboarding = (): JSX.Element => { const handleAccountModalClose = () => dispatch(showAccountModalAction(false)); - const handleAccountSelect = (account: InjectedAccountWithMeta) => { - const keyring = new Keyring({ type: 'sr25519', ss58Format: SS58_FORMAT }); - const keyringAccount = keyring.addFromAddress(account.address, account.meta); - setSelectedAccount(keyringAccount); - selectProps.onSelectionChange(keyringAccount as KeyringPair); - handleAccountModalClose(); - }; + // const handleAccountSelect = (account: InjectedAccountWithMeta) => { + // const keyring = new Keyring({ type: 'sr25519', ss58Format: SS58_FORMAT }); + // const keyringAccount = keyring.addFromAddress(account.address, account.meta); + // setSelectedAccount(keyringAccount); + // // selectProps.onSelectionChange(keyringAccount as KeyringPair); + // handleAccountModalClose(); + // }; - const handleDisconnect = () => { - removeSelectedAccount(); - handleAccountModalClose(); - }; + // const handleDisconnect = () => { + // removeSelectedAccount(); + // handleAccountModalClose(); + // }; const handleCloseSignTermsModal = () => dispatch(showSignTermsModalAction(false)); @@ -174,12 +171,7 @@ const Onboarding = (): JSX.Element => { {getCta(step)} ))} - + diff --git a/yarn.lock b/yarn.lock index 148843ae1e..8a754ef5e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6046,6 +6046,11 @@ resolve "^1.20.0" tmp "^0.2.1" +"@talismn/connect-wallets@^1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@talismn/connect-wallets/-/connect-wallets-1.2.3.tgz#4aa91f2c554d692e9f49472f74dcc96fca319df5" + integrity sha512-6hLYeDnMjrlsNLF6a9e7ngxKMkKW3uTC2IbBjtTPq9fxA1VvApmxR8egboPzU7sgFQEuUFP36KN1hFAtfRIVhw== + "@testing-library/dom@^8.0.0": version "8.20.0" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.0.tgz#914aa862cef0f5e89b98cc48e3445c4c921010f6"