polkadot-js/apps

View on GitHub
packages/page-accounts/src/Vanity/index.tsx

Summary

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

import type { ActionStatus } from '@polkadot/react-components/Status/types';
import type { KeypairType } from '@polkadot/util-crypto/types';
import type { GeneratorMatch, GeneratorMatches, GeneratorResult } from '@polkadot/vanitygen/types';

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

import { Button, Dropdown, Input, styled, Table } from '@polkadot/react-components';
import { useApi, useIsMountedRef } from '@polkadot/react-hooks';
import { settings } from '@polkadot/ui-settings';
import { nextTick } from '@polkadot/util';
import generator from '@polkadot/vanitygen/generator';
import matchRegex from '@polkadot/vanitygen/regex';
import generatorSort from '@polkadot/vanitygen/sort';

import CreateModal from '../modals/Create.js';
import { useTranslation } from '../translate.js';
import Match from './Match.js';

interface Props {
  className?: string;
  onStatusChange: (status: ActionStatus) => void;
}

interface MatchState {
  isMatchValid: boolean;
  match: string;
}

interface Results {
  elapsed: number;
  isRunning: boolean;
  keyCount: number;
  keyTime: number;
  matches: GeneratorMatches;
  startAt: number;
}

const DEFAULT_MATCH = 'Some';
const BOOL_OPTIONS = [
  { text: 'No', value: false },
  { text: 'Yes', value: true }
];

function VanityApp ({ className = '', onStatusChange }: Props): React.ReactElement<Props> {
  const { t } = useTranslation();
  const { api, isEthereum } = useApi();
  const results = useRef<GeneratorResult[]>([]);
  const runningRef = useRef(false);
  const mountedRef = useIsMountedRef();
  const [createSeed, setCreateSeed] = useState<string | null>(null);
  const [{ elapsed, isRunning, keyCount, matches }, setResults] = useState<Results>({
    elapsed: 0,
    isRunning: false,
    keyCount: -1,
    keyTime: 0,
    matches: [],
    startAt: 0
  });
  const [{ isMatchValid, match }, setMatch] = useState<MatchState>({ isMatchValid: true, match: DEFAULT_MATCH });
  const [type, setType] = useState<KeypairType>('ed25519');
  const [withCase, setWithCase] = useState(true);

  const _clearSeed = useCallback(
    () => setCreateSeed(null),
    []
  );

  const _checkMatches = useCallback(
    (): void => {
      const checks = results.current;

      results.current = [];

      if (checks.length === 0 || !mountedRef.current) {
        return;
      }

      setResults(
        ({ isRunning, keyCount, keyTime, matches, startAt }: Results): Results => {
          let newKeyCount = keyCount;
          let newKeyTime = keyTime;
          const newMatches = checks.reduce((result, { elapsed, found }): GeneratorMatch[] => {
            newKeyCount += found.length;
            newKeyTime += elapsed;

            return result.concat(found);
          }, matches);

          return {
            elapsed: Date.now() - startAt,
            isRunning,
            keyCount: newKeyCount,
            keyTime: newKeyTime,
            matches: newMatches.sort(generatorSort).slice(0, 25),
            startAt
          };
        }
      );
    },
    [mountedRef]
  );

  const _executeGeneration = useCallback(
    (): void => {
      if (!runningRef.current) {
        return _checkMatches();
      }

      nextTick((): void => {
        if (mountedRef.current) {
          if (results.current.length === 25) {
            _checkMatches();
          }

          results.current.push(
            generator({ match, runs: 10, ss58Format: api.registry.chainSS58 || 0, type, withCase, withHex: true })
          );

          _executeGeneration();
        }
      });
    },
    [_checkMatches, api, match, mountedRef, runningRef, type, withCase]
  );

  const _onChangeMatch = useCallback(
    (match: string): void => setMatch({
      isMatchValid:
        matchRegex.test(match) &&
        (match.length !== 0) &&
        (match.length < 31),
      match
    }),
    []
  );

  const _onRemove = useCallback(
    (address: string): void => setResults(
      (results: Results): Results => ({
        ...results,
        matches: results.matches.filter((item) => item.address !== address)
      })
    ),
    []
  );

  const _toggleStart = useCallback(
    (): void => setResults(
      ({ elapsed, isRunning, keyCount, keyTime, matches, startAt }: Results): Results => ({
        elapsed,
        isRunning: !isRunning,
        keyCount: isRunning ? keyCount : 0,
        keyTime: isRunning ? keyTime : 0,
        matches,
        startAt: isRunning ? startAt : Date.now()
      })
    ),
    []
  );

  useEffect((): void => {
    runningRef.current = isRunning;

    if (isRunning) {
      _executeGeneration();
    }
  }, [_executeGeneration, isRunning]);

  const header = useMemo<[React.ReactNode?, string?, number?][]>(
    () => [
      [t('matches'), 'start', 2],
      [t('Evaluated {{count}} keys in {{elapsed}}s ({{avg}} keys/s)', {
        replace: {
          avg: (keyCount / (elapsed / 1000)).toFixed(3),
          count: keyCount,
          elapsed: (elapsed / 1000).toFixed(2)
        }
      }), 'start --digits'],
      [t('secret'), 'start'],
      []
    ],
    [elapsed, keyCount, t]
  );

  return (
    <StyledDiv className={className}>
      <div className='ui--row'>
        <Input
          autoFocus
          className='medium'
          isDisabled={isRunning}
          isError={!isMatchValid}
          label={t('Search for')}
          onChange={_onChangeMatch}
          onEnter={_toggleStart}
          value={match}
        />
        <Dropdown
          className='medium'
          isDisabled={isRunning}
          label={t('case sensitive')}
          onChange={setWithCase}
          options={BOOL_OPTIONS}
          value={withCase}
        />
      </div>
      <div className='ui--row'>
        <Dropdown
          className='medium'
          defaultValue={type}
          label={t('keypair crypto type')}
          onChange={setType}
          options={isEthereum ? settings.availableCryptosEth : settings.availableCryptos}
        />
      </div>
      <Button.Group>
        <Button
          icon={
            isRunning
              ? 'stop'
              : 'sign-in-alt'
          }
          isDisabled={!isMatchValid}
          label={
            isRunning
              ? t('Stop generation')
              : t('Start generation')
          }
          onClick={_toggleStart}
        />
      </Button.Group>
      {matches.length !== 0 && (
        <>
          <article className='warning centered'>{t('Ensure that you utilized the "Save" functionality before using a generated address to receive funds. Without saving the address and the associated seed any funds sent to it will be lost.')}</article>
          <Table
            className='vanity--App-matches'
            empty={t('No matches found')}
            header={header}
          >
            {matches.map((match): React.ReactNode => (
              <Match
                {...match}
                key={match.address}
                onCreateToggle={setCreateSeed}
                onRemove={_onRemove}
              />
            ))}
          </Table>
        </>
      )}
      {createSeed && (
        <CreateModal
          onClose={_clearSeed}
          onStatusChange={onStatusChange}
          seed={createSeed}
          type={type}
        />
      )}
    </StyledDiv>
  );
}

const StyledDiv = styled.div`
  .vanity--App-matches {
    overflow-x: auto;
    padding: 1em 0;
  }

  .vanity--App-stats {
    padding: 1em 0 0 0;
    opacity: 0.45;
    text-align: center;
  }
`;

export default React.memo(VanityApp);