Commit 8c1be25f authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #1704 from blockscout/hotfix/v1.26.2

Hotfix `v1.26.2`
parents 9278adb7 12f0781e
...@@ -53,7 +53,7 @@ frontend: ...@@ -53,7 +53,7 @@ frontend:
NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json NEXT_PUBLIC_FEATURED_NETWORKS: https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/goerli.svg NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/goerli.svg
NEXT_PUBLIC_API_HOST: eth-goerli.blockscout.com NEXT_PUBLIC_API_HOST: eth-sepolia.blockscout.com
NEXT_PUBLIC_STATS_API_HOST: https://stats-goerli.k8s-dev.blockscout.com/ NEXT_PUBLIC_STATS_API_HOST: https://stats-goerli.k8s-dev.blockscout.com/
NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/ NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com
......
...@@ -2,8 +2,11 @@ import filetype from 'magic-bytes.js'; ...@@ -2,8 +2,11 @@ import filetype from 'magic-bytes.js';
import hexToBytes from 'lib/hexToBytes'; import hexToBytes from 'lib/hexToBytes';
import removeNonSignificantZeroBytes from './removeNonSignificantZeroBytes';
export default function guessDataType(data: string) { export default function guessDataType(data: string) {
const bytes = new Uint8Array(hexToBytes(data)); const bytes = new Uint8Array(hexToBytes(data));
const filteredBytes = removeNonSignificantZeroBytes(bytes);
return filetype(bytes)[0]; return filetype(filteredBytes)[0];
} }
export default function removeNonSignificantZeroBytes(bytes: Uint8Array) {
return shouldRemoveBytes(bytes) ? bytes.filter((item, index) => index % 32) : bytes;
}
// check if every 0, 32, 64, etc byte is 0 in the provided array
function shouldRemoveBytes(bytes: Uint8Array) {
let result = true;
for (let index = 0; index < bytes.length; index += 32) {
const element = bytes[index];
if (element === 0) {
continue;
} else {
result = false;
break;
}
}
return result;
}
export default function bytesToBase64(bytes: Uint8Array) {
let binary = '';
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
const base64String = btoa(binary);
return base64String;
}
import bytesToBase64 from './bytesToBase64';
import hexToBytes from './hexToBytes'; import hexToBytes from './hexToBytes';
export default function hexToBase64(hex: string) { export default function hexToBase64(hex: string) {
const bytes = new Uint8Array(hexToBytes(hex)); const bytes = new Uint8Array(hexToBytes(hex));
let binary = ''; return bytesToBase64(bytes);
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
const base64String = btoa(binary);
return base64String;
} }
...@@ -18,7 +18,7 @@ export default function useFormatFieldValue({ argType, argTypeMatchInt }: Params ...@@ -18,7 +18,7 @@ export default function useFormatFieldValue({ argType, argTypeMatchInt }: Params
if (argTypeMatchInt) { if (argTypeMatchInt) {
const formattedString = value.replace(/\s/g, ''); const formattedString = value.replace(/\s/g, '');
return parseInt(formattedString); return formattedString;
} }
if (argType === 'bool') { if (argType === 'bool') {
...@@ -26,11 +26,11 @@ export default function useFormatFieldValue({ argType, argTypeMatchInt }: Params ...@@ -26,11 +26,11 @@ export default function useFormatFieldValue({ argType, argTypeMatchInt }: Params
switch (formattedValue) { switch (formattedValue) {
case 'true': { case 'true': {
return true; return 'true';
} }
case 'false':{ case 'false':{
return false; return 'false';
} }
default: default:
......
...@@ -18,15 +18,13 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt ...@@ -18,15 +18,13 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt
return argType.match(BYTES_REGEXP); return argType.match(BYTES_REGEXP);
}, [ argType ]); }, [ argType ]);
// some values are formatted before they are sent to the validator return React.useCallback((value: string | undefined) => {
// see ./useFormatFieldValue.tsx hook
return React.useCallback((value: string | number | boolean | undefined) => {
if (value === undefined || value === '') { if (value === undefined || value === '') {
return isOptional ? true : 'Field is required'; return isOptional ? true : 'Field is required';
} }
if (argType === 'address') { if (argType === 'address') {
if (typeof value !== 'string' || !isAddress(value)) { if (!isAddress(value)) {
return 'Invalid address format'; return 'Invalid address format';
} }
...@@ -41,11 +39,13 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt ...@@ -41,11 +39,13 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt
} }
if (argTypeMatchInt) { if (argTypeMatchInt) {
if (typeof value !== 'number' || Object.is(value, NaN)) { const formattedValue = Number(value);
if (Object.is(formattedValue, NaN)) {
return 'Invalid integer format'; return 'Invalid integer format';
} }
if (value > argTypeMatchInt.max || value < argTypeMatchInt.min) { if (formattedValue > argTypeMatchInt.max || formattedValue < argTypeMatchInt.min) {
const lowerBoundary = argTypeMatchInt.isUnsigned ? '0' : `-1 * 2 ^ ${ Number(argTypeMatchInt.power) - 1 }`; 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`; 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`; return `Value should be in range from "${ lowerBoundary }" to "${ upperBoundary }" inclusively`;
...@@ -55,7 +55,7 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt ...@@ -55,7 +55,7 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt
} }
if (argType === 'bool') { if (argType === 'bool') {
if (typeof value !== 'boolean') { if (value !== 'true' && value !== 'false') {
return 'Invalid boolean format. Allowed values: true, false'; return 'Invalid boolean format. Allowed values: true, false';
} }
} }
......
...@@ -4,6 +4,7 @@ import React from 'react'; ...@@ -4,6 +4,7 @@ import React from 'react';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import BlobData from './BlobData'; import BlobData from './BlobData';
import imageBlobWithZeroesBytes from './image_with_zeroes.blob';
test.use({ viewport: { width: 500, height: 300 } }); test.use({ viewport: { width: 500, height: 300 } });
...@@ -40,3 +41,13 @@ test('image', async({ mount }) => { ...@@ -40,3 +41,13 @@ test('image', async({ mount }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('image blob with zeroes bytes', async({ mount }) => {
const component = await mount(
<TestApp>
<BlobData hash="0x01" data={ imageBlobWithZeroesBytes }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
...@@ -2,6 +2,8 @@ import { Flex, GridItem, Select, Skeleton, Button } from '@chakra-ui/react'; ...@@ -2,6 +2,8 @@ import { Flex, GridItem, Select, Skeleton, Button } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import * as blobUtils from 'lib/blob'; import * as blobUtils from 'lib/blob';
import removeNonSignificantZeroBytes from 'lib/blob/removeNonSignificantZeroBytes';
import bytesToBase64 from 'lib/bytesToBase64';
import downloadBlob from 'lib/downloadBlob'; import downloadBlob from 'lib/downloadBlob';
import hexToBase64 from 'lib/hexToBase64'; import hexToBase64 from 'lib/hexToBase64';
import hexToBytes from 'lib/hexToBytes'; import hexToBytes from 'lib/hexToBytes';
...@@ -49,7 +51,8 @@ const BlobData = ({ data, isLoading, hash }: Props) => { ...@@ -49,7 +51,8 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
switch (format) { switch (format) {
case 'Image': { case 'Image': {
const bytes = new Uint8Array(hexToBytes(data)); const bytes = new Uint8Array(hexToBytes(data));
return new Blob([ bytes ], { type: guessedType?.mime }); const filteredBytes = removeNonSignificantZeroBytes(bytes);
return new Blob([ filteredBytes ], { type: guessedType?.mime });
} }
case 'UTF-8': { case 'UTF-8': {
return new Blob([ hexToUtf8(data) ], { type: guessedType?.mime ?? 'text/plain' }); return new Blob([ hexToUtf8(data) ], { type: guessedType?.mime ?? 'text/plain' });
...@@ -74,19 +77,22 @@ const BlobData = ({ data, isLoading, hash }: Props) => { ...@@ -74,19 +77,22 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
return <RawDataSnippet data="Not an image" showCopy={ false } isLoading={ isLoading }/>; return <RawDataSnippet data="Not an image" showCopy={ false } isLoading={ isLoading }/>;
} }
const base64 = hexToBase64(data); const bytes = new Uint8Array(hexToBytes(data));
const filteredBytes = removeNonSignificantZeroBytes(bytes);
const base64 = bytesToBase64(filteredBytes);
const imgSrc = `data:${ guessedType.mime };base64,${ base64 }`; const imgSrc = `data:${ guessedType.mime };base64,${ base64 }`;
return <BlobDataImage src={ imgSrc }/>; return <BlobDataImage src={ imgSrc }/>;
} }
case 'UTF-8': case 'UTF-8':
return <RawDataSnippet data={ hexToUtf8(data) } showCopy={ false } isLoading={ isLoading }/>; return <RawDataSnippet data={ hexToUtf8(data) } showCopy={ false } isLoading={ isLoading } contentProps={{ wordBreak: 'break-word' }}/>;
case 'Base64': case 'Base64':
return <RawDataSnippet data={ hexToBase64(data) } showCopy={ false } isLoading={ isLoading }/>; return <RawDataSnippet data={ hexToBase64(data) } showCopy={ false } isLoading={ isLoading }/>;
case 'Raw': case 'Raw':
return <RawDataSnippet data={ data } showCopy={ false } isLoading={ isLoading }/>; return <RawDataSnippet data={ data } showCopy={ false } isLoading={ isLoading }/>;
default: default:
return <span>fallback</span>; return <span/>;
} }
})(); })();
...@@ -119,7 +125,7 @@ const BlobData = ({ data, isLoading, hash }: Props) => { ...@@ -119,7 +125,7 @@ const BlobData = ({ data, isLoading, hash }: Props) => {
Download Download
</Button> </Button>
</Skeleton> </Skeleton>
<CopyToClipboard text={ JSON.stringify(data) } isLoading={ isLoading }/> <CopyToClipboard text={ data } isLoading={ isLoading }/>
</Flex> </Flex>
{ content } { content }
</GridItem> </GridItem>
......
This source diff could not be displayed because it is too large. You can view the blob instead.
import type { ChakraProps } from '@chakra-ui/react';
import { Box, Flex, chakra, useColorModeValue, Skeleton } from '@chakra-ui/react'; import { Box, Flex, chakra, useColorModeValue, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -12,9 +13,10 @@ interface Props { ...@@ -12,9 +13,10 @@ interface Props {
textareaMaxHeight?: string; textareaMaxHeight?: string;
showCopy?: boolean; showCopy?: boolean;
isLoading?: boolean; isLoading?: boolean;
contentProps?: ChakraProps;
} }
const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textareaMaxHeight, showCopy = true, isLoading }: Props) => { const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textareaMaxHeight, showCopy = true, isLoading, contentProps }: Props) => {
// see issue in theme/components/Textarea.ts // see issue in theme/components/Textarea.ts
const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b'); const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b');
return ( return (
...@@ -39,6 +41,7 @@ const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textare ...@@ -39,6 +41,7 @@ const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textare
overflowX="hidden" overflowX="hidden"
overflowY="auto" overflowY="auto"
isLoaded={ !isLoading } isLoaded={ !isLoading }
{ ...contentProps }
> >
{ data } { data }
</Skeleton> </Skeleton>
......
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