From 6ab31f4b683265d6d2caf1f119a4b7351ed522b3 Mon Sep 17 00:00:00 2001 From: alter-eggo Date: Sat, 23 Mar 2024 17:31:46 +0400 Subject: [PATCH] feat: add blockstream and hiro api rate limiters, closes #4926 --- package.json | 2 +- pnpm-lock.yaml | 36 ++++++++----- .../common/hooks/account/use-account-names.ts | 11 ++-- src/app/components/account-total-balance.tsx | 15 ++++-- src/app/components/account/account-name.tsx | 14 ++++- .../_collectible-types/collectible-image.tsx | 4 +- .../components/switch-account-list-item.tsx | 7 ++- .../choose-account/components/accounts.tsx | 2 +- .../components/network-list-item.layout.tsx | 15 ++---- .../account-list-item.tsx | 2 +- .../address/transactions-by-address.query.ts | 10 ++-- .../bitcoin/address/utxos-by-address.query.ts | 5 +- src/app/query/bitcoin/bitcoin-client.ts | 23 +++++--- .../query/bitcoin/blockstream-rate-limiter.ts | 18 +++++++ .../inscription-text-content.query.ts | 8 ++- .../bitcoin/ordinals/inscriptions.query.ts | 54 ++++++++++++++++--- .../query/stacks/balance/stx-balance.query.ts | 21 ++++++-- src/app/query/stacks/bns/bns.query.ts | 22 +++++--- src/app/query/stacks/bns/bns.utils.ts | 34 ++++++++---- .../query/stacks/contract/contract.query.ts | 17 +++--- src/app/query/stacks/fees/fees.query.ts | 20 ++++--- .../{rate-limiter.ts => hiro-rate-limiter.ts} | 19 +++---- src/app/query/stacks/mempool/mempool.query.ts | 10 ++-- src/app/query/stacks/network/network.query.ts | 12 +++-- .../stacks/nonce/account-nonces.query.ts | 19 ++++--- .../fungible-token-metadata.query.ts | 10 ++-- .../non-fungible-token-holdings.query.ts | 22 +++++--- .../non-fungible-token-metadata.query.ts | 16 +++--- .../transactions/transactions-by-id.query.ts | 23 +++++--- .../transactions-with-transfers.query.ts | 22 +++++--- tests/page-object-models/home.page.ts | 5 +- theme/global/shimmer-styles.ts | 10 ++++ theme/keyframes.ts | 5 ++ 33 files changed, 356 insertions(+), 157 deletions(-) create mode 100644 src/app/query/bitcoin/blockstream-rate-limiter.ts rename src/app/query/stacks/{rate-limiter.ts => hiro-rate-limiter.ts} (62%) create mode 100644 theme/global/shimmer-styles.ts diff --git a/package.json b/package.json index fe08cfbbabf..96e2d4be340 100644 --- a/package.json +++ b/package.json @@ -206,13 +206,13 @@ "jotai-redux": "0.2.1", "jsontokens": "4.0.1", "ledger-bitcoin": "0.2.3", - "limiter": "2.1.0", "lodash.get": "4.4.2", "lodash.isequal": "4.5.0", "lodash.uniqby": "4.7.0", "micro-packed": "0.3.2", "object-hash": "3.0.0", "observable-hooks": "4.2.3", + "p-queue": "8.0.1", "pino": "8.19.0", "postcss-preset-env": "9.4.0", "prism-react-renderer": "2.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a98b55fac59..2e200048c32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -241,9 +241,6 @@ dependencies: ledger-bitcoin: specifier: 0.2.3 version: 0.2.3 - limiter: - specifier: 2.1.0 - version: 2.1.0 lodash.get: specifier: 4.4.2 version: 4.4.2 @@ -262,6 +259,9 @@ dependencies: observable-hooks: specifier: 4.2.3 version: 4.2.3(react-dom@18.2.0)(react@18.2.0)(rxjs@7.8.1) + p-queue: + specifier: 8.0.1 + version: 8.0.1 pino: specifier: 8.19.0 version: 8.19.0 @@ -14131,6 +14131,10 @@ packages: /eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + /eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + dev: false + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -16675,10 +16679,6 @@ packages: engines: {node: '>=12.20'} dev: true - /just-performance@4.3.0: - resolution: {integrity: sha512-L7RjvtJsL0QO8xFs5wEoDDzzJwoiowRw6Rn/GnvldlchS2JQr9wFYPiwZcDfrbbujEKqKN0tvENdbjXdYhDp5Q==} - dev: false - /jwa@1.4.1: resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} dependencies: @@ -16931,12 +16931,6 @@ packages: lightningcss-win32-x64-msvc: 1.23.0 dev: true - /limiter@2.1.0: - resolution: {integrity: sha512-361TYz6iay6n+9KvUUImqdLuFigK+K79qrUtBsXhJTLdH4rIt/r1y8r1iozwh8KbZNpujbFTSh74mJ7bwbAMOw==} - dependencies: - just-performance: 4.3.0 - dev: false - /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true @@ -18836,6 +18830,14 @@ packages: aggregate-error: 3.1.0 dev: true + /p-queue@8.0.1: + resolution: {integrity: sha512-NXzu9aQJTAzbBqOt2hwsR63ea7yvxJc0PwN/zobNAudYfb1B7R08SzB4TsLeSbUCuG467NhnoT0oO6w1qRO+BA==} + engines: {node: '>=18'} + dependencies: + eventemitter3: 5.0.1 + p-timeout: 6.1.2 + dev: false + /p-retry@4.6.2: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} @@ -18843,6 +18845,11 @@ packages: '@types/retry': 0.12.0 retry: 0.13.1 + /p-timeout@6.1.2: + resolution: {integrity: sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==} + engines: {node: '>=14.16'} + dev: false + /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -21675,6 +21682,9 @@ packages: /sqlite3@5.1.6: resolution: {integrity: sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==} requiresBuild: true + peerDependenciesMeta: + node-gyp: + optional: true dependencies: '@mapbox/node-pre-gyp': 1.0.11 node-addon-api: 4.3.0 diff --git a/src/app/common/hooks/account/use-account-names.ts b/src/app/common/hooks/account/use-account-names.ts index dbfe0849c73..0074a54bce9 100644 --- a/src/app/common/hooks/account/use-account-names.ts +++ b/src/app/common/hooks/account/use-account-names.ts @@ -24,9 +24,12 @@ export function useCurrentAccountDisplayName() { } export function useAccountDisplayName({ address, index }: { index: number; address: string }) { - const { data: names = [] } = useGetAccountNamesByAddressQuery(address); + const { data: names = [], isLoading } = useGetAccountNamesByAddressQuery(address); return useMemo(() => { - if (names[0]) return parseIfValidPunycode(names[0]); - return getAutogeneratedAccountDisplayName(index); - }, [names, index]); + const name = names[0] || getAutogeneratedAccountDisplayName(index); + return { + name, + isLoading, + }; + }, [names, index, isLoading]); } diff --git a/src/app/components/account-total-balance.tsx b/src/app/components/account-total-balance.tsx index efb430ded57..0606100efd9 100644 --- a/src/app/components/account-total-balance.tsx +++ b/src/app/components/account-total-balance.tsx @@ -4,18 +4,25 @@ import { styled } from 'leather-styles/jsx'; import { useTotalBalance } from '@app/common/hooks/balance/use-total-balance'; +import { shimmerStyles } from '../../../theme/global/shimmer-styles'; + interface AccountTotalBalanceProps { btcAddress: string; stxAddress: string; } export const AccountTotalBalance = memo(({ btcAddress, stxAddress }: AccountTotalBalanceProps) => { - const totalBalance = useTotalBalance({ btcAddress, stxAddress }); + const { totalUsdBalance, isLoading } = useTotalBalance({ btcAddress, stxAddress }); - if (!totalBalance) return null; + if (!totalUsdBalance) return null; return ( - - {totalBalance.totalUsdBalance} + + {totalUsdBalance} ); }); diff --git a/src/app/components/account/account-name.tsx b/src/app/components/account/account-name.tsx index 7101f5d76d5..9a267497878 100644 --- a/src/app/components/account/account-name.tsx +++ b/src/app/components/account/account-name.tsx @@ -2,11 +2,21 @@ import { memo } from 'react'; import { styled } from 'leather-styles/jsx'; +import { shimmerStyles } from '../../../../theme/global/shimmer-styles'; + interface AccountNameLayoutProps { children: React.ReactNode; + isLoading?: boolean; } -export const AccountNameLayout = memo(({ children }: AccountNameLayoutProps) => ( - + +export const AccountNameLayout = memo(({ children, isLoading }: AccountNameLayoutProps) => ( + {children} )); diff --git a/src/app/features/collectibles/components/_collectible-types/collectible-image.tsx b/src/app/features/collectibles/components/_collectible-types/collectible-image.tsx index 4033ff021a3..529f7530f8d 100644 --- a/src/app/features/collectibles/components/_collectible-types/collectible-image.tsx +++ b/src/app/features/collectibles/components/_collectible-types/collectible-image.tsx @@ -26,6 +26,7 @@ export function CollectibleImage(props: CollectibleImageProps) { {alt} setIsError(true)} + loading="lazy" onLoad={event => { const target = event.target as HTMLImageElement; setWidth(target.naturalWidth); @@ -37,7 +38,8 @@ export function CollectibleImage(props: CollectibleImageProps) { height: '100%', aspectRatio: '1 / 1', objectFit: 'cover', - display: isLoading ? 'none' : 'inherit', + // display: 'none' breaks onLoad event firing + opacity: isLoading ? '0' : '1', imageRendering: width <= 40 ? 'pixelated' : 'auto', }} /> diff --git a/src/app/features/switch-account-drawer/components/switch-account-list-item.tsx b/src/app/features/switch-account-drawer/components/switch-account-list-item.tsx index 384fb50567c..432b4a4e798 100644 --- a/src/app/features/switch-account-drawer/components/switch-account-list-item.tsx +++ b/src/app/features/switch-account-drawer/components/switch-account-list-item.tsx @@ -28,7 +28,10 @@ export const SwitchAccountListItem = memo( 'SWITCH_ACCOUNTS' + stxAddress || btcAddress ); const { handleSwitchAccount } = useSwitchAccount(handleClose); - const name = useAccountDisplayName({ address: stxAddress, index }); + const { name, isLoading: isLoadingBnsName } = useAccountDisplayName({ + address: stxAddress, + index, + }); const handleClick = async () => { setIsLoading(); @@ -41,7 +44,7 @@ export const SwitchAccountListItem = memo( return ( } - accountName={{name}} + accountName={{name}} avatar={ { - const name = useAccountDisplayName(account); + const { name } = useAccountDisplayName(account); const btcAddress = useNativeSegwitAccountIndexAddressIndexZero(account.index); const accountSlug = useMemo(() => slugify(`Account ${account?.index + 1}`), [account?.index]); diff --git a/src/app/pages/select-network/components/network-list-item.layout.tsx b/src/app/pages/select-network/components/network-list-item.layout.tsx index 3faf4f4c132..a862a4168a2 100644 --- a/src/app/pages/select-network/components/network-list-item.layout.tsx +++ b/src/app/pages/select-network/components/network-list-item.layout.tsx @@ -1,5 +1,5 @@ import { SettingsSelectors } from '@tests/selectors/settings.selectors'; -import { Box, Flex, Stack, styled } from 'leather-styles/jsx'; +import { Flex, Stack, styled } from 'leather-styles/jsx'; import { NetworkConfiguration } from '@shared/constants'; @@ -30,22 +30,17 @@ export function NetworkListItemLayout({ const unSelectable = !isOnline || isActive; return ( - @@ -70,7 +65,7 @@ export function NetworkListItemLayout({ )} - + ); } diff --git a/src/app/pages/send/send-crypto-asset-form/components/recipient-accounts-drawer/account-list-item.tsx b/src/app/pages/send/send-crypto-asset-form/components/recipient-accounts-drawer/account-list-item.tsx index d59114ed9d0..03b38962918 100644 --- a/src/app/pages/send/send-crypto-asset-form/components/recipient-accounts-drawer/account-list-item.tsx +++ b/src/app/pages/send/send-crypto-asset-form/components/recipient-accounts-drawer/account-list-item.tsx @@ -23,7 +23,7 @@ export const AccountListItem = memo(({ index, stacksAccount, onClose }: AccountL BitcoinSendFormValues | StacksSendFormValues >(); const stacksAddress = stacksAccount?.address || ''; - const name = useAccountDisplayName({ address: stacksAddress, index }); + const { name } = useAccountDisplayName({ address: stacksAddress, index }); const bitcoinSigner = useNativeSegwitSigner(index); const bitcoinAddress = bitcoinSigner?.(0).address || ''; diff --git a/src/app/query/bitcoin/address/transactions-by-address.query.ts b/src/app/query/bitcoin/address/transactions-by-address.query.ts index 7a44f6fc55e..794a6b80687 100644 --- a/src/app/query/bitcoin/address/transactions-by-address.query.ts +++ b/src/app/query/bitcoin/address/transactions-by-address.query.ts @@ -1,4 +1,4 @@ -import { useQueries, useQuery } from '@tanstack/react-query'; +import { type QueryFunctionContext, useQueries, useQuery } from '@tanstack/react-query'; import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model'; @@ -18,7 +18,9 @@ export function useGetBitcoinTransactionsByAddressQuery client.addressApi.getTransactionsByAddress(address), + queryFn: async ({ signal }) => { + return client.addressApi.getTransactionsByAddress(address, signal); + }, ...queryOptions, ...options, }); @@ -35,7 +37,9 @@ export function useGetBitcoinTransactionsByAddressesQuery client.addressApi.getTransactionsByAddress(address), + queryFn: async ({ signal }: QueryFunctionContext) => { + return client.addressApi.getTransactionsByAddress(address, signal); + }, ...queryOptions, ...options, }; diff --git a/src/app/query/bitcoin/address/utxos-by-address.query.ts b/src/app/query/bitcoin/address/utxos-by-address.query.ts index efdda83d2ed..a47ce189354 100644 --- a/src/app/query/bitcoin/address/utxos-by-address.query.ts +++ b/src/app/query/bitcoin/address/utxos-by-address.query.ts @@ -23,10 +23,13 @@ export function useGetUtxosByAddressQuery ) { const client = useBitcoinClient(); + return useQuery({ enabled: !!address, queryKey: ['btc-utxos-by-address', address], - queryFn: () => client.addressApi.getUtxosByAddress(address), + queryFn: async ({ signal }) => { + return client.addressApi.getUtxosByAddress(address, signal); + }, ...queryOptions, ...options, }); diff --git a/src/app/query/bitcoin/bitcoin-client.ts b/src/app/query/bitcoin/bitcoin-client.ts index 13c2ac2e015..5ed7a546830 100644 --- a/src/app/query/bitcoin/bitcoin-client.ts +++ b/src/app/query/bitcoin/bitcoin-client.ts @@ -1,7 +1,11 @@ import axios from 'axios'; +import PQueue from 'p-queue'; import { HIRO_API_BASE_URL_MAINNET } from '@shared/constants'; import { Paginated } from '@shared/models/api-types'; +import type { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model'; + +import { getBlockstreamRatelimiter } from './blockstream-rate-limiter'; class Configuration { constructor(public baseUrl: string) {} @@ -162,16 +166,23 @@ class HiroApi { } class AddressApi { - constructor(public configuration: Configuration) {} + rateLimiter: PQueue; + constructor(public configuration: Configuration) { + this.rateLimiter = getBlockstreamRatelimiter(this.configuration.baseUrl); + } - async getTransactionsByAddress(address: string) { - const resp = await axios.get(`${this.configuration.baseUrl}/address/${address}/txs`); + async getTransactionsByAddress(address: string, signal?: AbortSignal) { + const resp = await this.rateLimiter.add( + () => axios.get(`${this.configuration.baseUrl}/address/${address}/txs`), + { signal, throwOnTimeout: true } + ); return resp.data; } - async getUtxosByAddress(address: string): Promise { - const resp = await axios.get( - `${this.configuration.baseUrl}/address/${address}/utxo` + async getUtxosByAddress(address: string, signal?: AbortSignal): Promise { + const resp = await this.rateLimiter.add( + () => axios.get(`${this.configuration.baseUrl}/address/${address}/utxo`), + { signal, priority: 1, throwOnTimeout: true } ); return resp.data.sort((a, b) => a.vout - b.vout); } diff --git a/src/app/query/bitcoin/blockstream-rate-limiter.ts b/src/app/query/bitcoin/blockstream-rate-limiter.ts new file mode 100644 index 00000000000..121d782f712 --- /dev/null +++ b/src/app/query/bitcoin/blockstream-rate-limiter.ts @@ -0,0 +1,18 @@ +import PQueue from 'p-queue'; + +import { BITCOIN_API_BASE_URL_TESTNET } from '@shared/constants'; + +const blockstreamMainnetApiLimiter = new PQueue({ + interval: 5000, + intervalCap: 20, +}); + +const blockstreamTestnetApiLimiter = new PQueue({ + interval: 5000, + intervalCap: 30, +}); + +export function getBlockstreamRatelimiter(url: string) { + if (url.includes(BITCOIN_API_BASE_URL_TESTNET)) return blockstreamTestnetApiLimiter; + return blockstreamMainnetApiLimiter; +} diff --git a/src/app/query/bitcoin/ordinals/inscription-text-content.query.ts b/src/app/query/bitcoin/ordinals/inscription-text-content.query.ts index a233d28aa16..893dae93451 100644 --- a/src/app/query/bitcoin/ordinals/inscription-text-content.query.ts +++ b/src/app/query/bitcoin/ordinals/inscription-text-content.query.ts @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; import { QueryPrefixes } from '@app/query/query-prefixes'; +import { useHiroApiRateLimiter } from '@app/query/stacks/hiro-rate-limiter'; async function getInscriptionTextContent(src: string) { const res = await axios.get(src, { responseType: 'text' }); @@ -9,9 +10,14 @@ async function getInscriptionTextContent(src: string) { } export function useInscriptionTextContentQuery(contentSrc: string) { + const limiter = useHiroApiRateLimiter(); return useQuery( [QueryPrefixes.OrdinalTextContent, contentSrc], - () => getInscriptionTextContent(contentSrc), + async () => { + return limiter.add(() => getInscriptionTextContent(contentSrc), { + throwOnTimeout: true, + }); + }, { cacheTime: Infinity, staleTime: Infinity, diff --git a/src/app/query/bitcoin/ordinals/inscriptions.query.ts b/src/app/query/bitcoin/ordinals/inscriptions.query.ts index a3a4495c986..9e59097da48 100644 --- a/src/app/query/bitcoin/ordinals/inscriptions.query.ts +++ b/src/app/query/bitcoin/ordinals/inscriptions.query.ts @@ -10,6 +10,7 @@ import { ensureArray } from '@shared/utils'; import { createNumArrayOfRange } from '@app/common/utils'; import { QueryPrefixes } from '@app/query/query-prefixes'; +import { useHiroApiRateLimiter } from '@app/query/stacks/hiro-rate-limiter'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentTaprootAccount } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; @@ -27,6 +28,7 @@ interface InfiniteQueryPageParam { addressesWithoutOrdinalsNum: number; addressesMap: Record; }; + signal?: AbortSignal; } interface InscriptionsQueryResponse { @@ -36,14 +38,31 @@ interface InscriptionsQueryResponse { total: number; } -async function fetchInscriptions(addresses: string | string[], offset = 0, limit = 60) { +interface FetchInscriptionsArgs { + addresses: string | string[]; + offset?: number; + limit?: number; + signal?: AbortSignal; +} + +async function fetchInscriptions({ + addresses, + offset = 0, + limit = 60, + signal, +}: FetchInscriptionsArgs) { const params = new URLSearchParams(); ensureArray(addresses).forEach(address => params.append('address', address)); params.append('limit', limit.toString()); params.append('offset', offset.toString()); + const res = await axios.get( - `${HIRO_INSCRIPTIONS_API_URL}?${params.toString()}` + `${HIRO_INSCRIPTIONS_API_URL}?${params.toString()}`, + { + signal, + } ); + return res.data; } @@ -55,6 +74,7 @@ export function useGetInscriptionsInfiniteQuery() { const account = useCurrentTaprootAccount(); const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner(); const currentBitcoinAddress = nativeSegwitSigner.address; + const limiter = useHiroApiRateLimiter(); const getTaprootAddressData = useCallback( (fromIndex: number, toIndex: number) => { @@ -76,7 +96,7 @@ export function useGetInscriptionsInfiniteQuery() { const query = useInfiniteQuery({ queryKey: [QueryPrefixes.GetInscriptions, currentBitcoinAddress, network.id], - async queryFn({ pageParam }: InfiniteQueryPageParam) { + async queryFn({ pageParam, signal }: InfiniteQueryPageParam) { const responsesArr: InscriptionsQueryResponse[] = []; let fromIndex = pageParam?.fromIndex ?? 0; let addressesWithoutOrdinalsNum = pageParam?.addressesWithoutOrdinalsNum ?? 0; @@ -98,7 +118,18 @@ export function useGetInscriptionsInfiniteQuery() { if (fromIndex === 0) { addresses.unshift(currentBitcoinAddress); } - const response = await fetchInscriptions(addresses, offset, inscriptionsLazyLoadLimit); + const response = await limiter.add( + () => + fetchInscriptions({ + addresses, + offset, + limit: inscriptionsLazyLoadLimit, + }), + { + signal, + throwOnTimeout: true, + } + ); responsesArr.push(response); @@ -186,11 +217,20 @@ export function useGetInscriptionsInfiniteQuery() { export function useInscriptionsByAddressQuery(address: string) { const network = useCurrentNetwork(); + const limiter = useHiroApiRateLimiter(); const query = useInfiniteQuery({ - queryKey: [QueryPrefixes.InscriptionsByAddress, address, network.id], - async queryFn({ pageParam = 0 }) { - return fetchInscriptions(address, pageParam); + queryKey: [QueryPrefixes.InscriptionsByAddress, network.id, address], + async queryFn({ pageParam = 0, signal }) { + return limiter.add( + () => + fetchInscriptions({ + addresses: address, + offset: pageParam, + signal, + }), + { priority: 1, signal, throwOnTimeout: true } + ); }, getNextPageParam(prevInscriptionsQuery) { if (prevInscriptionsQuery.offset >= prevInscriptionsQuery.total) return undefined; diff --git a/src/app/query/stacks/balance/stx-balance.query.ts b/src/app/query/stacks/balance/stx-balance.query.ts index 5cb0b95910b..300a2222a52 100644 --- a/src/app/query/stacks/balance/stx-balance.query.ts +++ b/src/app/query/stacks/balance/stx-balance.query.ts @@ -7,7 +7,7 @@ import { StacksClient } from '@app/query/stacks/stacks-client'; import { useStacksClient } from '@app/store/common/api-clients.hooks'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; -import { RateLimiter, useHiroApiRateLimiter } from '../rate-limiter'; +import { useHiroApiRateLimiter } from '../hiro-rate-limiter'; const staleTime = 1 * 60 * 1000; @@ -17,11 +17,17 @@ const balanceQueryOptions = { refetchOnMount: true, } as const; -function fetchAccountBalance(client: StacksClient, limiter: RateLimiter) { +function fetchAccountBalance(client: StacksClient, signal?: AbortSignal) { return async (principal: string) => { - await limiter.removeTokens(1); // Coercing type with client-side one that's more accurate - return client.accountsApi.getAccountBalance({ principal }) as Promise; + return client.accountsApi.getAccountBalance( + { + principal, + }, + { + signal, + } + ) as Promise; }; } @@ -38,7 +44,12 @@ export function useStacksAccountBalanceQuery fetchAccountBalance(client, limiter)(address), + queryFn: async ({ signal }) => { + return limiter.add(() => fetchAccountBalance(client, signal)(address), { + signal, + throwOnTimeout: true, + }); + }, ...balanceQueryOptions, ...options, }); diff --git a/src/app/query/stacks/bns/bns.query.ts b/src/app/query/stacks/bns/bns.query.ts index 29603e09bfd..ba91acba931 100644 --- a/src/app/query/stacks/bns/bns.query.ts +++ b/src/app/query/stacks/bns/bns.query.ts @@ -7,14 +7,14 @@ import { StacksClient } from '@app/query/stacks/stacks-client'; import { useStacksClient } from '@app/store/common/api-clients.hooks'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; -import { RateLimiter, useHiroApiRateLimiter } from '../rate-limiter'; +import { useHiroApiRateLimiter } from '../hiro-rate-limiter'; import { fetchNamesForAddress } from './bns.utils'; -const staleTime = 15 * 60 * 1000; // 15 min +const staleTime = 24 * 60 * 60 * 1000; // 24 hours const bnsQueryOptions = { keepPreviousData: true, - cacheTime: staleTime, + cacheTime: Infinity, staleTime: staleTime, refetchOnMount: false, refetchInterval: false, @@ -25,17 +25,17 @@ type BnsNameFetcher = (address: string) => Promise interface GetBnsNameFetcherFactoryArgs { client: StacksClient; - limiter: RateLimiter; isTestnet: boolean; + signal?: AbortSignal; } + function getBnsNameFetcherFactory({ client, - limiter, isTestnet, + signal, }: GetBnsNameFetcherFactoryArgs): BnsNameFetcher { return async (address: string) => { - await limiter.removeTokens(1); - return fetchNamesForAddress({ client, address, isTestnet }); + return fetchNamesForAddress({ client, address, isTestnet, signal }); }; } @@ -51,7 +51,13 @@ export function useGetBnsNamesOwnedByAddress getBnsNameFetcherFactory({ client, limiter, isTestnet })(address), + queryFn: async ({ signal }) => { + return limiter.add(() => fetchNamesForAddress({ client, address, isTestnet, signal }), { + signal, + priority: 2, + throwOnTimeout: true, + }); + }, ...bnsQueryOptions, ...options, }); diff --git a/src/app/query/stacks/bns/bns.utils.ts b/src/app/query/stacks/bns/bns.utils.ts index 1e8a777885e..046818ba1a4 100644 --- a/src/app/query/stacks/bns/bns.utils.ts +++ b/src/app/query/stacks/bns/bns.utils.ts @@ -26,19 +26,26 @@ const bnsContractConsts = { } as const; // Fetch an address's "primary name" from the BNSx contract. -async function fetchBnsxName(client: StacksClient, address: string): Promise { +async function fetchBnsxName( + client: StacksClient, + address: string, + signal?: AbortSignal +): Promise { try { const addressCV = standardPrincipalCV(address); const addressHex = cvToHex(addressCV); - const res = await client.smartContractsApi.callReadOnlyFunction({ - ...bnsContractConsts, - functionName: 'get-primary-name', - tip: 'latest', - readOnlyFunctionArgs: { - sender: address, - arguments: [addressHex], + const res = await client.smartContractsApi.callReadOnlyFunction( + { + ...bnsContractConsts, + functionName: 'get-primary-name', + tip: 'latest', + readOnlyFunctionArgs: { + sender: address, + arguments: [addressHex], + }, }, - }); + { signal } + ); if (!res.okay || !res.result) return null; const { result } = res; const cv = deserializeCV(result) as OptionalCV< @@ -97,21 +104,26 @@ interface FetchNamesForAddressArgs { client: StacksClient; address: string; isTestnet: boolean; + signal?: AbortSignal; } export async function fetchNamesForAddress({ client, address, isTestnet, + signal, }: FetchNamesForAddressArgs): Promise { const fetchFromApi = async () => { - return client.namesApi.getNamesOwnedByAddress({ address, blockchain: 'stacks' }); + return client.namesApi.getNamesOwnedByAddress({ address, blockchain: 'stacks' }, { signal }); }; if (isTestnet) { return fetchFromApi(); } // Return BNSx name if available, otherwise return names from API. - const [bnsxName, bnsNames] = await Promise.all([fetchBnsxName(client, address), fetchFromApi()]); + const [bnsxName, bnsNames] = await Promise.all([ + fetchBnsxName(client, address, signal), + fetchFromApi(), + ]); const bnsName = 'names' in bnsNames ? bnsNames.names[0] : null; const names: string[] = []; if (bnsName) names.push(bnsName); diff --git a/src/app/query/stacks/contract/contract.query.ts b/src/app/query/stacks/contract/contract.query.ts index 7bcbd70d1a9..fc20b8a8a7d 100644 --- a/src/app/query/stacks/contract/contract.query.ts +++ b/src/app/query/stacks/contract/contract.query.ts @@ -5,7 +5,7 @@ import { ContractInterfaceResponseWithFunctions } from '@shared/models/contract- import { useStacksClient } from '@app/store/common/api-clients.hooks'; -import { useHiroApiRateLimiter } from '../rate-limiter'; +import { useHiroApiRateLimiter } from '../hiro-rate-limiter'; export function useGetContractInterface(transactionRequest: ContractCallPayload | null) { const { smartContractsApi } = useStacksClient(); @@ -15,11 +15,16 @@ export function useGetContractInterface(transactionRequest: ContractCallPayload if (!transactionRequest || transactionRequest?.txType !== TransactionTypes.ContractCall) return; const contractAddress = transactionRequest.contractAddress; const contractName = transactionRequest.contractName; - await limiter.removeTokens(1); - return smartContractsApi.getContractInterface({ - contractAddress, - contractName, - }) as unknown as Promise; + return limiter.add( + () => + smartContractsApi.getContractInterface({ + contractAddress, + contractName, + }) as unknown as Promise, + { + throwOnTimeout: true, + } + ); } return useQuery({ diff --git a/src/app/query/stacks/fees/fees.query.ts b/src/app/query/stacks/fees/fees.query.ts index 114a9079bb6..6ede33dfddc 100644 --- a/src/app/query/stacks/fees/fees.query.ts +++ b/src/app/query/stacks/fees/fees.query.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; +import PQueue from 'p-queue'; import { logger } from '@shared/logger'; import { StacksTxFeeEstimation } from '@shared/models/fees/stacks-fees.model'; @@ -7,17 +8,22 @@ import { StacksTxFeeEstimation } from '@shared/models/fees/stacks-fees.model'; import { AppUseQueryConfig } from '@app/query/query-config'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; -import { RateLimiter, useHiroApiRateLimiter } from '../rate-limiter'; +import { useHiroApiRateLimiter } from '../hiro-rate-limiter'; import { defaultApiFeeEstimations } from './fees.utils'; -function fetchTransactionFeeEstimation(currentNetwork: any, limiter: RateLimiter) { +function fetchTransactionFeeEstimation(currentNetwork: any, limiter: PQueue) { return async (estimatedLen: number | null, transactionPayload: string) => { - await limiter.removeTokens(1); - const resp = await axios.post( - currentNetwork.chain.stacks.url + '/v2/fees/transaction', + const resp = await limiter.add( + () => + axios.post( + currentNetwork.chain.stacks.url + '/v2/fees/transaction', + { + estimated_len: estimatedLen, + transaction_payload: transactionPayload, + } + ), { - estimated_len: estimatedLen, - transaction_payload: transactionPayload, + throwOnTimeout: true, } ); return resp.data; diff --git a/src/app/query/stacks/rate-limiter.ts b/src/app/query/stacks/hiro-rate-limiter.ts similarity index 62% rename from src/app/query/stacks/rate-limiter.ts rename to src/app/query/stacks/hiro-rate-limiter.ts index c7a56234454..57cbfc94c90 100644 --- a/src/app/query/stacks/rate-limiter.ts +++ b/src/app/query/stacks/hiro-rate-limiter.ts @@ -1,17 +1,20 @@ import { ChainID } from '@stacks/transactions'; -import { RateLimiter } from 'limiter'; +import PQueue from 'p-queue'; import { whenStacksChainId } from '@app/common/utils'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; -const hiroStacksMainnetApiLimiter = new RateLimiter({ - tokensPerInterval: 500, - interval: 'minute', +const hiroStacksMainnetApiLimiter = new PQueue({ + interval: 5000, + intervalCap: 10, + timeout: 60000, }); -const hiroStacksTestnetApiLimiter = new RateLimiter({ - tokensPerInterval: 100, - interval: 'minute', +const hiroStacksTestnetApiLimiter = new PQueue({ + concurrency: 20, + interval: 60000, + intervalCap: 20, + timeout: 60000, }); export function useHiroApiRateLimiter() { @@ -22,5 +25,3 @@ export function useHiroApiRateLimiter() { [ChainID.Testnet]: hiroStacksTestnetApiLimiter, }); } - -export type { RateLimiter }; diff --git a/src/app/query/stacks/mempool/mempool.query.ts b/src/app/query/stacks/mempool/mempool.query.ts index bed57dfb823..64f782d1b70 100644 --- a/src/app/query/stacks/mempool/mempool.query.ts +++ b/src/app/query/stacks/mempool/mempool.query.ts @@ -6,7 +6,7 @@ import { useStacksClient } from '@app/store/common/api-clients.hooks'; import { useSubmittedTransactionsActions } from '@app/store/submitted-transactions/submitted-transactions.hooks'; import { useSubmittedTransactions } from '@app/store/submitted-transactions/submitted-transactions.selectors'; -import { useHiroApiRateLimiter } from '../rate-limiter'; +import { useHiroApiRateLimiter } from '../hiro-rate-limiter'; export function useAccountMempoolQuery(address: string) { const client = useStacksClient(); @@ -15,8 +15,12 @@ export function useAccountMempoolQuery(address: string) { const limiter = useHiroApiRateLimiter(); async function accountMempoolFetcher() { - await limiter.removeTokens(1); - return client.transactionsApi.getAddressMempoolTransactions({ address, limit: 50 }); + return limiter.add( + () => client.transactionsApi.getAddressMempoolTransactions({ address, limit: 50 }), + { + throwOnTimeout: true, + } + ); } return useQuery({ diff --git a/src/app/query/stacks/network/network.query.ts b/src/app/query/stacks/network/network.query.ts index 405b54edef4..0deaaa73b47 100644 --- a/src/app/query/stacks/network/network.query.ts +++ b/src/app/query/stacks/network/network.query.ts @@ -1,7 +1,8 @@ import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; +import PQueue from 'p-queue'; -import { RateLimiter, useHiroApiRateLimiter } from '../rate-limiter'; +import { useHiroApiRateLimiter } from '../hiro-rate-limiter'; const staleTime = 15 * 60 * 1000; // 15 min @@ -13,9 +14,12 @@ const networkStatusQueryOptions = { refetchOnReconnect: false, } as const; -async function getNetworkStatusFetcher(url: string, limiter: RateLimiter) { - await limiter.removeTokens(1); - const resp = await axios.get(url, { timeout: 4500 }); +async function getNetworkStatusFetcher(url: string, limiter: PQueue) { + const resp = await limiter.add(() => axios.get(url, { timeout: 30000 }), { + throwOnTimeout: true, + priority: 1, + }); + return resp.data; } diff --git a/src/app/query/stacks/nonce/account-nonces.query.ts b/src/app/query/stacks/nonce/account-nonces.query.ts index e3059a52b39..e4ca85d3fd7 100644 --- a/src/app/query/stacks/nonce/account-nonces.query.ts +++ b/src/app/query/stacks/nonce/account-nonces.query.ts @@ -1,11 +1,12 @@ import { useQuery } from '@tanstack/react-query'; +import PQueue from 'p-queue'; import { AppUseQueryConfig } from '@app/query/query-config'; import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useStacksClient } from '@app/store/common/api-clients.hooks'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; -import { RateLimiter, useHiroApiRateLimiter } from '../rate-limiter'; +import { useHiroApiRateLimiter } from '../hiro-rate-limiter'; import { StacksClient } from '../stacks-client'; const accountNoncesQueryOptions = { @@ -14,13 +15,19 @@ const accountNoncesQueryOptions = { refetchOnWindowFocus: 'always', } as const; -function fetchAccountNonces(client: StacksClient, limiter: RateLimiter) { +function fetchAccountNonces(client: StacksClient, limiter: PQueue) { return async (principal: string) => { if (!principal) return; - await limiter.removeTokens(1); - return client.accountsApi.getAccountNonces({ - principal, - }); + + return limiter.add( + () => + client.accountsApi.getAccountNonces({ + principal, + }), + { + throwOnTimeout: true, + } + ); }; } diff --git a/src/app/query/stacks/tokens/fungible-tokens/fungible-token-metadata.query.ts b/src/app/query/stacks/tokens/fungible-tokens/fungible-token-metadata.query.ts index 2735ba0801b..7e53a2db505 100644 --- a/src/app/query/stacks/tokens/fungible-tokens/fungible-token-metadata.query.ts +++ b/src/app/query/stacks/tokens/fungible-tokens/fungible-token-metadata.query.ts @@ -1,9 +1,10 @@ import { UseQueryResult, useQueries, useQuery } from '@tanstack/react-query'; +import PQueue from 'p-queue'; import { useTokenMetadataClient } from '@app/store/common/api-clients.hooks'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; -import { RateLimiter, useHiroApiRateLimiter } from '../../rate-limiter'; +import { useHiroApiRateLimiter } from '../../hiro-rate-limiter'; import { TokenMetadataClient } from '../../token-metadata-client'; import { FtAssetResponse } from '../token-metadata.utils'; @@ -20,10 +21,11 @@ const queryOptions = { retry: 0, } as const; -function fetchFungibleTokenMetadata(client: TokenMetadataClient, limiter: RateLimiter) { +function fetchFungibleTokenMetadata(client: TokenMetadataClient, limiter: PQueue) { return (principal: string) => async () => { - await limiter.removeTokens(1); - return client.tokensApi.getFtMetadata(principal); + return limiter.add(() => client.tokensApi.getFtMetadata(principal), { + throwOnTimeout: true, + }); }; } diff --git a/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-holdings.query.ts b/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-holdings.query.ts index c839c4c117d..edb127a54bf 100644 --- a/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-holdings.query.ts +++ b/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-holdings.query.ts @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import PQueue from 'p-queue'; import { logger } from '@shared/logger'; import { Paginated } from '@shared/models/api-types'; @@ -8,7 +9,7 @@ import { StacksClient } from '@app/query/stacks/stacks-client'; import { useStacksClient } from '@app/store/common/api-clients.hooks'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; -import { RateLimiter, useHiroApiRateLimiter } from '../../rate-limiter'; +import { useHiroApiRateLimiter } from '../../hiro-rate-limiter'; const staleTime = 15 * 60 * 1000; // 15 min @@ -26,14 +27,19 @@ const queryOptions = { cacheTime: staleTime, staleTime, refetchhOnFocus: false } type FetchNonFungibleTokenHoldingsResp = Paginated; -function fetchNonFungibleTokenHoldings(client: StacksClient, limiter: RateLimiter) { +function fetchNonFungibleTokenHoldings(client: StacksClient, limiter: PQueue) { return async (address: string) => { if (!address) return; - await limiter.removeTokens(1); - return client.nonFungibleTokensApi.getNftHoldings({ - principal: address, - limit: 50, - }) as unknown as Promise; + return limiter.add( + () => + client.nonFungibleTokensApi.getNftHoldings({ + principal: address, + limit: 50, + }) as unknown as Promise, + { + throwOnTimeout: true, + } + ); }; } @@ -41,7 +47,7 @@ function makeNonFungibleTokenHoldingsQuery( address: string, network: string, client: StacksClient, - limiter: RateLimiter + limiter: PQueue ) { if (address === '') logger.warn('No address passed to ' + QueryPrefixes.GetNftHoldings); return { diff --git a/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-metadata.query.ts b/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-metadata.query.ts index b93f7531d98..02311940d5f 100644 --- a/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-metadata.query.ts +++ b/src/app/query/stacks/tokens/non-fungible-tokens/non-fungible-token-metadata.query.ts @@ -6,8 +6,7 @@ import { QueryPrefixes } from '@app/query/query-prefixes'; import { StacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.models'; import { useTokenMetadataClient } from '@app/store/common/api-clients.hooks'; -import { RateLimiter, useHiroApiRateLimiter } from '../../rate-limiter'; -import { TokenMetadataClient } from '../../token-metadata-client'; +import { useHiroApiRateLimiter } from '../../hiro-rate-limiter'; import { NftAssetResponse } from '../token-metadata.utils'; import { useGetNonFungibleTokenHoldingsQuery } from './non-fungible-token-holdings.query'; @@ -22,13 +21,6 @@ function getTokenId(hex: string) { return clarityValue.type === 1 ? Number(clarityValue.value) : 0; } -function fetchNonFungibleTokenMetadata(client: TokenMetadataClient, limiter: RateLimiter) { - return (principal: string, tokenId: number) => async () => { - await limiter.removeTokens(1); - return client.tokensApi.getNftMetadata(principal, tokenId); - }; -} - export function useGetNonFungibleTokenMetadataListQuery( account: StacksAccount ): UseQueryResult[] { @@ -44,7 +36,11 @@ export function useGetNonFungibleTokenMetadataListQuery( return { enabled: !!tokenId, queryKey: [QueryPrefixes.GetNftMetadata, principal, tokenId], - queryFn: fetchNonFungibleTokenMetadata(client, limiter)(principal, tokenId), + queryFn: async () => { + return limiter.add(() => client.tokensApi.getNftMetadata(principal, tokenId), { + throwOnTimeout: true, + }); + }, ...queryOptions, }; }), diff --git a/src/app/query/stacks/transactions/transactions-by-id.query.ts b/src/app/query/stacks/transactions/transactions-by-id.query.ts index 2be7d5620fd..705b7b60fbf 100644 --- a/src/app/query/stacks/transactions/transactions-by-id.query.ts +++ b/src/app/query/stacks/transactions/transactions-by-id.query.ts @@ -3,7 +3,7 @@ import { useQueries, useQuery } from '@tanstack/react-query'; import { useStacksClient } from '@app/store/common/api-clients.hooks'; -import { useHiroApiRateLimiter } from '../rate-limiter'; +import { useHiroApiRateLimiter } from '../hiro-rate-limiter'; const options = { staleTime: 30 * 1000, @@ -17,8 +17,12 @@ export function useTransactionsById(txids: string[]) { const limiter = useHiroApiRateLimiter(); async function transactionByIdFetcher(txId: string) { - await limiter.removeTokens(1); - return client.transactionsApi.getTransactionById({ txId }) as unknown as MempoolTransaction; + return limiter.add( + () => client.transactionsApi.getTransactionById({ txId }) as unknown as MempoolTransaction, + { + throwOnTimeout: true, + } + ); } return useQueries({ @@ -36,10 +40,15 @@ export function useTransactionById(txid: string) { const client = useStacksClient(); const limiter = useHiroApiRateLimiter(); async function transactionByIdFetcher(txId: string) { - await limiter.removeTokens(1); - return client.transactionsApi.getTransactionById({ txId }) as unknown as - | Transaction - | MempoolTransaction; + return limiter.add( + () => + client.transactionsApi.getTransactionById({ txId }) as unknown as + | Transaction + | MempoolTransaction, + { + throwOnTimeout: true, + } + ); } return useQuery({ diff --git a/src/app/query/stacks/transactions/transactions-with-transfers.query.ts b/src/app/query/stacks/transactions/transactions-with-transfers.query.ts index 3b5e7c24a03..73659faee96 100644 --- a/src/app/query/stacks/transactions/transactions-with-transfers.query.ts +++ b/src/app/query/stacks/transactions/transactions-with-transfers.query.ts @@ -7,7 +7,7 @@ import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain import { useStacksClient } from '@app/store/common/api-clients.hooks'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; -import { useHiroApiRateLimiter } from '../rate-limiter'; +import { useHiroApiRateLimiter } from '../hiro-rate-limiter'; const queryOptions: UseQueryOptions = { staleTime: 60 * 1000, @@ -23,18 +23,24 @@ export function useGetAccountTransactionsWithTransfersQuery() { const client = useStacksClient(); const limiter = useHiroApiRateLimiter(); - async function fetchAccountTxsWithTransfers() { + async function fetchAccountTxsWithTransfers(signal?: AbortSignal) { if (!principal) return; - await limiter.removeTokens(1); - return client.accountsApi.getAccountTransactionsWithTransfers({ - principal, - limit: DEFAULT_LIST_LIMIT, - }); + return limiter.add( + () => + client.accountsApi.getAccountTransactionsWithTransfers({ + principal, + limit: DEFAULT_LIST_LIMIT, + }), + { + signal, + throwOnTimeout: true, + } + ); } return useQuery({ queryKey: ['account-txs-with-transfers', principal, chain.stacks.url], - queryFn: fetchAccountTxsWithTransfers, + queryFn: ({ signal }) => fetchAccountTxsWithTransfers(signal), enabled: !!principal && !!chain.stacks.url, ...queryOptions, }) as UseQueryResult; diff --git a/tests/page-object-models/home.page.ts b/tests/page-object-models/home.page.ts index e54b5b11b18..afd587bb7bd 100644 --- a/tests/page-object-models/home.page.ts +++ b/tests/page-object-models/home.page.ts @@ -99,10 +99,7 @@ export class HomePage { async enableTestMode() { await this.page.getByTestId(SettingsSelectors.SettingsMenuBtn).click(); await this.page.getByTestId(SettingsSelectors.ChangeNetworkAction).click(); - await this.page.waitForTimeout(1000); - await ( - await this.page.waitForSelector(this.testNetworkSelector, { timeout: 30000 }) - ).isVisible(); + await this.page.getByTestId(WalletDefaultNetworkConfigurationIds.testnet).click(); } diff --git a/theme/global/shimmer-styles.ts b/theme/global/shimmer-styles.ts new file mode 100644 index 00000000000..549a0d4091f --- /dev/null +++ b/theme/global/shimmer-styles.ts @@ -0,0 +1,10 @@ +import { css } from 'leather-styles/css'; + +export const shimmerStyles = css({ + '&[data-state=loading]': { + display: 'inline-block', + WebkitMask: 'linear-gradient(-60deg, #000 30%, #0005, #000 70%) right/300% 100%', + backgroundRepeat: 'no-repeat', + animation: 'shimmer 1.5s infinite', + }, +}); diff --git a/theme/keyframes.ts b/theme/keyframes.ts index beee9e090e6..23225294b17 100644 --- a/theme/keyframes.ts +++ b/theme/keyframes.ts @@ -12,4 +12,9 @@ export const keyframes: CssKeyframes = { from: { opacity: 0, transform: 'translateY(-12px) scale(0.9)' }, to: { opacity: 1, transform: 'translateY(0) scale(1)' }, }, + shimmer: { + '100%': { + maskPosition: 'left', + }, + }, };