// Copyright 2017-2026 @pezkuwi/react-components authors & contributors // SPDX-License-Identifier: Apache-2.0 import type { DropdownItemProps } from 'semantic-ui-react'; import type { KeyringOption$Type, KeyringOptions, KeyringSectionOption } from '@pezkuwi/ui-keyring/options/types'; import type { Option } from './types.js'; import React from 'react'; import store from 'store'; import { withMulti, withObservable } from '@pezkuwi/react-api/hoc'; import { keyring } from '@pezkuwi/ui-keyring'; import { createOptionItem } from '@pezkuwi/ui-keyring/options/item'; import { isNull, isUndefined } from '@pezkuwi/util'; import { isAddress } from '@pezkuwi/util-crypto'; import Dropdown from '../Dropdown.js'; import Static from '../Static.js'; import { styled } from '../styled.js'; import { getAddressName, toAddress } from '../util/index.js'; import createHeader from './createHeader.js'; import createItem from './createItem.js'; interface Props { className?: string; defaultValue?: Uint8Array | string | null; filter?: string[] | null; hideAddress?: boolean; isDisabled?: boolean; isError?: boolean; isInput?: boolean; isMultiple?: boolean; label?: React.ReactNode; labelExtra?: React.ReactNode; onChange?: (value: string | null) => void; onChangeMulti?: (value: string[]) => void; options?: KeyringSectionOption[] | null; optionsAll?: Record; placeholder?: string; type?: KeyringOption$Type; value?: string | Uint8Array | string[] | null; withEllipsis?: boolean; withExclude?: boolean; withLabel?: boolean; } type ExportedType = React.ComponentType & { createOption: (option: KeyringSectionOption, isUppercase?: boolean) => Option | null; setLastValue: (type: KeyringOption$Type, value: string) => void; }; interface State { lastValue?: string; value?: string | string[]; } const STORAGE_KEY = 'options:InputAddress'; const DEFAULT_TYPE = 'all'; const MULTI_DEFAULT: string[] = []; function transformToAddress (value?: string | Uint8Array | null): string | null { try { return toAddress(value, false, keyring.keyring.type === 'ethereum' ? 20 : 32) || null; } catch { // noop, handled by return } return null; } function transformToAccountId (value: string): string | null { if (!value) { return null; } const accountId = transformToAddress(value); return !accountId ? null : accountId; } function createOption (address: string): Option | null { let isRecent: boolean | undefined; const pair = keyring.getAccount(address); let name: string | undefined; if (pair) { name = pair.meta.name; } else { const addr = keyring.getAddress(address); if (addr) { name = addr.meta.name; isRecent = addr.meta.isRecent; } else { isRecent = true; } } return createItem(createOptionItem(address, name), !isRecent); } function readOptions (): Record> { return store.get(STORAGE_KEY) as Record> || { defaults: {} }; } function getLastValue (type: KeyringOption$Type = DEFAULT_TYPE): string { const options = readOptions(); return options.defaults[type]; } function setLastValue (type: KeyringOption$Type = DEFAULT_TYPE, value: string): void { const options = readOptions(); options.defaults[type] = value; store.set(STORAGE_KEY, options); } function dedupe (options: Option[]): Option[] { return options.reduce((all, o, index) => { const hasDupe = all.some(({ key }, eindex) => eindex !== index && key === o.key ); if (!hasDupe) { all.push(o); } return all; }, []); } class InputAddress extends React.PureComponent { public override state: State = {}; public static getDerivedStateFromProps ({ type, value }: Props, { lastValue }: State): Pick | null { try { return { lastValue: lastValue || getLastValue(type), value: Array.isArray(value) ? value.map((v) => toAddress(v)) : (toAddress(value) || undefined) }; } catch { return null; } } public override render (): React.ReactNode { const { className = '', defaultValue, hideAddress = false, isDisabled = false, isError, isMultiple, label, labelExtra, options, optionsAll, placeholder, type = DEFAULT_TYPE, withEllipsis, withLabel } = this.props; const hasOptions = (options && options.length !== 0) || (optionsAll && Object.keys(optionsAll[type]).length !== 0); // the options could be delayed, don't render without if (!hasOptions && !isDisabled && !['allPlus'].includes(type)) { // This is nasty, but since this things is non-functional, there is not much // we can do (well, wrap it, however that approach is deprecated here) return ( No accounts are available for selection. ); } const { lastValue, value } = this.state; const lastOption = this.getLastOptionValue(); const actualValue = transformToAddress( isDisabled || (defaultValue && defaultValue !== '0x' && (this.hasValue(defaultValue) || type === 'allPlus')) ? defaultValue : this.hasValue(lastValue) ? lastValue : lastOption?.value ); const actualOptions: Option[] = options ? dedupe( options .map((o) => createItem(o)) .filter((o): o is Option => !!o) ) : isDisabled && actualValue ? [createOption(actualValue)].filter((o): o is Option => !!o) : actualValue ? this.addActual(actualValue) : this.getFiltered(); const _defaultValue = (isMultiple || !isUndefined(value)) ? undefined : actualValue; return ( ); } private addActual (actualValue: string): Option[] { const base = this.getFiltered(); return this.hasValue(actualValue) ? base : base.concat(...[createOption(actualValue)].filter((o): o is Option => !!o)); } private renderLabel = ({ value }: KeyringSectionOption): React.ReactNode => { if (!value) { return undefined; } return getAddressName(value); }; private getLastOptionValue (): KeyringSectionOption | undefined { const available = this.getFiltered(); return available.length ? available[available.length - 1] : undefined; } private hasValue (test?: Uint8Array | string | null): boolean { const address = test?.toString(); return this.getFiltered().some(({ value }) => value === address); } private getFiltered (): Option[] { const { filter, optionsAll, type = DEFAULT_TYPE, withExclude = false } = this.props; return !optionsAll ? [] : dedupe(optionsAll[type]).filter(({ value }) => !filter || ( !!value && ( withExclude ? !filter.includes(value) : filter.includes(value) ) ) ); } private onChange = (address: string): void => { const { filter, onChange, type } = this.props; !filter && setLastValue(type, address); onChange && onChange( !!address && (this.hasValue(address) || (type === 'allPlus' && isAddress(address))) ? transformToAccountId(address) : null ); }; private onChangeMulti = (addresses: string[]): void => { const { onChangeMulti } = this.props; if (onChangeMulti) { onChangeMulti( addresses .map(transformToAccountId) .filter((address): address is string => !!address) ); } }; private onSearch = (filteredOptions: DropdownItemProps[], _query: string): DropdownItemProps[] => { const { isInput = true } = this.props; const query = _query.trim(); const queryLower = query.toLowerCase(); const matches = filteredOptions.filter((item): boolean => !!item.value && typeof item.name === 'string' && ( (item.name.toLowerCase?.().includes(queryLower)) || item.value.toString().toLowerCase().includes(queryLower) ) ); if (isInput && matches.length === 0) { const accountId = transformToAccountId(query); if (accountId) { const account = keyring.getAccount(accountId); if (account) { matches.push({ key: account.address, name: account.meta.name, value: account.address }); } else { const item = keyring.saveRecent( accountId.toString() ).option; matches.push({ key: item.key, name: item.name, value: item.value || undefined }); } } } // FIXME The return here is _very_ suspect, but it actually does exactly // what it is meant to do... filter and return the options for clicking // // (effectively it seems to be the value type that should allow undefined // instead of null in there...) return matches.filter((item, index): boolean => { const isLast = index === matches.length - 1; const nextItem = matches[index + 1]; const hasNext = nextItem?.value; return !(isNull(item.value) || isUndefined(item.value)) || (!isLast && !!hasNext); }); }; } const StyledDropdown = styled(Dropdown)` .ui.dropdown .text { width: 100%; } .ui.disabled.search { pointer-events: all; } .ui.search.selection.dropdown { > .text > .ui--KeyPair { .ui--IdentityIcon { left: -2.75rem; top: -1.05rem; > div, img, svg { height: 32px !important; width: 32px !important; } } .name { margin-left: 0; > .ui--AccountName { height: auto; } } } > .menu > div.item > .ui--KeyPair > .name > .ui--AccountName { height: auto; } } &.hideAddress .ui.search.selection.dropdown > .text > .ui--KeyPair .address { flex: 0; max-width: 0; } `; const ExportedComponent = withMulti( InputAddress, withObservable(keyring.keyringOption.optionsSubject, { propName: 'optionsAll', transform: (optionsAll: KeyringOptions): Record => Object.entries(optionsAll).reduce((result: Record, [type, options]): Record => { result[type] = options .map((option): Option | React.ReactNode | null => option.value === null ? createHeader(option) : createItem(option) ) .filter((o): o is Option | React.ReactNode => !!o); return result; }, {}) }) ) as ExportedType; ExportedComponent.createOption = createItem; ExportedComponent.setLastValue = setLastValue; export default ExportedComponent;