polkadot-js/apps

View on GitHub
packages/page-accounts/src/modals/Ledger.tsx

Summary

Maintainability
C
7 hrs
Test Coverage
// Copyright 2017-2024 @polkadot/app-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { ApiPromise } from '@polkadot/api';
import type { Ledger } from '@polkadot/hw-ledger';

import React, { useCallback, useRef, useState } from 'react';

import { Button, Dropdown, Input, MarkError, Modal } from '@polkadot/react-components';
import { useApi, useLedger } from '@polkadot/react-hooks';
import { keyring } from '@polkadot/ui-keyring';
import { arrayRange } from '@polkadot/util';

import { useTranslation } from '../translate.js';

interface Option {
  text: string;
  value: number;
}

interface Props {
  className?: string;
  onClose: () => void;
}

export const AVAIL_INDEXES = arrayRange(20);

// query the ledger for the address, adding it to the keyring
async function queryLedger (api: ApiPromise, getLedger: () => Ledger, name: string, accountOffset: number, addressOffset: number): Promise<void> {
  const { address } = await getLedger().getAddress(false, accountOffset, addressOffset);

  keyring.addHardware(address, 'ledger', {
    accountOffset,
    addressOffset,
    genesisHash: api.genesisHash.toHex(),
    name: name || `ledger ${accountOffset}/${addressOffset}`
  });
}

function LedgerModal ({ className, onClose }: Props): React.ReactElement<Props> {
  const { t } = useTranslation();
  const { api } = useApi();
  const { getLedger } = useLedger();
  const [accIndex, setAccIndex] = useState(0);
  const [addIndex, setAddIndex] = useState(0);
  const [error, setError] = useState<Error | null>(null);
  const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
  const [isBusy, setIsBusy] = useState(false);

  const accOps = useRef(AVAIL_INDEXES.map((value): Option => ({
    text: t('Account type {{index}}', { replace: { index: value } }),
    value
  })));

  const addOps = useRef(AVAIL_INDEXES.map((value): Option => ({
    text: t('Address index {{index}}', { replace: { index: value } }),
    value
  })));

  const _onChangeName = useCallback(
    (name: string) => setName({ isNameValid: !!name.trim(), name }),
    []
  );

  const _onSave = useCallback(
    (): void => {
      setError(null);
      setIsBusy(true);

      queryLedger(api, getLedger, name, accIndex, addIndex)
        .then(() => onClose())
        .catch((error: Error): void => {
          console.error(error);

          setIsBusy(false);
          setError(error);
        });
    },
    [accIndex, addIndex, api, getLedger, name, onClose]
  );

  return (
    <Modal
      className={className}
      header={t('Add account via Ledger')}
      onClose={onClose}
      size='large'
    >
      <Modal.Content>
        <Modal.Columns hint={t('The name for this account as it will appear under your accounts.')}>
          <Input
            autoFocus
            className='full'
            isError={!isNameValid}
            label={t('name')}
            onChange={_onChangeName}
            placeholder={t('account name')}
            value={name}
          />
        </Modal.Columns>
        <Modal.Columns hint={t('The account type that you wish to create. This is the top-level derivation.')}>
          <Dropdown
            label={t('account type')}
            onChange={setAccIndex}
            options={accOps.current}
            value={accIndex}
          />
        </Modal.Columns>
        <Modal.Columns hint={t('The address index on the account that you wish to add. This is the second-level derivation.')}>
          <Dropdown
            label={t('address index')}
            onChange={setAddIndex}
            options={addOps.current}
            value={addIndex}
          />
          {error && (
            <MarkError content={error.message} />
          )}
        </Modal.Columns>
      </Modal.Content>
      <Modal.Actions>
        <Button
          icon='plus'
          isBusy={isBusy}
          isDisabled={!isNameValid}
          label={t('Save')}
          onClick={_onSave}
        />
      </Modal.Actions>
    </Modal>
  );
}

export default React.memo(LedgerModal);