Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes for v1.26.0 #1687

Merged
merged 12 commits into from
Mar 13, 2024
12 changes: 11 additions & 1 deletion mocks/blobs/blobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
6 changes: 3 additions & 3 deletions types/api/blobs.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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 = {
Expand Down
17 changes: 13 additions & 4 deletions ui/address/contract/methodForm/ContractMethodFieldInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();
Expand All @@ -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;

Expand Down Expand Up @@ -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 }
Expand All @@ -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>
Expand Down
4 changes: 2 additions & 2 deletions ui/address/contract/methodForm/ContractMethodFormOutputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
43 changes: 43 additions & 0 deletions ui/address/contract/methodForm/useFormatFieldValue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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 ]);
}
21 changes: 10 additions & 11 deletions ui/address/contract/methodForm/useValidateField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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';
}

Expand All @@ -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`;
Expand All @@ -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';
}
}

Expand Down
19 changes: 0 additions & 19 deletions ui/address/contract/methodForm/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = [];

Expand Down
86 changes: 50 additions & 36 deletions ui/blob/BlobInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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';
Expand All @@ -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
Expand All @@ -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>
);
};
Expand Down
2 changes: 1 addition & 1 deletion ui/marketplace/MarketplaceAppAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand Down
17 changes: 10 additions & 7 deletions ui/marketplace/useMarketplaceApps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 [];
Expand All @@ -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(() => ({
Expand Down
Loading
Loading