Commit d7b9d940 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #1687 from blockscout/hotfix/v1.26.0

fixes for `v1.26.0`
parents 760c69e2 5524c8d5
......@@ -20,7 +20,17 @@ export const base2: Blob = {
],
};
export const withoutData: Blob = {
blob_data: null,
hash: '0x0197fdb17195c176b23160f335daabd4b6a231aaaadd73ec567877c66a3affd3',
kzg_commitment: null,
kzg_proof: null,
transaction_hashes: [
{ block_consensus: true, transaction_hash: '0x22d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193' },
],
};
export const txBlobs: TxBlobs = {
items: [ base1, base2 ],
items: [ base1, base2, withoutData ],
next_page_params: null,
};
export interface TxBlob {
hash: string;
blob_data: string;
kzg_commitment: string;
kzg_proof: string;
blob_data: string | null;
kzg_commitment: string | null;
kzg_proof: string | null;
}
export type TxBlobs = {
......
......@@ -10,6 +10,7 @@ import ClearButton from 'ui/shared/ClearButton';
import ContractMethodFieldLabel from './ContractMethodFieldLabel';
import ContractMethodMultiplyButton from './ContractMethodMultiplyButton';
import useArgTypeMatchInt from './useArgTypeMatchInt';
import useFormatFieldValue from './useFormatFieldValue';
import useValidateField from './useValidateField';
interface Props {
......@@ -29,15 +30,22 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
const argTypeMatchInt = useArgTypeMatchInt({ argType: data.type });
const validate = useValidateField({ isOptional, argType: data.type, argTypeMatchInt });
const format = useFormatFieldValue({ argType: data.type, argTypeMatchInt });
const { control, setValue, getValues } = useFormContext();
const { field, fieldState } = useController({ control, name, rules: { validate, required: isOptional ? false : 'Field is required' } });
const { field, fieldState } = useController({ control, name, rules: { validate } });
const inputBgColor = useColorModeValue('white', 'black');
const nativeCoinRowBgColor = useColorModeValue('gray.100', 'gray.700');
const hasMultiplyButton = argTypeMatchInt && Number(argTypeMatchInt.power) >= 64;
const handleChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const formattedValue = format(event.target.value);
field.onChange(formattedValue); // data send back to hook form
setValue(name, formattedValue); // UI state
}, [ field, name, setValue, format ]);
const handleClear = React.useCallback(() => {
setValue(name, '');
ref.current?.focus();
......@@ -46,9 +54,9 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
const handleMultiplyButtonClick = React.useCallback((power: number) => {
const zeroes = Array(power).fill('0').join('');
const value = getValues(name);
const newValue = value ? value + zeroes : '1' + zeroes;
const newValue = format(value ? value + zeroes : '1' + zeroes);
setValue(name, newValue);
}, [ getValues, name, setValue ]);
}, [ format, getValues, name, setValue ]);
const error = fieldState.error;
......@@ -76,6 +84,7 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
allowNegative: !argTypeMatchInt.isUnsigned,
} : {}) }
ref={ ref }
onChange={ handleChange }
required={ !isOptional }
isInvalid={ Boolean(error) }
placeholder={ data.type }
......@@ -84,7 +93,7 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
paddingRight={ hasMultiplyButton ? '120px' : '40px' }
/>
<InputRightElement w="auto" right={ 1 }>
{ typeof field.value === 'string' && field.value.replace('\n', '') && <ClearButton onClick={ handleClear } isDisabled={ isDisabled }/> }
{ field.value !== undefined && field.value !== '' && <ClearButton onClick={ handleClear } isDisabled={ isDisabled }/> }
{ hasMultiplyButton && <ContractMethodMultiplyButton onClick={ handleMultiplyButtonClick } isDisabled={ isDisabled }/> }
</InputRightElement>
</InputGroup>
......
......@@ -20,11 +20,11 @@ const ContractMethodFormOutputs = ({ data }: Props) => {
<p>
{ data.map(({ type, name }, index) => {
return (
<>
<React.Fragment key={ index }>
<chakra.span fontWeight={ 500 }>{ name } </chakra.span>
<span>{ name ? `(${ type })` : type }</span>
{ index < data.length - 1 && <span>, </span> }
</>
</React.Fragment>
);
}) }
</p>
......
import React from 'react';
import type { SmartContractMethodArgType } from 'types/api/contract';
import type { MatchInt } from './useArgTypeMatchInt';
interface Params {
argType: SmartContractMethodArgType;
argTypeMatchInt: MatchInt | null;
}
export default function useFormatFieldValue({ argType, argTypeMatchInt }: Params) {
return React.useCallback((value: string | undefined) => {
if (!value) {
return;
}
if (argTypeMatchInt) {
const formattedString = value.replace(/\s/g, '');
return parseInt(formattedString);
}
if (argType === 'bool') {
const formattedValue = value.toLowerCase();
switch (formattedValue) {
case 'true': {
return true;
}
case 'false':{
return false;
}
default:
return value;
}
}
return value;
}, [ argType, argTypeMatchInt ]);
}
......@@ -4,7 +4,7 @@ import { getAddress, isAddress, isHex } from 'viem';
import type { SmartContractMethodArgType } from 'types/api/contract';
import type { MatchInt } from './useArgTypeMatchInt';
import { BYTES_REGEXP, formatBooleanValue } from './utils';
import { BYTES_REGEXP } from './utils';
interface Params {
argType: SmartContractMethodArgType;
......@@ -18,13 +18,15 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt
return argType.match(BYTES_REGEXP);
}, [ argType ]);
return React.useCallback((value: string | undefined) => {
if (!value) {
// some values are formatted before they are sent to the validator
// see ./useFormatFieldValue.tsx hook
return React.useCallback((value: string | number | boolean | undefined) => {
if (value === undefined || value === '') {
return isOptional ? true : 'Field is required';
}
if (argType === 'address') {
if (!isAddress(value)) {
if (typeof value !== 'string' || !isAddress(value)) {
return 'Invalid address format';
}
......@@ -39,13 +41,11 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt
}
if (argTypeMatchInt) {
const formattedValue = Number(value.replace(/\s/g, ''));
if (Object.is(formattedValue, NaN)) {
if (typeof value !== 'number' || Object.is(value, NaN)) {
return 'Invalid integer format';
}
if (formattedValue > argTypeMatchInt.max || formattedValue < argTypeMatchInt.min) {
if (value > argTypeMatchInt.max || value < argTypeMatchInt.min) {
const lowerBoundary = argTypeMatchInt.isUnsigned ? '0' : `-1 * 2 ^ ${ Number(argTypeMatchInt.power) - 1 }`;
const upperBoundary = argTypeMatchInt.isUnsigned ? `2 ^ ${ argTypeMatchInt.power } - 1` : `2 ^ ${ Number(argTypeMatchInt.power) - 1 } - 1`;
return `Value should be in range from "${ lowerBoundary }" to "${ upperBoundary }" inclusively`;
......@@ -55,9 +55,8 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt
}
if (argType === 'bool') {
const formattedValue = formatBooleanValue(value);
if (formattedValue === undefined) {
return 'Invalid boolean format. Allowed values: 0, 1, true, false';
if (typeof value !== 'boolean') {
return 'Invalid boolean format. Allowed values: true, false';
}
}
......
......@@ -17,25 +17,6 @@ export const getIntBoundaries = (power: number, isUnsigned: boolean) => {
return [ min, max ];
};
export const formatBooleanValue = (value: string) => {
const formattedValue = value.toLowerCase();
switch (formattedValue) {
case 'true':
case '1': {
return 'true';
}
case 'false':
case '0': {
return 'false';
}
default:
return;
}
};
export function transformFormDataToMethodArgs(formData: ContractMethodFormFields) {
const result: Array<unknown> = [];
......
import { Grid, Skeleton } from '@chakra-ui/react';
import { Alert, Grid, GridItem, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { Blob } from 'types/api/blobs';
......@@ -17,45 +17,56 @@ interface Props {
}
const BlobInfo = ({ data, isLoading }: Props) => {
const size = data.blob_data.replace('0x', '').length / 2;
return (
<Grid
columnGap={ 8 }
rowGap={ 3 }
templateColumns={{ base: 'minmax(0, 1fr)', lg: '216px minmax(728px, auto)' }}
>
<DetailsInfoItem
title="Proof"
hint="Zero knowledge proof. Allows for quick verification of commitment"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="pre-wrap" wordBreak="break-all" lineHeight={ 6 } my="-2px">
{ data.kzg_proof }
<CopyToClipboard text={ data.kzg_proof } isLoading={ isLoading }/>
</Skeleton>
</DetailsInfoItem>
<DetailsInfoItem
title="Commitment"
hint="Commitment to the data in the blob"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="pre-wrap" wordBreak="break-all" lineHeight={ 6 } my="-2px">
{ data.kzg_commitment }
<CopyToClipboard text={ data.kzg_commitment } isLoading={ isLoading }/>
</Skeleton>
</DetailsInfoItem>
<DetailsInfoItem
title="Size, bytes"
hint="Blob size in bytes"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="pre-wrap" wordBreak="break-all">
{ size.toLocaleString() }
</Skeleton>
</DetailsInfoItem>
{ !data.blob_data && (
<GridItem colSpan={{ base: undefined, lg: 2 }} mb={ 3 }>
<Skeleton isLoaded={ !isLoading }>
<Alert status="warning">This blob is not yet indexed</Alert>
</Skeleton>
</GridItem>
) }
{ data.kzg_proof && (
<DetailsInfoItem
title="Proof"
hint="Zero knowledge proof. Allows for quick verification of commitment"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="pre-wrap" wordBreak="break-all" lineHeight={ 6 } my="-2px">
{ data.kzg_proof }
<CopyToClipboard text={ data.kzg_proof } isLoading={ isLoading }/>
</Skeleton>
</DetailsInfoItem>
) }
{ data.kzg_commitment && (
<DetailsInfoItem
title="Commitment"
hint="Commitment to the data in the blob"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="pre-wrap" wordBreak="break-all" lineHeight={ 6 } my="-2px">
{ data.kzg_commitment }
<CopyToClipboard text={ data.kzg_commitment } isLoading={ isLoading }/>
</Skeleton>
</DetailsInfoItem>
) }
{ data.blob_data && (
<DetailsInfoItem
title="Size, bytes"
hint="Blob size in bytes"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="pre-wrap" wordBreak="break-all">
{ (data.blob_data.replace('0x', '').length / 2).toLocaleString() }
</Skeleton>
</DetailsInfoItem>
) }
<DetailsInfoItemDivider/>
{ data.blob_data && <DetailsInfoItemDivider/> }
{ data.transaction_hashes[0] && (
<DetailsInfoItem
......@@ -68,9 +79,12 @@ const BlobInfo = ({ data, isLoading }: Props) => {
) }
<DetailsSponsoredItem isLoading={ isLoading }/>
<DetailsInfoItemDivider/>
<BlobData data={ data.blob_data } hash={ data.hash } isLoading={ isLoading }/>
{ data.blob_data && (
<>
<DetailsInfoItemDivider/>
<BlobData data={ data.blob_data } hash={ data.hash } isLoading={ isLoading }/>
</>
) }
</Grid>
);
};
......
......@@ -19,7 +19,7 @@ const MarketplaceAppAlert = ({ internalWallet, isWalletConnected }: Props) => {
icon = 'integration/full';
text = 'Your wallet is connected with Blockscout';
status = 'success';
} else if (isWalletConnected) {
} else if (!internalWallet) {
icon = 'integration/partial';
text = 'Connect your wallet in the app below';
}
......
......@@ -50,20 +50,23 @@ function sortApps(apps: Array<MarketplaceAppOverview>, favoriteApps: Array<strin
export default function useMarketplaceApps(
filter: string,
selectedCategoryId: string = MarketplaceCategory.ALL,
favoriteApps: Array<string> = [],
favoriteApps: Array<string> | undefined = undefined,
isFavoriteAppsLoaded: boolean = false, // eslint-disable-line @typescript-eslint/no-inferrable-types
) {
const fetch = useFetch();
const apiFetch = useApiFetch();
// Update favorite apps only when selectedCategoryId changes to avoid sortApps to be called on each favorite app click
const lastFavoriteAppsRef = React.useRef(favoriteApps);
const [ snapshotFavoriteApps, setSnapshotFavoriteApps ] = React.useState<Array<string> | undefined>();
React.useEffect(() => {
lastFavoriteAppsRef.current = favoriteApps;
if (isFavoriteAppsLoaded) {
setSnapshotFavoriteApps(favoriteApps);
}
}, [ selectedCategoryId, isFavoriteAppsLoaded ]); // eslint-disable-line react-hooks/exhaustive-deps
const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>({
queryKey: [ 'marketplace-dapps' ],
queryKey: [ 'marketplace-dapps', snapshotFavoriteApps, favoriteApps ],
queryFn: async() => {
if (!feature.isEnabled) {
return [];
......@@ -73,14 +76,14 @@ export default function useMarketplaceApps(
return apiFetch('marketplace_dapps', { pathParams: { chainId: config.chain.id } });
}
},
select: (data) => sortApps(data as Array<MarketplaceAppOverview>, lastFavoriteAppsRef.current),
select: (data) => sortApps(data as Array<MarketplaceAppOverview>, snapshotFavoriteApps || []),
placeholderData: feature.isEnabled ? Array(9).fill(MARKETPLACE_APP) : undefined,
staleTime: Infinity,
enabled: feature.isEnabled,
enabled: feature.isEnabled && (!favoriteApps || Boolean(snapshotFavoriteApps)),
});
const displayedApps = React.useMemo(() => {
return data?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps)) || [];
return data?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps || [])) || [];
}, [ selectedCategoryId, data, filter, favoriteApps ]);
return React.useMemo(() => ({
......
......@@ -47,3 +47,22 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
maskColor: configs.maskColor,
});
});
test('without data', async({ mount, page }) => {
await page.route(BLOB_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blobsMock.withoutData),
}));
const component = await mount(
<TestApp>
<Blob/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot({
mask: [ page.locator(configs.adsBannerSelector) ],
maskColor: configs.maskColor,
});
});
......@@ -16,7 +16,7 @@ const ValidatorStatus = ({ state, isLoading }: Props) => {
case 'probation':
return <StatusTag type="pending" text="Probation" isLoading={ isLoading }/>;
case 'inactive':
return <StatusTag type="error" text="Failed" isLoading={ isLoading }/>;
return <StatusTag type="error" text="Inactive" isLoading={ isLoading }/>;
}
};
......
......@@ -13,7 +13,7 @@ interface Props {
}
const TxBlobListItem = ({ data, isLoading }: Props) => {
const size = data.blob_data.replace('0x', '').length / 2;
const size = data.blob_data ? data.blob_data.replace('0x', '').length / 2 : '-';
return (
<ListItemMobileGrid.Container>
......@@ -24,7 +24,7 @@ const TxBlobListItem = ({ data, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>Data type</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BlobDataType isLoading={ isLoading } data={ data.blob_data }/>
{ data.blob_data ? <BlobDataType isLoading={ isLoading } data={ data.blob_data }/> : '-' }
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Size, bytes</ListItemMobileGrid.Label>
......
......@@ -12,7 +12,7 @@ interface Props {
}
const TxBlobsTableItem = ({ data, isLoading }: Props) => {
const size = data.blob_data.replace('0x', '').length / 2;
const size = data.blob_data ? data.blob_data.replace('0x', '').length / 2 : '-';
return (
<Tr alignItems="top">
......@@ -20,7 +20,7 @@ const TxBlobsTableItem = ({ data, isLoading }: Props) => {
<BlobEntity hash={ data.hash } noIcon isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle">
<BlobDataType isLoading={ isLoading } data={ data.blob_data }/>
{ data.blob_data ? <BlobDataType isLoading={ isLoading } data={ data.blob_data }/> : '-' }
</Td>
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading } display="inline-block">
......
......@@ -36,7 +36,7 @@ const ValidatorsFilter = ({ onChange, defaultValue, isActive }: Props) => {
<MenuItemOption value="all">All</MenuItemOption>
<MenuItemOption value="active">Active</MenuItemOption>
<MenuItemOption value="probation">Probation</MenuItemOption>
<MenuItemOption value="inactive">Failed</MenuItemOption>
<MenuItemOption value="inactive">Inactive</MenuItemOption>
</MenuOptionGroup>
</MenuList>
</Menu>
......
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