diff --git a/jest.config.ts b/jest.config.ts index 150dd7a034..ccb36c9d48 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -15,7 +15,7 @@ const config = { '.+\\.tsx$': 'ts-jest' }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], - setupFiles: ['dotenv/config', '@serh11p/jest-webextension-mock'], + setupFiles: ['dotenv/config', '@temple-wallet/jest-webextension-mock'], setupFilesAfterEnv: ['./jest.setup.js'] }; diff --git a/package.json b/package.json index 6b2f2d9cc6..9766341e6a 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "@redux-devtools/remote": "^0.8.0", "@reduxjs/toolkit": "^1.8.5", "@rnw-community/shared": "^0.48.0", - "@serh11p/jest-webextension-mock": "4.0.0", "@svgr/webpack": "6.4.0", "@taquito/ledger-signer": "17.0.0", "@taquito/local-forging": "17.0.0", @@ -58,6 +57,7 @@ "@taquito/tzip16": "17.0.0", "@taquito/utils": "17.0.0", "@temple-wallet/dapp": "5.0.2", + "@temple-wallet/jest-webextension-mock": "^4.1.0", "@temple-wallet/wallet-address-validator": "^0.4.3", "@tezos-domains/core": "1.26.0", "@tezos-domains/taquito-client": "1.26.0", @@ -94,6 +94,7 @@ "@typescript-eslint/parser": "^5.43.0", "@vespaiach/axios-fetch-adapter": "^0.3.1", "assert": "1.5.0", + "async-mutex": "^0.4.0", "async-retry": "1.3.3", "autoprefixer": "10.4.2", "axios": "0.26.1", @@ -175,7 +176,7 @@ "rxjs": "^7.5.6", "scryptsy": "2.1.0", "stream-browserify": "3.0.0", - "swr": "1.3.0", + "swr": "2.2.4", "tailwindcss": "2.2.19", "terser-webpack-plugin": "5.3.6", "three": "^0.151.2", diff --git a/public/_locales/uk/messages.json b/public/_locales/uk/messages.json index c34bdbfb99..bc5505ab9c 100644 --- a/public/_locales/uk/messages.json +++ b/public/_locales/uk/messages.json @@ -116,6 +116,9 @@ "lock": { "message": "Заблокувати" }, + "lockUpSettingsDescription": { + "message": "Розширення буде заблоковане після 5 хвилин відсутності активності." + }, "importedAccount": { "message": "Імпортовані" }, diff --git a/src/app/ConfirmPage.tsx b/src/app/ConfirmPage.tsx index 3c7b113897..9d797cf5f8 100644 --- a/src/app/ConfirmPage.tsx +++ b/src/app/ConfirmPage.tsx @@ -131,7 +131,7 @@ const ConfirmDAppForm: FC = () => { return pageId; }, [loc.search]); - const { data } = useRetryableSWR([id], getDAppPayload, { + const { data } = useRetryableSWR(id, getDAppPayload, { suspense: true, shouldRetryOnError: false, revalidateOnFocus: false, diff --git a/src/app/a11y/AwaitFonts.tsx b/src/app/a11y/AwaitFonts.tsx index 93c84b6c9f..9e7905f434 100644 --- a/src/app/a11y/AwaitFonts.tsx +++ b/src/app/a11y/AwaitFonts.tsx @@ -1,7 +1,8 @@ import React, { FC } from 'react'; import FontFaceObserver from 'fontfaceobserver'; -import useSWR from 'swr'; + +import { useTypedSWR } from 'lib/swr'; interface AwaitFontsProps extends PropsWithChildren { name: string; @@ -10,7 +11,7 @@ interface AwaitFontsProps extends PropsWithChildren { } const AwaitFonts: FC = ({ name, weights, className, children }) => { - useSWR([name, weights, className], awaitFonts, { + useTypedSWR([name, weights, className], awaitFonts, { suspense: true, shouldRetryOnError: false, revalidateOnFocus: false, @@ -22,7 +23,7 @@ const AwaitFonts: FC = ({ name, weights, className, children }) export default AwaitFonts; -async function awaitFonts(name: string, weights: number[], className: string) { +async function awaitFonts([name, weights, className]: [string, number[], string]) { try { const fonts = weights.map(weight => new FontFaceObserver(name, { weight })); await Promise.all(fonts.map(font => font.load())); diff --git a/src/app/a11y/AwaitI18N.tsx b/src/app/a11y/AwaitI18N.tsx index 045db77b26..5b4d8ae4de 100644 --- a/src/app/a11y/AwaitI18N.tsx +++ b/src/app/a11y/AwaitI18N.tsx @@ -1,12 +1,11 @@ import { FC } from 'react'; -import useSWR from 'swr'; - import { onInited } from 'lib/i18n'; +import { useTypedSWR } from 'lib/swr'; import { delay } from 'lib/utils'; const AwaitI18N: FC = () => { - useSWR('i18n', awaitI18n, { + useTypedSWR('i18n', awaitI18n, { suspense: true, shouldRetryOnError: false, revalidateOnFocus: false, diff --git a/src/app/pages/AddAsset/AddAsset.tsx b/src/app/pages/AddAsset/AddAsset.tsx index 8d5fb100cd..e149966158 100644 --- a/src/app/pages/AddAsset/AddAsset.tsx +++ b/src/app/pages/AddAsset/AddAsset.tsx @@ -3,7 +3,7 @@ import React, { FC, ReactNode, useCallback, useEffect, useRef, useMemo } from 'r import classNames from 'clsx'; import { FormContextValues, useForm } from 'react-hook-form'; import { useDispatch } from 'react-redux'; -import { useSWRConfig } from 'swr'; +import { useSWRConfig, unstable_serialize } from 'swr'; import { useDebouncedCallback } from 'use-debounce'; import { Alert, FormField, FormSubmitButton, NoSpaceField } from 'app/atoms'; @@ -228,7 +228,7 @@ const Form: FC = () => { Repo.toAccountTokenKey(chainId, accountPkh, tokenSlug) ); - swrCache.delete(getBalanceSWRKey(tezos, tokenSlug, accountPkh)); + swrCache.delete(unstable_serialize(getBalanceSWRKey(tezos, tokenSlug, accountPkh))); formAnalytics.trackSubmitSuccess(); diff --git a/src/app/pages/Buy/Crypto/Exolix/steps/InitialStep.tsx b/src/app/pages/Buy/Crypto/Exolix/steps/InitialStep.tsx index fedb8a8782..e9b52a4a4b 100644 --- a/src/app/pages/Buy/Crypto/Exolix/steps/InitialStep.tsx +++ b/src/app/pages/Buy/Crypto/Exolix/steps/InitialStep.tsx @@ -1,7 +1,6 @@ import React, { FC, useEffect, useState, useMemo } from 'react'; import classNames from 'clsx'; -import useSWR from 'swr'; import { useDebounce } from 'use-debounce'; import { FormSubmitButton } from 'app/atoms'; @@ -12,6 +11,7 @@ import WarningComponent from 'app/pages/Buy/Crypto/Exolix/steps/WarningComponent import { TopUpInput } from 'app/templates/TopUpInput'; import { useAssetUSDPrice } from 'lib/fiat-currency'; import { T } from 'lib/i18n'; +import { useTypedSWR } from 'lib/swr'; import { useAccount } from 'lib/temple/front'; import { EXOLIX_PRIVICY_LINK, EXOLIX_TERMS_LINK, outputTokensList } from '../config'; @@ -54,7 +54,7 @@ const InitialStep: FC = ({ exchangeData, setExchangeData, setStep, isErro const coinToPriceUSD = useAssetUSDPrice(coinTo.slug!); - const { data: currencies, isValidating: isCurrenciesLoading } = useSWR(['exolix/api/currencies'], getCurrencies); + const { data: currencies, isValidating: isCurrenciesLoading } = useTypedSWR(['exolix/api/currencies'], getCurrencies); const currenciesCount = useCurrenciesCount(); @@ -82,13 +82,13 @@ const InitialStep: FC = ({ exchangeData, setExchangeData, setStep, isErro } }; - const { data: ratesData } = useSWR(['exolix/api/rate', coinFrom, coinTo, amount], () => + const { data: ratesData } = useTypedSWR(['exolix/api/rate', coinFrom, coinTo, amount], () => queryExchange({ coinFrom: coinFrom.code, - coinFromNetwork: coinFrom.network!.code, + coinFromNetwork: coinFrom.network.code, amount: amount ?? 0, coinTo: coinTo.code, - coinToNetwork: coinTo!.network.code + coinToNetwork: coinTo.network.code }) ); @@ -103,10 +103,10 @@ const InitialStep: FC = ({ exchangeData, setExchangeData, setStep, isErro const { toAmount: maxCoinFromAmount } = await queryExchange({ coinFrom: coinTo.code, - coinFromNetwork: coinTo.network!.code, + coinFromNetwork: coinTo.network.code, amount: maxCoinToAmount, coinTo: coinFrom.code, - coinToNetwork: coinFrom.network!.code + coinToNetwork: coinFrom.network.code }); setMaxAmountFetched(maxCoinFromAmount); diff --git a/src/app/pages/Home/OtherComponents/BakingSection.tsx b/src/app/pages/Home/OtherComponents/BakingSection.tsx index d1c900f945..025815ebab 100644 --- a/src/app/pages/Home/OtherComponents/BakingSection.tsx +++ b/src/app/pages/Home/OtherComponents/BakingSection.tsx @@ -86,7 +86,7 @@ const BakingSection = memo(() => { }; const getBakingHistory = useCallback( - async (_k: string, accountPkh: string) => { + async ([, accountPkh, , chainId]: [string, string, string | nullish, string | nullish]) => { if (!isKnownChainId(chainId!)) { return []; } @@ -97,7 +97,7 @@ const BakingSection = memo(() => { })) || [] ); }, - [chainId] + [] ); const { data: bakingHistory, isValidating: loadingBakingHistory } = useRetryableSWR( ['baking-history', acc.publicKeyHash, myBakerPkh, chainId], diff --git a/src/app/pages/ImportAccount/ManagedKTForm.tsx b/src/app/pages/ImportAccount/ManagedKTForm.tsx index 6b51221e63..63df2cb72c 100644 --- a/src/app/pages/ImportAccount/ManagedKTForm.tsx +++ b/src/app/pages/ImportAccount/ManagedKTForm.tsx @@ -36,12 +36,11 @@ export const ManagedKTForm: FC = () => { const [error, setError] = useState(null); const queryKey = useMemo( - () => - [ - 'get-accounts-contracts', - chainId, - ...accounts.filter(({ type }) => type !== TempleAccountType.ManagedKT).map(({ publicKeyHash }) => publicKeyHash) - ] as string[], + () => [ + 'get-accounts-contracts', + chainId!, + ...accounts.filter(({ type }) => type !== TempleAccountType.ManagedKT).map(({ publicKeyHash }) => publicKeyHash) + ], [accounts, chainId] ); const { data: usersContracts = [] } = useRetryableSWR(queryKey, getUsersContracts, {}); @@ -224,7 +223,7 @@ export const ManagedKTForm: FC = () => { ); }; -const getUsersContracts = async (_k: string, chainId: string, ...accounts: string[]) => { +const getUsersContracts = async ([, chainId, ...accounts]: string[]) => { if (!isKnownChainId(chainId)) { return []; } diff --git a/src/app/pages/ImportAccount/WatchOnlyForm.tsx b/src/app/pages/ImportAccount/WatchOnlyForm.tsx index d2ab0eed3a..aa37371602 100644 --- a/src/app/pages/ImportAccount/WatchOnlyForm.tsx +++ b/src/app/pages/ImportAccount/WatchOnlyForm.tsx @@ -1,12 +1,12 @@ import React, { FC, ReactNode, useCallback, useMemo, useRef, useState } from 'react'; import { useForm, Controller } from 'react-hook-form'; -import useSWR from 'swr'; import { Alert, FormSubmitButton, NoSpaceField } from 'app/atoms'; import { useFormAnalytics } from 'lib/analytics'; import { T, t } from 'lib/i18n'; import { useTempleClient, useTezos, useTezosDomainsClient, validateDelegate } from 'lib/temple/front'; +import { useTezosAddressByDomainName } from 'lib/temple/front/tzdns'; import { isAddressValid, isKTAddress } from 'lib/temple/helpers'; import { delay } from 'lib/utils'; @@ -32,14 +32,7 @@ export const WatchOnlyForm: FC = () => { const addressValue = watch('address'); - const domainAddressFactory = useCallback( - (_k: string, _checksum: string, address: string) => domainsClient.resolver.resolveNameToAddress(address), - [domainsClient] - ); - const { data: resolvedAddress } = useSWR(['tzdns-address', tezos.checksum, addressValue], domainAddressFactory, { - shouldRetryOnError: false, - revalidateOnFocus: false - }); + const { data: resolvedAddress } = useTezosAddressByDomainName(addressValue); const finalAddress = useMemo( () => (resolvedAddress && resolvedAddress !== null ? resolvedAddress : addressValue), diff --git a/src/app/pages/Receive/Receive.tsx b/src/app/pages/Receive/Receive.tsx index 1eba1f3f19..1fa7fbda78 100644 --- a/src/app/pages/Receive/Receive.tsx +++ b/src/app/pages/Receive/Receive.tsx @@ -1,8 +1,7 @@ -import React, { FC, memo, useCallback, useEffect } from 'react'; +import React, { FC, memo, useEffect } from 'react'; import classNames from 'clsx'; import { QRCode } from 'react-qr-svg'; -import useSWR from 'swr'; import { FormField } from 'app/atoms'; import { ReactComponent as CopyIcon } from 'app/icons/copy.svg'; @@ -13,7 +12,8 @@ import PageLayout from 'app/layouts/PageLayout'; import ViewsSwitcher, { ViewsSwitcherProps } from 'app/templates/ViewsSwitcher/ViewsSwitcher'; import { setTestID } from 'lib/analytics'; import { T, t } from 'lib/i18n'; -import { useAccount, useTezos, useTezosDomainsClient } from 'lib/temple/front'; +import { useAccount, useTezosDomainsClient } from 'lib/temple/front'; +import { useTezosDomainNameByAddress } from 'lib/temple/front/tzdns'; import { useSafeState } from 'lib/ui/hooks'; import useCopyToClipboard from 'lib/ui/useCopyToClipboard'; @@ -35,23 +35,13 @@ const ADDRESS_FIELD_VIEWS = [ const Receive: FC = () => { const account = useAccount(); - const tezos = useTezos(); - const { resolver: domainsResolver, isSupported } = useTezosDomainsClient(); + const { isSupported } = useTezosDomainsClient(); const address = account.publicKeyHash; const { fieldRef, copy, copied } = useCopyToClipboard(); const [activeView, setActiveView] = useSafeState(ADDRESS_FIELD_VIEWS[1]); - const resolveDomainReverseName = useCallback( - (_k: string, pkh: string) => domainsResolver.resolveAddressToName(pkh), - [domainsResolver] - ); - - const { data: reverseName } = useSWR( - () => ['tzdns-reverse-name', address, tezos.checksum], - resolveDomainReverseName, - { shouldRetryOnError: false, revalidateOnFocus: false } - ); + const { data: reverseName } = useTezosDomainNameByAddress(address); useEffect(() => { if (!isSupported) { diff --git a/src/app/templates/AddressChip.tsx b/src/app/templates/AddressChip.tsx index 61def0ae11..922ecc7097 100644 --- a/src/app/templates/AddressChip.tsx +++ b/src/app/templates/AddressChip.tsx @@ -1,13 +1,13 @@ import React, { FC, useCallback } from 'react'; import classNames from 'clsx'; -import useSWR from 'swr'; import { Button, HashChip } from 'app/atoms'; import { ReactComponent as GlobeIcon } from 'app/icons/globe.svg'; import { ReactComponent as HashIcon } from 'app/icons/hash.svg'; import { TestIDProps } from 'lib/analytics'; -import { useTezos, useTezosDomainsClient, useStorage } from 'lib/temple/front'; +import { useStorage } from 'lib/temple/front'; +import { useTezosDomainNameByAddress } from 'lib/temple/front/tzdns'; type Props = TestIDProps & { pkh: string; @@ -19,18 +19,7 @@ type Props = TestIDProps & { const TZDNS_MODE_ON_STORAGE_KEY = 'domain-displayed'; const AddressChip: FC = ({ pkh, className, small, modeSwitch, ...rest }) => { - const tezos = useTezos(); - const { resolver: domainsResolver } = useTezosDomainsClient(); - - const resolveDomainReverseName = useCallback( - (_k: string, pkh: string) => domainsResolver.resolveAddressToName(pkh), - [domainsResolver] - ); - - const { data: tzdnsName } = useSWR(() => ['pkh-tzdns-name', pkh, tezos.checksum], resolveDomainReverseName, { - shouldRetryOnError: false, - revalidateOnFocus: false - }); + const { data: tzdnsName } = useTezosDomainNameByAddress(pkh); const [domainDisplayed, setDomainDisplayed] = useStorage(TZDNS_MODE_ON_STORAGE_KEY, false); diff --git a/src/app/templates/DelegateForm.tsx b/src/app/templates/DelegateForm.tsx index 8725f1cba3..0c22806a5c 100644 --- a/src/app/templates/DelegateForm.tsx +++ b/src/app/templates/DelegateForm.tsx @@ -4,7 +4,6 @@ import { DEFAULT_FEE, WalletOperation } from '@taquito/taquito'; import BigNumber from 'bignumber.js'; import classNames from 'clsx'; import { Control, Controller, FieldError, FormStateProxy, NestDataObject, useForm } from 'react-hook-form'; -import useSWR from 'swr'; import browser from 'webextension-polyfill'; import { Alert, Button, FormSubmitButton, NoSpaceField } from 'app/atoms'; @@ -23,6 +22,7 @@ import { fetchTezosBalance } from 'lib/balances'; import { BLOCK_DURATION } from 'lib/fixed-times'; import { TID, T, t } from 'lib/i18n'; import { setDelegate } from 'lib/michelson'; +import { useTypedSWR } from 'lib/swr'; import { loadContract } from 'lib/temple/contract'; import { Baker, @@ -37,10 +37,11 @@ import { useTezosDomainsClient, validateDelegate } from 'lib/temple/front'; +import { useTezosAddressByDomainName } from 'lib/temple/front/tzdns'; import { hasManager, isAddressValid, isKTAddress, mutezToTz, tzToMutez } from 'lib/temple/helpers'; import { TempleAccountType } from 'lib/temple/types'; import { useSafeState } from 'lib/ui/hooks'; -import { delay } from 'lib/utils'; +import { delay, fifoResolve } from 'lib/utils'; import { Link, useLocation } from 'lib/woozie'; import { useUserTestingGroupNameSelector } from '../store/ab-testing/selectors'; @@ -92,14 +93,7 @@ const DelegateForm: FC = () => { () => toValue && isDomainNameValid(toValue, domainsClient), [toValue, domainsClient] ); - const domainAddressFactory = useCallback( - (_k: string, _checksum: string, value: string) => domainsClient.resolver.resolveNameToAddress(value), - [domainsClient] - ); - const { data: resolvedAddress } = useSWR(['tzdns-address', tezos.checksum, toValue], domainAddressFactory, { - shouldRetryOnError: false, - revalidateOnFocus: false - }); + const { data: resolvedAddress } = useTezosAddressByDomainName(toValue); const toFieldRef = useRef(null); @@ -185,11 +179,15 @@ const DelegateForm: FC = () => { data: baseFee, error: estimateBaseFeeError, isValidating: estimating - } = useSWR(() => (toFilled ? ['delegate-base-fee', tezos.checksum, accountPkh, toResolved] : null), estimateBaseFee, { - shouldRetryOnError: false, - focusThrottleInterval: 10_000, - dedupingInterval: BLOCK_DURATION - }); + } = useTypedSWR( + () => (toFilled ? ['delegate-base-fee', tezos.checksum, accountPkh, toResolved] : null), + estimateBaseFee, + { + shouldRetryOnError: false, + focusThrottleInterval: 10_000, + dedupingInterval: BLOCK_DURATION + } + ); const baseFeeError = baseFee instanceof Error ? baseFee : estimateBaseFeeError; const estimationError = !estimating ? baseFeeError : null; @@ -202,6 +200,11 @@ const DelegateForm: FC = () => { return undefined; }, [balanceNum, baseFee]); + const fifoValidateDelegate = useMemo( + () => fifoResolve((value: any) => validateDelegate(value, domainsClient, validateAddress)), + [domainsClient] + ); + const handleFeeFieldChange = useCallback( ([v]) => (maxAddFee && v > maxAddFee ? maxAddFee : v), [maxAddFee] @@ -314,9 +317,7 @@ const DelegateForm: FC = () => { name="to" as={} control={control} - rules={{ - validate: (value: any) => validateDelegate(value, domainsClient, validateAddress) - }} + rules={{ validate: fifoValidateDelegate }} onChange={([v]) => v} onFocus={() => toFieldRef.current?.focus()} textarea diff --git a/src/app/templates/InternalConfirmation.tsx b/src/app/templates/InternalConfirmation.tsx index 55330a0b6c..65bb89f6d5 100644 --- a/src/app/templates/InternalConfirmation.tsx +++ b/src/app/templates/InternalConfirmation.tsx @@ -105,7 +105,7 @@ const InternalConfirmation: FC = ({ payload, onConfir if (tzToMutez(tezBalance).isLessThanOrEqualTo(totalTransactionCost)) { dispatch(setOnRampPossibilityAction(true)); } - }, [tezBalance, totalTransactionCost]); + }, [dispatch, tezBalance, totalTransactionCost]); const signPayloadFormats: ViewsSwitcherItemProps[] = useMemo(() => { if (payload.type === 'operations') { diff --git a/src/app/templates/SendForm/Form.tsx b/src/app/templates/SendForm/Form.tsx index cdeaeefdcb..bb8170f9ae 100644 --- a/src/app/templates/SendForm/Form.tsx +++ b/src/app/templates/SendForm/Form.tsx @@ -15,7 +15,6 @@ import { DEFAULT_FEE, TransferParams, WalletOperation, Estimate } from '@taquito import BigNumber from 'bignumber.js'; import classNames from 'clsx'; import { Controller, FieldError, useForm } from 'react-hook-form'; -import useSWR from 'swr'; import { NoSpaceField } from 'app/atoms'; import AssetField from 'app/atoms/AssetField'; @@ -36,6 +35,7 @@ import { BLOCK_DURATION } from 'lib/fixed-times'; import { toLocalFixed, T, t } from 'lib/i18n'; import { AssetMetadataBase, useAssetMetadata, getAssetSymbol } from 'lib/metadata'; import { transferImplicit, transferToContract } from 'lib/michelson'; +import { useTypedSWR } from 'lib/swr'; import { loadContract } from 'lib/temple/contract'; import { ReactiveTezosToolkit, @@ -48,6 +48,7 @@ import { useFilteredContacts, validateRecipient } from 'lib/temple/front'; +import { useTezosAddressByDomainName } from 'lib/temple/front/tzdns'; import { hasManager, isAddressValid, isKTAddress, mutezToTz, tzToMutez } from 'lib/temple/helpers'; import { TempleAccountType, TempleAccount, TempleNetworkType } from 'lib/temple/types'; import { useSafeState } from 'lib/ui/hooks'; @@ -129,7 +130,7 @@ export const Form: FC = ({ assetSlug, setOperation, onAddContactReque const amount = new BigNumber(getValues().amount); setValue( 'amount', - (newShouldUseFiat ? amount.multipliedBy(assetPrice!) : amount.div(assetPrice!)).toFormat( + (newShouldUseFiat ? amount.multipliedBy(assetPrice) : amount.div(assetPrice)).toFormat( newShouldUseFiat ? 2 : 6, BigNumber.ROUND_FLOOR, { @@ -164,14 +165,7 @@ export const Form: FC = ({ assetSlug, setOperation, onAddContactReque [toValue, domainsClient] ); - const domainAddressFactory = useCallback( - (_k: string, _checksum: string, address: string) => domainsClient.resolver.resolveNameToAddress(address), - [domainsClient] - ); - const { data: resolvedAddress } = useSWR(['tzdns-address', tezos.checksum, toValue], domainAddressFactory, { - shouldRetryOnError: false, - revalidateOnFocus: false - }); + const { data: resolvedAddress } = useTezosAddressByDomainName(toValue); const toFilled = useMemo( () => (resolvedAddress ? toFilledWithDomain : toFilledWithAddress), @@ -257,7 +251,7 @@ export const Form: FC = ({ assetSlug, setOperation, onAddContactReque data: baseFee, error: estimateBaseFeeError, isValidating: estimating - } = useSWR( + } = useTypedSWR( () => (toFilled ? ['transfer-base-fee', tezos.checksum, assetSlug, accountPkh, toResolved] : null), estimateBaseFee, { diff --git a/src/lib/i18n/core.ts b/src/lib/i18n/core.ts index c3c2e03cee..dd868ec336 100644 --- a/src/lib/i18n/core.ts +++ b/src/lib/i18n/core.ts @@ -29,43 +29,36 @@ let fetchedLocaleMessages: FetchedLocaleMessages = { let cldrLocale = cldrjsLocales.en; export async function init() { - const refetched: FetchedLocaleMessages = { - target: null, - fallback: null - }; - const saved = getSavedLocale(); + const deflt = getDefaultLocale(); + const native = getNativeLocale(); - if (saved) { - const native = getNativeLocale(); - - await Promise.all([ - // Fetch target locale messages if needed - (async () => { - if (!areLocalesEqual(saved, native)) { - refetched.target = await fetchLocaleMessages(saved); - } - })(), - // Fetch fallback locale messages if needed - (async () => { - const deflt = getDefaultLocale(); - if (!areLocalesEqual(deflt, native) && !areLocalesEqual(deflt, saved)) { - refetched.fallback = await fetchLocaleMessages(deflt); - } - })() - ]); - } + const [target, fallback] = await Promise.all([ + !saved || areLocalesEqual(saved, native) ? null : fetchLocaleMessages(saved), + areLocalesEqual(deflt, native) || (saved && areLocalesEqual(deflt, saved)) ? null : fetchLocaleMessages(deflt) + ]); - fetchedLocaleMessages = refetched; + fetchedLocaleMessages = { target, fallback }; cldrLocale = (cldrjsLocales as Record)[getCurrentLocale()] || cldrjsLocales.en; } export function getMessage(messageName: string, substitutions?: Substitutions) { - const val = fetchedLocaleMessages.target?.[messageName] ?? fetchedLocaleMessages.fallback?.[messageName]; + const { target, fallback } = fetchedLocaleMessages; + const targetVal = target?.[messageName]; + + if (targetVal) return applySubstitutions(targetVal, substitutions); + + if (!target) { + const nativeVal = browser.i18n.getMessage(messageName, substitutions); + + if (nativeVal) return nativeVal; + } - if (val) return applySubstitutions(val, substitutions); + const fallbackVal = fallback?.[messageName]; - return browser.i18n.getMessage(messageName, substitutions); + return fallbackVal + ? applySubstitutions(fallbackVal, substitutions) + : browser.i18n.getMessage(messageName, substitutions) ?? ''; } export function getDateFnsLocale() { diff --git a/src/lib/swr/index.ts b/src/lib/swr/index.ts index c7b117fce8..714301ce0d 100644 --- a/src/lib/swr/index.ts +++ b/src/lib/swr/index.ts @@ -1,4 +1,13 @@ -import useSWR, { Key, Fetcher, SWRConfiguration, SWRResponse } from 'swr'; +import useSWR, { Key, SWRConfiguration, SWRResponse } from 'swr'; +import { FetcherResponse } from 'swr/_internal'; + +type Fetcher = (arg: SWRKey) => FetcherResponse; + +export const useTypedSWR = ( + key: SWRKey, + fetcher: Fetcher | null, + config?: SWRConfiguration> +): SWRResponse => useSWR(key, fetcher, config); export const useRetryableSWR = ( key: SWRKey, diff --git a/src/lib/temple/front/assets.ts b/src/lib/temple/front/assets.ts index a0024e60df..d80a1411b5 100644 --- a/src/lib/temple/front/assets.ts +++ b/src/lib/temple/front/assets.ts @@ -1,7 +1,7 @@ import { useCallback, useMemo } from 'react'; import { isDefined } from '@rnw-community/shared'; -import { ScopedMutator } from 'swr/dist/types'; +import { ScopedMutator } from 'swr/_internal'; import { useTokensMetadataSelector } from 'app/store/tokens-metadata/selectors'; import { isTezAsset, TEMPLE_TOKEN_SLUG } from 'lib/assets'; diff --git a/src/lib/temple/front/balance.ts b/src/lib/temple/front/balance.ts index 44d755d50c..e0323cf560 100644 --- a/src/lib/temple/front/balance.ts +++ b/src/lib/temple/front/balance.ts @@ -3,6 +3,7 @@ import { useCallback, useMemo } from 'react'; import BigNumber from 'bignumber.js'; import { fetchBalance } from 'lib/balances'; +import { TOKENS_SYNC_INTERVAL } from 'lib/fixed-times'; import { useAssetMetadata } from 'lib/metadata'; import { useRetryableSWR } from 'lib/swr'; import { michelEncoder, loadFastRpcClient } from 'lib/temple/helpers'; @@ -41,7 +42,8 @@ export function useBalance(assetSlug: string, address: string, opts: UseBalanceO suspense: opts.suspense ?? true, revalidateOnFocus: false, dedupingInterval: 20_000, - fallbackData: opts.initial + fallbackData: opts.initial, + refreshInterval: TOKENS_SYNC_INTERVAL }); } diff --git a/src/lib/temple/front/storage.ts b/src/lib/temple/front/storage.ts index 17a6d36963..4c3877738f 100644 --- a/src/lib/temple/front/storage.ts +++ b/src/lib/temple/front/storage.ts @@ -9,7 +9,7 @@ import { useDidUpdate } from 'lib/ui/hooks'; export function useStorage(key: string): [T | null | undefined, (val: SetStateAction) => Promise]; export function useStorage(key: string, fallback: T): [T, (val: SetStateAction) => Promise]; export function useStorage(key: string, fallback?: T) { - const { data, mutate } = useRetryableSWR(key, fetchFromStorage, { + const { data, mutate } = useRetryableSWR(key, fetchFromStorage, { suspense: true, revalidateOnFocus: false, revalidateOnReconnect: false @@ -34,7 +34,7 @@ export function useStorage(key: string, fallback?: T) { export function usePassiveStorage(key: string): [T | null | undefined, Dispatch>]; export function usePassiveStorage(key: string, fallback: T): [T, Dispatch>]; export function usePassiveStorage(key: string, fallback?: T) { - const { data } = useRetryableSWR(key, fetchFromStorage, { + const { data } = useRetryableSWR(key, fetchFromStorage, { suspense: true, revalidateOnFocus: false, revalidateOnReconnect: false diff --git a/src/lib/temple/front/sync-tokens.ts b/src/lib/temple/front/sync-tokens.ts index ca77ced84c..c2fc2f4bc6 100644 --- a/src/lib/temple/front/sync-tokens.ts +++ b/src/lib/temple/front/sync-tokens.ts @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react'; import constate from 'constate'; import { useSWRConfig } from 'swr'; -import { ScopedMutator } from 'swr/dist/types'; +import { ScopedMutator } from 'swr/_internal'; import { fetchWhitelistTokenSlugs } from 'lib/apis/temple'; import { TzktAccountToken, fetchTzktTokens } from 'lib/apis/tzkt'; diff --git a/src/lib/temple/front/tzdns.ts b/src/lib/temple/front/tzdns.ts index fa0b8f07bd..36acd649e6 100644 --- a/src/lib/temple/front/tzdns.ts +++ b/src/lib/temple/front/tzdns.ts @@ -1,9 +1,10 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { TezosToolkit } from '@taquito/taquito'; import { DomainNameValidationResult, isTezosDomainsSupportedNetwork } from '@tezos-domains/core'; import { TaquitoTezosDomainsClient } from '@tezos-domains/taquito-client'; +import { useTypedSWR } from 'lib/swr'; import { NETWORK_IDS } from 'lib/temple/networks'; import { useTezos, useChainId } from './ready'; @@ -25,3 +26,32 @@ export function useTezosDomainsClient() { const networkId = NETWORK_IDS.get(chainId)!; return useMemo(() => getClient(networkId === 'mainnet' ? networkId : 'custom', tezos), [networkId, tezos]); } + +export function useTezosAddressByDomainName(domainName: string) { + const domainsClient = useTezosDomainsClient(); + const tezos = useTezos(); + + const domainAddressFactory = useCallback( + ([, , name]: [string, string, string]) => domainsClient.resolver.resolveNameToAddress(name), + [domainsClient] + ); + + return useTypedSWR(['tzdns-address', tezos.checksum, domainName], domainAddressFactory, { + shouldRetryOnError: false, + revalidateOnFocus: false + }); +} + +export function useTezosDomainNameByAddress(address: string) { + const { resolver: domainsResolver } = useTezosDomainsClient(); + const tezos = useTezos(); + const resolveDomainReverseName = useCallback( + ([, pkh]: [string, string, string]) => domainsResolver.resolveAddressToName(pkh), + [domainsResolver] + ); + + return useTypedSWR(['tzdns-reverse-name', address, tezos.checksum], resolveDomainReverseName, { + shouldRetryOnError: false, + revalidateOnFocus: false + }); +} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 76b9f6a76f..be9e65b3b5 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1,3 +1,6 @@ +import { Mutex } from 'async-mutex'; +import { noop } from 'lodash'; + export { arrayBufferToString, stringToArrayBuffer, uInt8ArrayToString, stringToUInt8Array } from './buffers'; /** From lodash */ @@ -8,6 +11,28 @@ export const isTruthy = (value: T): value is Truthy => Boolean(value); /** With strict equality check (i.e. `===`) */ export const filterUnique = (array: T[]) => Array.from(new Set(array)); +/** Creates the function that runs promises paralelly but resolves them in FIFO order. */ +export const fifoResolve = (fn: (...args: A) => Promise) => { + const queueMutex = new Mutex(); + const queue: Array> = []; + + return async (...args: A): Promise => { + const promise = fn(...args); + await queueMutex.runExclusive(async () => queue.push(promise)); + + try { + const result = await promise; + const prevPromises = await queueMutex.runExclusive(async () => queue.slice(0, queue.indexOf(promise))); + await Promise.all(prevPromises.map(promise => promise.catch(noop))); + await delay(prevPromises.length + 1); + + return result; + } finally { + await queueMutex.runExclusive(async () => queue.splice(queue.indexOf(promise), 1)); + } + }; +}; + const DEFAULT_DELAY = 300; export const delay = (ms = DEFAULT_DELAY) => new Promise(res => setTimeout(res, ms)); diff --git a/src/lib/utils/utils.test.ts b/src/lib/utils/utils.test.ts index 20cb86aa35..d1aa8d7aeb 100644 --- a/src/lib/utils/utils.test.ts +++ b/src/lib/utils/utils.test.ts @@ -1,4 +1,4 @@ -import { isTruthy, createQueue, delay } from './index'; +import { isTruthy, createQueue, delay, fifoResolve } from './index'; /** See: https://developer.mozilla.org/en-US/docs/Glossary/Falsy */ const ALL_FALSY_VALUES = [false, 0, -0, BigInt(0), '', NaN, null, undefined]; @@ -47,6 +47,29 @@ describe('Queue', () => { }); }); +describe('fifoResolve', () => { + it('should run promises paralelly but resolve them in FIFO order', async () => { + const t0 = Date.now(); + const ids: number[] = []; + const fn = fifoResolve((ms: number) => delay(ms)); + const pushAfterFnResolves = (ms: number, id: number) => fn(ms).then(() => ids.push(id)); + await Promise.all([ + pushAfterFnResolves(300, 1), + pushAfterFnResolves(200, 2), + pushAfterFnResolves(100, 3), + pushAfterFnResolves(0, 4), + pushAfterFnResolves(300, 5), + pushAfterFnResolves(200, 6), + pushAfterFnResolves(100, 7), + pushAfterFnResolves(0, 8) + ]); + const t1 = Date.now(); + expect(t1 - t0).toBeGreaterThanOrEqual(300); + expect(t1 - t0).toBeLessThan(400); + expect(ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); + }); +}); + async function withDelay(ms: number, factory: () => any) { await delay(ms); diff --git a/yarn.lock b/yarn.lock index 91c22cef79..6557dac438 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2310,11 +2310,6 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== -"@serh11p/jest-webextension-mock@4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@serh11p/jest-webextension-mock/-/jest-webextension-mock-4.0.0.tgz#c3b2a00e8c758e156a4a922718b35183e9023af9" - integrity sha512-SQDFFOJGwQpaPopzYkFgzdDdi8tL8US/XM2UjBu4rmLLztTI/wOSecwXj/i3lhOUwOs3bHO2Zz9eRu2rPy5p9A== - "@sinclair/typebox@^0.24.1": version "0.24.44" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.44.tgz#0a0aa3bf4a155a678418527342a3ee84bd8caa5c" @@ -2796,6 +2791,11 @@ dependencies: nanoid "^3.1.25" +"@temple-wallet/jest-webextension-mock@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@temple-wallet/jest-webextension-mock/-/jest-webextension-mock-4.1.0.tgz#6834acd98297eb1c76b85a2585a0584ff5578e8f" + integrity sha512-1D/3WpaJS9rk5SENnFB1RZ3oeYkrfDvCV4qyIYjAD6uWe6RHVzcGxQ8UPtSQ/MRrcqjT95q2VwDNxek+9z6+yw== + "@temple-wallet/wallet-address-validator@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@temple-wallet/wallet-address-validator/-/wallet-address-validator-0.4.3.tgz#4bb7905a824e290872b7a7fc449df2e8c332e9e1" @@ -4437,6 +4437,13 @@ ast-types-flow@^0.0.7: resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= +async-mutex@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.0.tgz#ae8048cd4d04ace94347507504b3cf15e631c25f" + integrity sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA== + dependencies: + tslib "^2.4.0" + async-retry@1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" @@ -5133,6 +5140,11 @@ clean-webpack-plugin@4.0.0, clean-webpack-plugin@^4.0.0: dependencies: del "^4.1.1" +client-only@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -11665,10 +11677,13 @@ svgson@^4.0.0: omit-deep "0.3.0" xml-reader "2.4.3" -swr@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8" - integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw== +swr@2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07" + integrity sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ== + dependencies: + client-only "^0.0.1" + use-sync-external-store "^1.2.0" symbol-observable@^4.0.0: version "4.0.0" @@ -12282,7 +12297,7 @@ use-onclickoutside@0.4.1: are-passive-events-supported "^1.1.1" use-latest "^1.2.1" -use-sync-external-store@^1.0.0: +use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==