Commit 36dae944 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Graceful service degradation: address page (#1580)

* Graceful service degradation: address page

Fixes #1579

* fix ts

* [skip ci] check if public client was initialized
parent 85cc44e1
...@@ -53,6 +53,10 @@ export const GET_TRANSACTION_RECEIPT: TransactionReceipt = { ...@@ -53,6 +53,10 @@ export const GET_TRANSACTION_RECEIPT: TransactionReceipt = {
export const GET_TRANSACTION_CONFIRMATIONS = BigInt(420); export const GET_TRANSACTION_CONFIRMATIONS = BigInt(420);
export const GET_BALANCE = BigInt(42_000_000_000_000);
export const GET_TRANSACTIONS_COUNT = 42;
export const GET_BLOCK: GetBlockReturnType<Chain, false, 'latest'> = { export const GET_BLOCK: GetBlockReturnType<Chain, false, 'latest'> = {
baseFeePerGas: BigInt(11), baseFeePerGas: BigInt(11),
difficulty: BigInt(111), difficulty: BigInt(111),
......
...@@ -39,7 +39,7 @@ export interface Address extends UserTags { ...@@ -39,7 +39,7 @@ export interface Address extends UserTags {
export interface AddressCounters { export interface AddressCounters {
transactions_count: string; transactions_count: string;
token_transfers_count: string; token_transfers_count: string;
gas_usage_count: string; gas_usage_count: string | null;
validations_count: string | null; validations_count: string | null;
} }
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { WindowProvider } from 'wagmi'; import type { WindowProvider } from 'wagmi';
import type { Address } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources';
import * as addressMock from 'mocks/address/address'; import * as addressMock from 'mocks/address/address';
import * as countersMock from 'mocks/address/counters'; import * as countersMock from 'mocks/address/counters';
import * as tokensMock from 'mocks/address/tokens'; import * as tokensMock from 'mocks/address/tokens';
...@@ -15,6 +11,7 @@ import * as configs from 'playwright/utils/configs'; ...@@ -15,6 +11,7 @@ import * as configs from 'playwright/utils/configs';
import AddressDetails from './AddressDetails'; import AddressDetails from './AddressDetails';
import MockAddressPage from './testUtils/MockAddressPage'; import MockAddressPage from './testUtils/MockAddressPage';
import type { AddressQuery } from './utils/useAddressQuery';
const ADDRESS_HASH = addressMock.hash; const ADDRESS_HASH = addressMock.hash;
const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH }); const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH });
...@@ -40,7 +37,7 @@ test('contract +@mobile', async({ mount, page }) => { ...@@ -40,7 +37,7 @@ test('contract +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<AddressDetails addressQuery={{ data: addressMock.contract } as UseQueryResult<Address, ResourceError>}/> <AddressDetails addressQuery={{ data: addressMock.contract } as AddressQuery}/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -82,7 +79,7 @@ test('token', async({ mount, page }) => { ...@@ -82,7 +79,7 @@ test('token', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<MockAddressPage> <MockAddressPage>
<AddressDetails addressQuery={{ data: addressMock.token } as UseQueryResult<Address, ResourceError>}/> <AddressDetails addressQuery={{ data: addressMock.token } as AddressQuery}/>
</MockAddressPage> </MockAddressPage>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
...@@ -106,7 +103,7 @@ test('validator +@mobile', async({ mount, page }) => { ...@@ -106,7 +103,7 @@ test('validator +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<AddressDetails addressQuery={{ data: addressMock.validator } as UseQueryResult<Address, ResourceError>}/> <AddressDetails addressQuery={{ data: addressMock.validator } as AddressQuery}/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
This diff is collapsed.
...@@ -16,6 +16,7 @@ interface Props { ...@@ -16,6 +16,7 @@ interface Props {
address: string; address: string;
onClick: () => void; onClick: () => void;
isAddressQueryLoading: boolean; isAddressQueryLoading: boolean;
isDegradedData: boolean;
} }
const PROP_TO_TAB = { const PROP_TO_TAB = {
...@@ -24,7 +25,7 @@ const PROP_TO_TAB = { ...@@ -24,7 +25,7 @@ const PROP_TO_TAB = {
validations_count: 'blocks_validated', validations_count: 'blocks_validated',
}; };
const AddressCounterItem = ({ prop, query, address, onClick, isAddressQueryLoading }: Props) => { const AddressCounterItem = ({ prop, query, address, onClick, isAddressQueryLoading, isDegradedData }: Props) => {
if (query.isPlaceholderData || isAddressQueryLoading) { if (query.isPlaceholderData || isAddressQueryLoading) {
return <Skeleton h={ 5 } w="80px" borderRadius="full"/>; return <Skeleton h={ 5 } w="80px" borderRadius="full"/>;
} }
...@@ -44,6 +45,11 @@ const AddressCounterItem = ({ prop, query, address, onClick, isAddressQueryLoadi ...@@ -44,6 +45,11 @@ const AddressCounterItem = ({ prop, query, address, onClick, isAddressQueryLoadi
if (data === '0') { if (data === '0') {
return <span>0</span>; return <span>0</span>;
} }
if (isDegradedData) {
return <span>{ Number(data).toLocaleString() }</span>;
}
return ( return (
<LinkInternal href={ route({ pathname: '/address/[hash]', query: { hash: address, tab: PROP_TO_TAB[prop] } }) } onClick={ onClick }> <LinkInternal href={ route({ pathname: '/address/[hash]', query: { hash: address, tab: PROP_TO_TAB[prop] } }) } onClick={ onClick }>
{ Number(data).toLocaleString() } { Number(data).toLocaleString() }
......
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import type { AddressCounters } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import { publicClient } from 'lib/web3/client';
import { ADDRESS_COUNTERS } from 'stubs/address';
import { GET_TRANSACTIONS_COUNT } from 'stubs/RPC';
import type { AddressQuery } from './useAddressQuery';
type RpcResponseType = [
number | null,
];
export type AddressCountersQuery = UseQueryResult<AddressCounters, ResourceError<{ status: number }>> & {
isDegradedData: boolean;
};
interface Params {
hash: string;
addressQuery: AddressQuery;
}
export default function useAddressQuery({ hash, addressQuery }: Params): AddressCountersQuery {
const enabled = Boolean(hash) && !addressQuery.isPlaceholderData;
const apiQuery = useApiQuery<'address_counters', { status: number }>('address_counters', {
pathParams: { hash },
queryOptions: {
enabled: enabled && !addressQuery.isDegradedData,
placeholderData: ADDRESS_COUNTERS,
refetchOnMount: false,
},
});
const rpcQuery = useQuery<RpcResponseType, unknown, AddressCounters | null>({
queryKey: [ 'RPC', 'address_counters', { hash } ],
queryFn: async() => {
const txCount = publicClient.getTransactionCount({ address: hash as `0x${ string }` }).catch(() => null);
return Promise.all([
txCount,
]);
},
select: (response) => {
const [ txCount ] = response;
return {
transactions_count: txCount?.toString() ?? '0',
token_transfers_count: '0',
gas_usage_count: null,
validations_count: null,
};
},
placeholderData: [ GET_TRANSACTIONS_COUNT ],
enabled: enabled && (addressQuery.isDegradedData || apiQuery.isError),
retry: false,
refetchOnMount: false,
});
const isRpcQuery = Boolean((addressQuery.isDegradedData || apiQuery.isError) && rpcQuery.data);
const query = isRpcQuery ? rpcQuery as UseQueryResult<AddressCounters, ResourceError<{ status: number }>> : apiQuery;
return {
...query,
isDegradedData: isRpcQuery,
};
}
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { Address } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import { retry } from 'lib/api/useQueryClientConfig';
import { SECOND } from 'lib/consts';
import { publicClient } from 'lib/web3/client';
import { ADDRESS_INFO } from 'stubs/address';
import { GET_BALANCE } from 'stubs/RPC';
type RpcResponseType = [
bigint | null,
];
export type AddressQuery = UseQueryResult<Address, ResourceError<{ status: number }>> & {
isDegradedData: boolean;
};
interface Params {
hash: string;
}
export default function useAddressQuery({ hash }: Params): AddressQuery {
const [ isRefetchEnabled, setRefetchEnabled ] = React.useState(false);
const apiQuery = useApiQuery<'address', { status: number }>('address', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
placeholderData: ADDRESS_INFO,
refetchOnMount: false,
retry: (failureCount, error) => {
if (isRefetchEnabled) {
return false;
}
return retry(failureCount, error);
},
refetchInterval: (): number | false => {
return isRefetchEnabled ? 15 * SECOND : false;
},
},
});
const rpcQuery = useQuery<RpcResponseType, unknown, Address | null>({
queryKey: [ 'RPC', 'address', { hash } ],
queryFn: async() => {
if (!publicClient) {
throw new Error('No public RPC client');
}
const balance = publicClient.getBalance({ address: hash as `0x${ string }` }).catch(() => null);
return Promise.all([
balance,
]);
},
select: (response) => {
const [ balance ] = response;
if (!balance) {
return null;
}
return {
hash,
block_number_balance_updated_at: null,
coin_balance: balance.toString(),
creator_address_hash: null,
creation_tx_hash: null,
exchange_rate: null,
ens_domain_name: null,
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false,
has_logs: false,
has_methods_read: false,
has_methods_read_proxy: false,
has_methods_write: false,
has_methods_write_proxy: false,
has_token_transfers: false,
has_tokens: false,
has_validated_blocks: false,
implementation_address: null,
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
token: null,
watchlist_address_id: null,
private_tags: null,
public_tags: null,
watchlist_names: null,
};
},
placeholderData: [ GET_BALANCE ],
enabled: apiQuery.isError || apiQuery.errorUpdateCount > 0,
retry: false,
refetchOnMount: false,
});
React.useEffect(() => {
if (apiQuery.isPlaceholderData || !publicClient) {
return;
}
if (apiQuery.isError && apiQuery.errorUpdateCount === 1) {
setRefetchEnabled(true);
} else if (!apiQuery.isError) {
setRefetchEnabled(false);
}
}, [ apiQuery.errorUpdateCount, apiQuery.isError, apiQuery.isPlaceholderData ]);
React.useEffect(() => {
if (!rpcQuery.isPlaceholderData && !rpcQuery.data) {
setRefetchEnabled(false);
}
}, [ rpcQuery.data, rpcQuery.isPlaceholderData ]);
const isRpcQuery = Boolean((apiQuery.isError || apiQuery.isPlaceholderData) && apiQuery.errorUpdateCount > 0 && rpcQuery.data && publicClient);
const query = isRpcQuery ? rpcQuery as UseQueryResult<Address, ResourceError<{ status: number }>> : apiQuery;
return {
...query,
isDegradedData: isRpcQuery,
};
}
...@@ -10,7 +10,7 @@ import { useAppContext } from 'lib/contexts/app'; ...@@ -10,7 +10,7 @@ import { useAppContext } from 'lib/contexts/app';
import useContractTabs from 'lib/hooks/useContractTabs'; import useContractTabs from 'lib/hooks/useContractTabs';
import useIsSafeAddress from 'lib/hooks/useIsSafeAddress'; import useIsSafeAddress from 'lib/hooks/useIsSafeAddress';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_INFO, ADDRESS_TABS_COUNTERS } from 'stubs/address'; import { ADDRESS_TABS_COUNTERS } from 'stubs/address';
import { USER_OPS_ACCOUNT } from 'stubs/userOps'; import { USER_OPS_ACCOUNT } from 'stubs/userOps';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated'; import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressCoinBalance from 'ui/address/AddressCoinBalance';
...@@ -27,6 +27,7 @@ import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton'; ...@@ -27,6 +27,7 @@ import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton';
import AddressQrCode from 'ui/address/details/AddressQrCode'; import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressEnsDomains from 'ui/address/ensDomains/AddressEnsDomains'; import AddressEnsDomains from 'ui/address/ensDomains/AddressEnsDomains';
import SolidityscanReport from 'ui/address/SolidityscanReport'; import SolidityscanReport from 'ui/address/SolidityscanReport';
import useAddressQuery from 'ui/address/utils/useAddressQuery';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet'; import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
...@@ -48,13 +49,7 @@ const AddressPageContent = () => { ...@@ -48,13 +49,7 @@ const AddressPageContent = () => {
const tabsScrollRef = React.useRef<HTMLDivElement>(null); const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const addressQuery = useApiQuery('address', { const addressQuery = useAddressQuery({ hash });
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
placeholderData: ADDRESS_INFO,
},
});
const addressTabsCountersQuery = useApiQuery('address_tabs_counters', { const addressTabsCountersQuery = useApiQuery('address_tabs_counters', {
pathParams: { hash }, pathParams: { hash },
...@@ -176,7 +171,7 @@ const AddressPageContent = () => { ...@@ -176,7 +171,7 @@ const AddressPageContent = () => {
/> />
); );
const content = addressQuery.isError ? null : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/>; const content = (addressQuery.isError || addressQuery.isDegradedData) ? null : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/>;
const backLink = React.useMemo(() => { const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts'); const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment