Skip to content

Commit

Permalink
Merge pull request #3916 from Koniverse/koni/dev/issue-3915
Browse files Browse the repository at this point in the history
[Issue-3915] Extension - Ledger - Support Avail Recovery app
  • Loading branch information
saltict authored Jan 2, 2025
2 parents 71e85db + 15c3560 commit ba8fa8b
Show file tree
Hide file tree
Showing 13 changed files with 264 additions and 39 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"@subwallet/react-ui": "5.1.2-b79",
"@subwallet/ui-keyring": "0.1.8-beta.0",
"@types/bn.js": "^5.1.6",
"@zondax/ledger-substrate": "0.44.2",
"@zondax/ledger-substrate": "1.0.1",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^29.3.1",
"browserify-sign": "^4.2.2",
Expand Down
3 changes: 3 additions & 0 deletions packages/extension-base/src/background/KoniTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,7 @@ export interface CreateHardwareAccountItem {
name: string;
isEthereum: boolean;
isGeneric: boolean;
isLedgerRecovery?: boolean;
}

export interface RequestAccountCreateHardwareV2 extends CreateHardwareAccountItem {
Expand Down Expand Up @@ -1264,6 +1265,8 @@ export interface LedgerNetwork {
isEthereum: boolean;
/** Hide networks that are supported by the dot migration app */
isHide?: boolean;
/** Recovery app */
isRecovery?: boolean;
/** Slip44 in the derivation path */
slip44: number;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class AccountLedgerHandler extends AccountBaseHandler {
const pairs: KeyringPair[] = [];

for (const account of accounts) {
const { accountIndex, address, addressOffset, genesisHash, hardwareType, isEthereum, isGeneric, name, originGenesisHash } = account;
const { accountIndex, address, addressOffset, genesisHash, hardwareType, isEthereum, isGeneric, isLedgerRecovery, name, originGenesisHash } = account;

const baseMeta: KeyringPair$Meta = {
name,
Expand All @@ -95,7 +95,8 @@ export class AccountLedgerHandler extends AccountBaseHandler {
addressOffset,
genesisHash,
originGenesisHash,
isGeneric
isGeneric,
isLedgerRecovery
};

const type = isEthereum ? 'ethereum' : 'sr25519';
Expand Down
2 changes: 2 additions & 0 deletions packages/extension-base/src/types/account/info/keyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export interface AccountLedgerData {
originGenesisHash?: string | null;
/** Ledger's availableGenesisHashes */
availableGenesisHashes?: string[];
/** Is Ledger recovery chain */
isLedgerRecovery?: boolean;
}

/**
Expand Down
20 changes: 13 additions & 7 deletions packages/extension-koni-ui/src/Popup/Account/ConnectLedger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import { LedgerNetwork, MigrationLedgerNetwork } from '@subwallet/extension-base/background/KoniTypes';
import { reformatAddress } from '@subwallet/extension-base/utils';
import { AccountItemWithName, AccountWithNameSkeleton, BasicOnChangeFunction, ChainSelector, DualLogo, InfoIcon, Layout, PageWrapper } from '@subwallet/extension-koni-ui/components';
import { LedgerChainSelector, LedgerItemType } from '@subwallet/extension-koni-ui/components/Field/LedgerChainSelector';
import { ATTACH_ACCOUNT_MODAL, SUBSTRATE_MIGRATION_KEY, USER_GUIDE_URL } from '@subwallet/extension-koni-ui/constants';
import { useAutoNavigateToCreatePassword, useCompleteCreateAccount, useGetSupportedLedger, useGoBackFromCreateAccount, useLedger } from '@subwallet/extension-koni-ui/hooks';
import { createAccountHardwareMultiple } from '@subwallet/extension-koni-ui/messaging';
import { RootState } from '@subwallet/extension-koni-ui/stores';
import { ChainItemType, ThemeProps } from '@subwallet/extension-koni-ui/types';
import { convertNetworkSlug } from '@subwallet/extension-koni-ui/utils';
import { BackgroundIcon, Button, Icon, Image, SwList } from '@subwallet/react-ui';
import CN from 'classnames';
import { CheckCircle, CircleNotch, Swatches } from 'phosphor-react';
Expand Down Expand Up @@ -54,11 +56,14 @@ const Component: React.FC<Props> = (props: Props) => {

const { accounts } = useSelector((state: RootState) => state.accountState);

const networks = useMemo((): ChainItemType[] => supportedLedger
const networks = useMemo((): LedgerItemType[] => supportedLedger
.filter(({ isHide }) => !isHide)
.map((network) => ({
name: !network.isGeneric ? network.networkName.replace(' network', '') : network.networkName,
slug: network.slug
name: !network.isGeneric
? network.networkName.replace(' network', '').concat(network.isRecovery ? ' Recovery' : '')
: network.networkName,
chain: network.slug,
slug: convertNetworkSlug(network)
})), [supportedLedger]);

const networkMigrates = useMemo((): ChainItemType[] => migrateSupportLedger
Expand All @@ -78,7 +83,7 @@ const Component: React.FC<Props> = (props: Props) => {
const [isSubmitting, setIsSubmitting] = useState(false);

const selectedChain = useMemo((): LedgerNetwork | undefined => {
return supportedLedger.find((n) => n.slug === chain);
return supportedLedger.find((n) => convertNetworkSlug(n) === chain);
}, [chain, supportedLedger]);

const selectedChainMigrateMode = useMemo((): MigrationLedgerNetwork | undefined => {
Expand All @@ -93,7 +98,7 @@ const Component: React.FC<Props> = (props: Props) => {
return chainMigrateMode && selectedChain ? `${selectedChain.accountName}` : '';
}, [chainMigrateMode, migrateSupportLedger]);

const { error, getAllAddress, isLoading, isLocked, ledger, refresh, warning } = useLedger(chain, true, false, false, selectedChainMigrateMode?.genesisHash);
const { error, getAllAddress, isLoading, isLocked, ledger, refresh, warning } = useLedger(selectedChain?.slug, true, false, false, selectedChainMigrateMode?.genesisHash, selectedChain?.isRecovery);

const onPreviousStep = useCallback(() => {
setFirstStep(true);
Expand Down Expand Up @@ -248,7 +253,8 @@ const Component: React.FC<Props> = (props: Props) => {
hardwareType: 'ledger',
name: item.name,
isEthereum: selectedChain.isEthereum,
isGeneric: selectedChain.isGeneric
isGeneric: selectedChain.isGeneric,
isLedgerRecovery: selectedChain?.isRecovery
}))
})
.then(() => {
Expand Down Expand Up @@ -326,7 +332,7 @@ const Component: React.FC<Props> = (props: Props) => {
sizeSquircleBorder={108}
/>
</div>
<ChainSelector
<LedgerChainSelector
className={'select-ledger-app'}
items={networks}
label={t('Select Ledger app')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ const Component: React.FC<Props> = (props: Props) => {
refresh: refreshLedger,
signMessage: ledgerSignMessage,
signTransaction: ledgerSignTransaction,
warning: ledgerWarning } = useLedger(chainSlug, activeLedger, true, forceUseMigrationApp, account?.originGenesisHash);
warning: ledgerWarning } = useLedger(chainSlug, activeLedger, true, forceUseMigrationApp, account?.originGenesisHash, account?.isLedgerRecovery);

const isLedgerConnected = useMemo(() => !isLocked && !isLedgerLoading && !!ledger, [isLedgerLoading, isLocked, ledger]);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright 2019-2022 @subwallet/extension-koni-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0

import { BasicInputWrapper } from '@subwallet/extension-koni-ui/components/Field/Base';
import { RECOVERY_SLUG } from '@subwallet/extension-koni-ui/constants';
import useTranslation from '@subwallet/extension-koni-ui/hooks/common/useTranslation';
import { useSelectModalInputHelper } from '@subwallet/extension-koni-ui/hooks/form/useSelectModalInputHelper';
import { ChainItemType, Theme, ThemeProps } from '@subwallet/extension-koni-ui/types';
import { Icon, InputRef, Logo, NetworkItem, SelectModal, Tooltip } from '@subwallet/react-ui';
import CN from 'classnames';
import { CheckCircle } from 'phosphor-react';
import React, { ForwardedRef, forwardRef, useCallback, useMemo } from 'react';
import styled, { useTheme } from 'styled-components';

import { GeneralEmptyList } from '../EmptyList';

export interface LedgerItemType extends ChainItemType {
chain: string;
}

interface Props extends ThemeProps, BasicInputWrapper {
items: LedgerItemType[];
loading?: boolean;
messageTooltip?: string;
}

const renderEmpty = () => <GeneralEmptyList />;

function Component (props: Props, ref: ForwardedRef<InputRef>): React.ReactElement<Props> {
const { className = '', disabled, id = 'address-input', items, label, loading, messageTooltip, placeholder, statusHelp, title, tooltip, value } = props;
const { t } = useTranslation();
const { token } = useTheme() as Theme;
const { onSelect } = useSelectModalInputHelper(props, ref);

const renderChainSelected = useCallback((item: LedgerItemType) => {
if (loading) {
return (
<div className={'__loading-text'}>{t('Loading ...')}</div>
);
}

return (
<div className={'__selected-item'}>{item.name}</div>
);
}, [loading, t]);

const searchFunction = useCallback((item: LedgerItemType, searchText: string) => {
const searchTextLowerCase = searchText.toLowerCase();

return (
item.name.toLowerCase().includes(searchTextLowerCase)
);
}, []);

const chainLogo = useMemo(() => {
const chain = value?.replaceAll(RECOVERY_SLUG, '') || '';

return (
<Logo
className='chain-logo'
network={chain}
shape='circle'
size={token.controlHeightSM}
/>
);
}, [token.controlHeightSM, value]);

const renderItem = useCallback((item: LedgerItemType, selected: boolean) => {
if (item.disabled && !!messageTooltip) {
return (
<Tooltip
placement='topRight'
title={t(messageTooltip)}
>
<div>
<NetworkItem
className={CN({ disabled: item.disabled })}
name={item.name}
networkKey={item.chain}
networkMainLogoShape='squircle'
networkMainLogoSize={28}
rightItem={selected && (<div className={'__check-icon'}>
<Icon
customSize={'20px'}
iconColor={token.colorSuccess}
phosphorIcon={CheckCircle}
type='phosphor'
weight='fill'
/>
</div>)}
/>
</div>
</Tooltip>
);
}

return (
<NetworkItem
className={CN({ disabled: item.disabled })}
name={item.name}
networkKey={item.chain}
networkMainLogoShape='squircle'
networkMainLogoSize={28}
rightItem={selected && (<div className={'__check-icon'}>
<Icon
customSize={'20px'}
iconColor={token.colorSuccess}
phosphorIcon={CheckCircle}
type='phosphor'
weight='fill'
/>
</div>)}
/>
);
}, [messageTooltip, t, token.colorSuccess]);

return (
<SelectModal
className={`${className} chain-selector-modal selector-${id}-modal`}
disabled={disabled}
id={id}
inputClassName={`${className} chain-selector-input`}
itemKey={'slug'}
items={items}
label={label}
loading={loading}
onSelect={onSelect}
placeholder={placeholder || t('Select chain')}
prefix={value !== '' && chainLogo}
renderItem={renderItem}
renderSelected={renderChainSelected}
renderWhenEmpty={renderEmpty}
searchFunction={searchFunction}
searchMinCharactersCount={2}
searchPlaceholder={t<string>('Network name')}
selected={value || ''}
statusHelp={statusHelp}
title={title || label || placeholder || t('Select network')}
tooltip={tooltip}
/>
);
}

export const LedgerChainSelector = styled(forwardRef(Component))<Props>(({ theme: { token } }: Props) => {
return ({
'&.ant-select-modal-input-container .ant-select-modal-input-wrapper': {
paddingLeft: 12,
paddingRight: 12
},

'&.chain-selector-input': {
'.__selected-item, .__loading-text': {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
},

'.__selected-item': {
color: token.colorText,
fontWeight: 500,
lineHeight: token.lineHeightHeading6
},

'.__loading-text': {
color: token.colorTextLight4
}
},

'.chain-logo': {
margin: '-1px 0'
},

'.ant-network-item .__check-icon': {
display: 'flex',
width: 40,
justifyContent: 'center'
},

'.ant-network-item.disabled': {
opacity: token.opacityDisable,
'.ant-network-item-content': {
cursor: 'not-allowed',

'&:hover': {
backgroundColor: token.colorBgSecondary
}
}
}
});
});
16 changes: 16 additions & 0 deletions packages/extension-koni-ui/src/constants/ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const SUBSTRATE_GENERIC_KEY = 'substrate_generic';
export const SUBSTRATE_MIGRATION_KEY = 'substrate_migration';
export const POLKADOT_KEY = 'polkadot';
export const POLKADOT_SLIP_44 = 354;
export const RECOVERY_SLUG = '_recovery';

export const PredefinedLedgerNetwork: LedgerNetwork[] = [
{
Expand Down Expand Up @@ -116,8 +117,23 @@ export const PredefinedLedgerNetwork: LedgerNetwork[] = [
isDevMode: false,
isGeneric: false,
isEthereum: false,
isRecovery: false,
slip44: 709
},
{
accountName: 'Avail Recovery',
appName: 'Avail Recovery',
networkName: 'Avail network',
genesisHash: ChainInfoMap.avail_mainnet.substrateInfo?.genesisHash || '0xb91746b45e0346cc2f815a520b9c6cb4d5c0902af848db0a80f85932d2e8276a',
icon: 'substrate',
network: 'availRecovery',
slug: ChainInfoMap.avail_mainnet.slug,
isDevMode: false,
isGeneric: false,
isEthereum: false,
isRecovery: true,
slip44: 354
},
{
accountName: 'Acala',
appName: 'Acala',
Expand Down
Loading

1 comment on commit ba8fa8b

@saltict
Copy link
Author

@saltict saltict commented on ba8fa8b Jan 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.