diff --git a/src/components/Args.tsx b/src/components/Args.tsx index 81ca11e6..1e6a8c62 100644 --- a/src/components/Args.tsx +++ b/src/components/Args.tsx @@ -4,11 +4,13 @@ import { ChainProperties } from '@polkadot/types/interfaces'; import { encodeAddress } from '@polkadot/util-crypto'; import { Table } from 'antd'; import { ColumnsType } from 'antd/lib/table'; +import { isAscii, isHex, u8aToString } from '@polkadot/util'; import { isArray, isObject, isString, toString } from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useApi } from '../hooks'; import { Chain } from '../providers/api-provider'; +import { hexToU8aFixed } from '../utils/helper/hexToU8a'; import { formatBalance, isAddressType, @@ -140,6 +142,17 @@ export function Args({ args, className, section, method }: ArgsProps) { return value; } + if (isString(value) && isHex(value)) { + try { + const bytes = hexToU8aFixed(value); + if (bytes.length > 0 && isAscii(bytes)) { + return
{u8aToString(bytes)}
; + } + } catch (_) { + // fall through to hex display + } + } + return
{value}
; }, }, diff --git a/src/components/Entries.tsx b/src/components/Entries.tsx index c76cad26..eb9691f1 100644 --- a/src/components/Entries.tsx +++ b/src/components/Entries.tsx @@ -54,7 +54,7 @@ function MemberStatus(props: { entry: Entry; pair: KeyringJson; isInProgress: bo const { entry, isInProgress } = props; const { address } = props.pair; const { approvals } = props.entry; - const approved = approvals.includes(address); + const approved = approvals?.includes(address) ?? false; const { networkConfig } = useApi(); const { constants } = useDataSourceTools(networkConfig); diff --git a/src/packages/react-components/src/InputFile.tsx b/src/packages/react-components/src/InputFile.tsx index cb5e55bf..4eb2e729 100644 --- a/src/packages/react-components/src/InputFile.tsx +++ b/src/packages/react-components/src/InputFile.tsx @@ -8,7 +8,8 @@ import React, { createRef, useCallback, useState } from 'react'; import Dropzone, { DropzoneRef } from 'react-dropzone'; import styled from 'styled-components'; -import { formatNumber, hexToU8a, isHex, u8aToString } from '@polkadot/util'; +import { formatNumber, isHex, u8aToString } from '@polkadot/util'; +import { hexToU8aFixed as hexToU8a } from 'src/utils/helper/hexToU8a'; import Labelled from './Labelled'; import { useTranslation } from './translate'; diff --git a/src/packages/react-components/src/util/toAddress.ts b/src/packages/react-components/src/util/toAddress.ts index 49692d12..ef328b89 100644 --- a/src/packages/react-components/src/util/toAddress.ts +++ b/src/packages/react-components/src/util/toAddress.ts @@ -3,7 +3,8 @@ // SPDX-License-Identifier: Apache-2.0 import { keyring } from '@polkadot/ui-keyring'; -import { assert, hexToU8a, isHex } from '@polkadot/util'; +import { assert, isHex } from '@polkadot/util'; +import { hexToU8aFixed as hexToU8a } from 'src/utils/helper/hexToU8a'; import { ethereumEncode } from '@polkadot/util-crypto'; // eslint-disable-next-line complexity diff --git a/src/packages/react-hooks/src/useAccounts.ts b/src/packages/react-hooks/src/useAccounts.ts index 673d981f..7621df45 100644 --- a/src/packages/react-hooks/src/useAccounts.ts +++ b/src/packages/react-hooks/src/useAccounts.ts @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import { keyring } from '@polkadot/ui-keyring'; -import { u8aToHex } from '@polkadot/util'; +import { u8aToHexFixed as u8aToHex } from 'src/utils/helper/u8aToHex'; import { decodeAddress } from '@polkadot/util-crypto'; import { useIsMountedRef } from './useIsMountedRef'; diff --git a/src/packages/react-hooks/src/useOwnStashInfos.ts b/src/packages/react-hooks/src/useOwnStashInfos.ts index 105aa036..ac786d24 100644 --- a/src/packages/react-hooks/src/useOwnStashInfos.ts +++ b/src/packages/react-hooks/src/useOwnStashInfos.ts @@ -6,7 +6,8 @@ import type { DeriveStakingAccount } from '@polkadot/api-derive/types'; import type { AccountId, ValidatorPrefs } from '@polkadot/types/interfaces'; import type { Codec, ITuple } from '@polkadot/types/types'; -import { u8aConcat, u8aToHex } from '@polkadot/util'; +import { u8aConcat } from '@polkadot/util'; +import { u8aToHexFixed as u8aToHex } from 'src/utils/helper/u8aToHex'; import { useEffect, useMemo, useState } from 'react'; import type { StakerState } from './types'; import { useAccounts } from './useAccounts'; diff --git a/src/packages/react-params/src/Param/BaseBytes.tsx b/src/packages/react-params/src/Param/BaseBytes.tsx index 3189f6b5..cb22b2f2 100644 --- a/src/packages/react-params/src/Param/BaseBytes.tsx +++ b/src/packages/react-params/src/Param/BaseBytes.tsx @@ -6,7 +6,9 @@ import type { TypeDef } from '@polkadot/types/types'; import React, { useCallback, useState } from 'react'; -import { compactAddLength, hexToU8a, isAscii, isHex, isU8a, stringToU8a, u8aToHex, u8aToString } from '@polkadot/util'; +import { compactAddLength, isAscii, isHex, stringToU8a, u8aToString, u8aToU8a } from '@polkadot/util'; +import { u8aToHexFixed as u8aToHex } from 'src/utils/helper/u8aToHex'; +import { hexToU8aFixed as hexToU8a } from 'src/utils/helper/hexToU8a'; import { decodeAddress } from '@polkadot/util-crypto'; import { CopyButton, Input } from '../../../react-components/src'; import type { RawParam, RawParamOnChange, RawParamOnEnter, RawParamOnEscape, Size } from '../types'; @@ -22,6 +24,7 @@ interface Props { isDisabled?: boolean; isError?: boolean; label?: React.ReactNode; + labelExtra?: React.ReactNode; length?: number; name?: string; onChange?: RawParamOnChange; @@ -35,27 +38,33 @@ interface Props { withLength?: boolean; } +interface Validity { + isAddress: boolean; + isValid: boolean; + lastValue?: Uint8Array; +} + const defaultValidate = (): boolean => true; -function convertInput(value: string): [boolean, Uint8Array] { +function convertInput(value: string): [boolean, boolean, Uint8Array] { if (value === '0x') { - return [true, new Uint8Array([])]; + return [true, false, new Uint8Array([])]; } else if (value.startsWith('0x')) { try { - return [true, hexToU8a(value)]; + return [true, false, isHex(value) ? hexToU8a(value) : stringToU8a(value)]; } catch (error) { - return [false, new Uint8Array([])]; + return [false, false, new Uint8Array([])]; } } // maybe it is an ss58? try { - return [true, decodeAddress(value)]; + return [true, true, decodeAddress(value)]; } catch (error) { // we continue } - return isAscii(value) ? [true, stringToU8a(value)] : [value === '0x', new Uint8Array([])]; + return isAscii(value) ? [true, false, stringToU8a(value)] : [value === '0x', false, new Uint8Array([])]; } function BaseBytes({ @@ -66,6 +75,7 @@ function BaseBytes({ isDisabled, isError, label, + labelExtra, length = -1, onChange, onEnter, @@ -77,27 +87,26 @@ function BaseBytes({ withLength, }: Props): React.ReactElement { const { t } = useTranslation(); - const [defaultValue] = useState( - value - ? isDisabled && isU8a(value) && isAscii(value) - ? u8aToString(value) - : isHex(value) - ? value - : // eslint-disable-next-line no-magic-numbers - u8aToHex(value as Uint8Array, isDisabled ? 256 : -1) - : undefined - ); - const [isValid, setIsValid] = useState(false); + const [defaultValue] = useState((): string | undefined => { + if (value) { + const u8a = u8aToU8a(value as Uint8Array); - const _onChange = useCallback( - (hex: string): void => { - let [beValid, val] = convertInput(hex); + return isAscii(u8a) ? u8aToString(u8a) : u8aToHex(u8a); + } - beValid = beValid && validate(val) && (length !== -1 ? val.length === length : val.length !== 0); + return undefined; + }); + const [{ isValid }, setValidity] = useState(() => ({ + isAddress: false, + isValid: isHex(defaultValue) || isAscii(defaultValue), + })); - if (withLength && beValid) { - val = compactAddLength(val); - } + const _onChange = useCallback( + (hex: string): void => { + const [convertedValid, isAddress, u8a] = convertInput(hex); + const beValid = + convertedValid && validate(u8a) && (length !== -1 ? u8a.length === length : u8a.length !== 0 || hex === '0x'); + const val = withLength && beValid ? compactAddLength(u8a) : u8a; // eslint-disable-next-line onChange && @@ -106,7 +115,7 @@ function BaseBytes({ value: asHex ? u8aToHex(val) : val, }); - setIsValid(beValid); + setValidity({ isAddress, isValid: beValid, lastValue: val }); }, [asHex, length, onChange, validate, withLength] ); @@ -115,11 +124,12 @@ function BaseBytes({ 9) { + HEX_U8[CHR.toUpperCase().charCodeAt(i)] = i; + } +} +for (let i = 0; i < 256; i++) { + const s = i << 8; + for (let j = 0; j < 256; j++) { + HEX_U16[s | j] = (HEX_U8[i] << 4) | HEX_U8[j]; + } +} + +export function hexToU8aFixed(value: string, bitLength = -1): Uint8Array { + let s = value.startsWith('0x') ? 2 : 0; + const decLength = Math.ceil((value.length - s) / 2); + const endLength = Math.ceil(bitLength === -1 ? decLength : bitLength / 8); + const result = new Uint8Array(endLength); + const offset = endLength > decLength ? endLength - decLength : 0; + + for (let i = offset; i < endLength; i++, s += 2) { + result[i] = HEX_U16[(value.charCodeAt(s) << 8) | value.charCodeAt(s + 1)]; + } + + return result; +} diff --git a/src/utils/helper/multisig.ts b/src/utils/helper/multisig.ts index e9f988ce..0d33a27d 100644 --- a/src/utils/helper/multisig.ts +++ b/src/utils/helper/multisig.ts @@ -2,10 +2,10 @@ import { ApiPromise } from '@polkadot/api'; import { Call } from '@polkadot/types/interfaces'; import keyring from '@polkadot/ui-keyring'; import { KeyringAddress } from '@polkadot/ui-keyring/types'; -import { u8aToHex } from '@polkadot/util'; import { createKeyMulti } from '@polkadot/util-crypto'; import store from 'store'; import { Network, ShareScope, WalletFormValue } from '../../model'; +import { u8aToHexFixed as u8aToHex } from './u8aToHex'; interface MultiInfo { isMultisig: boolean; diff --git a/src/utils/helper/u8aToHex.ts b/src/utils/helper/u8aToHex.ts new file mode 100644 index 00000000..9b7ad6a5 --- /dev/null +++ b/src/utils/helper/u8aToHex.ts @@ -0,0 +1,47 @@ +/* eslint-disable complexity */ +// Fixed u8aToHex: https://github.com/polkadot-js/common/blob/master/packages/util/src/u8a/toHex.ts +// The @polkadot/util@8.x version uses DataView which has alignment issues; +// the new version uses direct array indexing with pre-computed lookup tables. +/* eslint-disable no-bitwise */ +const U8: string[] = new Array(256); +const U16: string[] = new Array(256 * 256); + +for (let n = 0; n < 256; n++) { + U8[n] = n.toString(16).padStart(2, '0'); +} +for (let i = 0; i < 256; i++) { + const s = i << 8; + for (let j = 0; j < 256; j++) { + U16[s | j] = U8[i] + U8[j]; + } +} + +function hex(value: Uint8Array, result: string): string { + const mod = value.length % 2 | 0; + const length = (value.length - mod) | 0; + + for (let i = 0; i < length; i += 2) { + result += U16[(value[i] << 8) | value[i + 1]]; + } + if (mod) { + result += U8[value[length] | 0]; + } + + return result; +} + +export function u8aToHexFixed(value?: Uint8Array | null, bitLength = -1, isPrefixed = true): string { + const empty = isPrefixed ? '0x' : ''; + + if (!value || !value.length) { + return empty; + } else if (bitLength > 0) { + const length = Math.ceil(bitLength / 8); + + if (value.length > length) { + return `${hex(value.subarray(0, length / 2), empty)}\u2026${hex(value.subarray(value.length - length / 2), '')}`; + } + } + + return hex(value, empty); +} diff --git a/src/utils/helper/validate.ts b/src/utils/helper/validate.ts index 8cd3879f..ff3d2384 100644 --- a/src/utils/helper/validate.ts +++ b/src/utils/helper/validate.ts @@ -1,5 +1,6 @@ import { decodeAddress, encodeAddress } from '@polkadot/keyring'; -import { hexToU8a, isHex } from '@polkadot/util'; +import { isHex } from '@polkadot/util'; +import { hexToU8aFixed as hexToU8a } from './hexToU8a'; export const isSS58Address = (address: string) => { try {