react-icons/fa#FaExclamationCircle TypeScript Examples

The following examples show how to use react-icons/fa#FaExclamationCircle. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: Toast.tsx    From convoychat with GNU General Public License v3.0 6 votes vote down vote up
toast = {
  error: (message: string) => {
    notify.addNotification({
      type: "warning",
      content: (
        <Toast type="warning" icon={FaExclamationCircle}>
          {message}
        </Toast>
      ),
      ...(defaultConfig as any),
    });
  },
  success: (message: string) => {
    notify.addNotification({
      type: "success",
      content: (
        <Toast type="success" icon={FaCheckCircle}>
          {message}
        </Toast>
      ),
      ...(defaultConfig as any),
    });
  },
  info: (message: string) => {
    notify.addNotification({
      type: "info",
      content: (
        <Toast type="info" icon={FaInfoCircle}>
          {message}
        </Toast>
      ),
      ...(defaultConfig as any),
    });
  },
}
Example #2
Source File: index.tsx    From interbtc-ui with Apache License 2.0 6 votes vote down vote up
CancelledIssueRequest = (): JSX.Element => {
  const { t } = useTranslation();

  return (
    <RequestWrapper className='px-12'>
      <h2 className={clsx('text-3xl', 'font-medium', 'text-interlayCinnabar')}>{t('cancelled')}</h2>
      <FaTimesCircle className={clsx('w-40', 'h-40', 'text-interlayCinnabar')} />
      <p
        className={clsx(
          { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
          { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA },
          'text-justify'
        )}
      >
        {t('issue_page.you_did_not_send', {
          wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL
        })}
      </p>
      {/* TODO: could componentize */}
      <div>
        <h6 className={clsx('flex', 'items-center', 'justify-center', 'space-x-0.5', 'text-interlayCinnabar')}>
          <span>{t('note')}</span>
          <FaExclamationCircle />
        </h6>
        <p
          className={clsx(
            'text-justify',
            { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
            { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
          )}
        >
          {t('issue_page.contact_team')}
        </p>
      </div>
    </RequestWrapper>
  );
}
Example #3
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
SubmittedRedeemRequestModal = ({
  open,
  onClose,
  request
}: CustomProps & Omit<ModalProps, 'children'>): JSX.Element => {
  const { t } = useTranslation();

  const { prices } = useSelector((state: StoreType) => state.general);

  const focusRef = React.useRef(null);

  return (
    <InterlayModal initialFocus={focusRef} open={open} onClose={onClose}>
      <InterlayModalInnerWrapper className={clsx('p-8', 'max-w-lg')}>
        <CloseIconButton ref={focusRef} onClick={onClose} />
        <div className={clsx('flex', 'flex-col', 'space-y-8')}>
          <h4 className={clsx('text-2xl', 'text-interlayCalifornia', 'font-medium', 'text-center')}>
            {t('redeem_page.withdraw')}
          </h4>
          <div className='space-y-6'>
            <div className='space-y-1'>
              <h5
                className={clsx(
                  'font-medium',
                  'text-interlayCalifornia',
                  'flex',
                  'items-center',
                  'justify-center',
                  'space-x-1'
                )}
              >
                <FaExclamationCircle className='inline' />
                <span>{t('redeem_page.redeem_processed')}</span>
              </h5>
              <h1 className={clsx('text-3xl', 'font-medium', 'space-x-1', 'text-center')}>
                <span>{t('redeem_page.will_receive_BTC')}</span>
                <span className='text-interlayCalifornia'>{displayMonetaryAmount(request.amountBTC)} BTC</span>
              </h1>
              <span
                className={clsx(
                  'block',
                  { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
                  { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA },
                  'text-2xl',
                  'text-center'
                )}
              >
                {`≈ $${getUsdAmount(request.amountBTC, prices.bitcoin?.usd)}`}
              </span>
            </div>
            <div>
              <label
                htmlFor={USER_BTC_ADDRESS}
                className={clsx(
                  { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
                  { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
                )}
              >
                {t('redeem_page.btc_destination_address')}
              </label>
              <span
                id={USER_BTC_ADDRESS}
                // TODO: could componentize
                className={clsx('block', 'p-2.5', 'border-2', 'font-medium', 'rounded-lg', 'text-center')}
              >
                {request.userBTCAddress}
              </span>
            </div>
            <div>
              <p>{t('redeem_page.we_will_inform_you_btc')}</p>
              <p
                className={clsx(
                  { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
                  { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
                )}
              >
                {t('redeem_page.typically_takes')}
              </p>
            </div>
          </div>
          <InterlayRouterLink
            to={{
              pathname: PAGES.TRANSACTIONS,
              search: queryString.stringify({
                [QUERY_PARAMETERS.REDEEM_REQUEST_ID]: request.id
              })
            }}
          >
            <InterlayDefaultContainedButton onClick={onClose} className='w-full'>
              {t('redeem_page.view_progress')}
            </InterlayDefaultContainedButton>
          </InterlayRouterLink>
        </div>
      </InterlayModalInnerWrapper>
    </InterlayModal>
  );
}
Example #4
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
BTCPaymentPendingStatusUI = ({ request }: Props): JSX.Element => {
  const { t } = useTranslation();
  const { prices } = useSelector((state: StoreType) => state.general);
  const { issuePeriod } = useSelector((state: StoreType) => state.issue);
  const amountBTCToSend = (request.wrappedAmount || request.request.amountWrapped).add(
    request.bridgeFee || request.request.bridgeFeeWrapped
  );
  const [initialLeftSeconds, setInitialLeftSeconds] = React.useState<number>();

  React.useEffect(() => {
    // TODO: double-check `request.request?.timestamp`
    // Date.now() is an approximation, used with the parachain response until we can get the block timestamp later
    const requestCreationTimestamp = request.request?.timestamp ?? Date.now();

    const requestTimestamp = Math.floor(new Date(requestCreationTimestamp).getTime() / 1000);
    const theInitialLeftSeconds = requestTimestamp + issuePeriod - Math.floor(Date.now() / 1000);
    setInitialLeftSeconds(theInitialLeftSeconds);
  }, [request.request, issuePeriod]);

  return (
    <div className='space-y-8'>
      <div className={clsx('flex', 'flex-col', 'justify-center', 'items-center')}>
        <div className='text-xl'>
          {t('send')}
          <span className='text-interlayCalifornia'>&nbsp;{displayMonetaryAmount(amountBTCToSend)}&nbsp;</span>
          BTC
        </div>
        <span
          className={clsx(
            { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
            { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA },
            'block'
          )}
        >
          {`≈ $ ${getUsdAmount(amountBTCToSend, prices.bitcoin?.usd)}`}
        </span>
      </div>
      <div>
        <p
          className={clsx(
            'text-center',
            { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
            { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
          )}
        >
          {t('issue_page.single_transaction')}
        </p>
        {/* TODO: should improve UX */}
        <InterlayTooltip label={t('click_to_copy')}>
          <span
            className={clsx('block', 'p-2.5', 'border-2', 'font-medium', 'rounded-lg', 'cursor-pointer', 'text-center')}
            onClick={() => copyToClipboard(request.vaultWrappedAddress || request.vaultBackingAddress)}
          >
            {request.vaultWrappedAddress || request.vaultBackingAddress}
          </span>
        </InterlayTooltip>
        {initialLeftSeconds && (
          <p className={clsx('flex', 'justify-center', 'items-center', 'space-x-1')}>
            <span
              className={clsx(
                { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
                { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA },
                'capitalize'
              )}
            >
              {t('issue_page.within')}
            </span>
            <Timer initialLeftSeconds={initialLeftSeconds} />
          </p>
        )}
      </div>
      <p className='space-x-1'>
        <span
          className={clsx(
            { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
            { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA },
            'break-all'
          )}
        >
          {t('issue_page.warning_mbtc_wallets')}
        </span>
        <span className='text-interlayCalifornia'>{displayMonetaryAmount(amountBTCToSend.mul(1000))}&nbsp;mBTC</span>
      </p>
      <QRCode
        includeMargin
        className='mx-auto'
        // eslint-disable-next-line max-len
        value={`bitcoin:${request.vaultWrappedAddress || request.vaultBackingAddress}?amount=${displayMonetaryAmount(
          amountBTCToSend
        )}`}
      />
      <div
        className={clsx(
          { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
          { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
        )}
      >
        <div className={clsx('inline-flex', 'items-center', 'space-x-0.5', 'mr-1')}>
          <span>{t('note')}</span>
          <FaExclamationCircle />
          <span>:</span>
        </div>
        <span>{t('issue_page.waiting_deposit')}</span>
      </div>
    </div>
  );
}
Example #5
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
RetriedRedeemRequest = ({ request }: Props): JSX.Element => {
  const { t } = useTranslation();
  const { bridgeLoaded, prices } = useSelector((state: StoreType) => state.general);
  const [punishmentCollateralTokenAmount, setPunishmentCollateralTokenAmount] = React.useState(
    newMonetaryAmount(0, COLLATERAL_TOKEN)
  );

  React.useEffect(() => {
    if (!bridgeLoaded) return;
    if (!request) return;

    // TODO: should add loading UX
    (async () => {
      try {
        const [punishmentFee, btcDotRate] = await Promise.all([
          window.bridge.vaults.getPunishmentFee(),
          window.bridge.oracle.getExchangeRate(COLLATERAL_TOKEN)
        ]);

        const btcAmount = request.request.requestedAmountBacking;
        const theBurnDOTAmount = btcDotRate.toCounter(btcAmount);
        const thePunishmentDOTAmount = theBurnDOTAmount.mul(new Big(punishmentFee));
        setPunishmentCollateralTokenAmount(thePunishmentDOTAmount);
      } catch (error) {
        // TODO: should add error handling UX
        console.log('[RetriedRedeemRequest useEffect] error.message => ', error.message);
      }
    })();
  }, [request, bridgeLoaded]);

  return (
    <RequestWrapper>
      <h2 className={clsx('text-3xl', 'font-medium', 'text-interlayConifer')}>
        {t('redeem_page.compensation_success')}
      </h2>
      <p className='w-full'>{t('redeem_page.compensation_notice')}</p>
      <p className='font-medium'>
        <PrimaryColorSpan>{t('redeem_page.recover_receive_dot')}</PrimaryColorSpan>
        <PrimaryColorSpan>
          &nbsp;{`${displayMonetaryAmount(punishmentCollateralTokenAmount)} ${COLLATERAL_TOKEN_SYMBOL}`}
        </PrimaryColorSpan>
        <span>&nbsp;({`≈ $${getUsdAmount(punishmentCollateralTokenAmount, prices.collateralToken?.usd)}`})</span>
        <PrimaryColorSpan>&nbsp;{t('redeem_page.recover_receive_total')}.</PrimaryColorSpan>
      </p>
      <div className='w-full'>
        <PriceInfo
          title={
            <h5
              className={clsx(
                { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
                { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
              )}
            >
              {t('redeem_page.compensation_payment')}
            </h5>
          }
          unitIcon={<CollateralTokenLogoIcon width={20} />}
          value={displayMonetaryAmount(punishmentCollateralTokenAmount)}
          unitName={COLLATERAL_TOKEN_SYMBOL}
          approxUSD={getUsdAmount(punishmentCollateralTokenAmount, prices.collateralToken?.usd)}
        />
        <Hr2 className={clsx('border-t-2', 'my-2.5')} />
        <PriceInfo
          className='w-full'
          title={
            <h5
              className={clsx(
                { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
                { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
              )}
            >
              {t('you_received')}
            </h5>
          }
          unitIcon={<CollateralTokenLogoIcon width={20} />}
          value={displayMonetaryAmount(punishmentCollateralTokenAmount)}
          unitName={COLLATERAL_TOKEN_SYMBOL}
          approxUSD={getUsdAmount(punishmentCollateralTokenAmount, prices.collateralToken?.usd)}
        />
      </div>
      <ExternalLink className='text-sm' href={getPolkadotLink(request.request.height.absolute)}>
        {t('issue_page.view_parachain_block')}
      </ExternalLink>
      <div className='w-full'>
        <h6 className={clsx('flex', 'items-center', 'justify-center', 'space-x-0.5', 'text-interlayCinnabar')}>
          <span>{t('note')}</span>
          <FaExclamationCircle />
        </h6>
        <p
          className={clsx(
            'text-justify',
            { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
            { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
          )}
        >
          {t('redeem_page.retry_new_redeem')}
        </p>
      </div>
    </RequestWrapper>
  );
}
Example #6
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
ReimburseStatusUI = ({ request, onClose }: Props): JSX.Element => {
  const { bridgeLoaded, prices } = useSelector((state: StoreType) => state.general);
  const [punishmentCollateralTokenAmount, setPunishmentCollateralTokenAmount] = React.useState(
    newMonetaryAmount(0, COLLATERAL_TOKEN)
  );
  const [collateralTokenAmount, setCollateralTokenAmount] = React.useState(newMonetaryAmount(0, COLLATERAL_TOKEN));
  const { t } = useTranslation();
  const handleError = useErrorHandler();

  React.useEffect(() => {
    if (!bridgeLoaded) return;
    if (!request) return;
    if (!handleError) return;

    // TODO: should add loading UX
    (async () => {
      try {
        const [punishment, btcDotRate] = await Promise.all([
          window.bridge.vaults.getPunishmentFee(),
          window.bridge.oracle.getExchangeRate(COLLATERAL_TOKEN)
        ]);
        const wrappedTokenAmount = request ? request.request.requestedAmountBacking : BitcoinAmount.zero;
        setCollateralTokenAmount(btcDotRate.toCounter(wrappedTokenAmount));
        setPunishmentCollateralTokenAmount(btcDotRate.toCounter(wrappedTokenAmount).mul(new Big(punishment)));
      } catch (error) {
        handleError(error);
      }
    })();
  }, [request, bridgeLoaded, handleError]);

  const queryClient = useQueryClient();
  // TODO: should type properly (`Relay`)
  const retryMutation = useMutation<void, Error, any>(
    (variables: any) => {
      return window.bridge.redeem.cancel(variables.id, false);
    },
    {
      onSuccess: () => {
        queryClient.invalidateQueries([REDEEM_FETCHER]);
        toast.success(t('redeem_page.successfully_cancelled_redeem'));
        onClose();
      },
      onError: (error) => {
        // TODO: should add error handling UX
        console.log('[useMutation] error => ', error);
      }
    }
  );
  // TODO: should type properly (`Relay`)
  const reimburseMutation = useMutation<void, Error, any>(
    (variables: any) => {
      return window.bridge.redeem.cancel(variables.id, true);
    },
    {
      onSuccess: () => {
        queryClient.invalidateQueries([REDEEM_FETCHER]);
        toast.success(t('redeem_page.successfully_cancelled_redeem'));
        onClose();
      },
      onError: (error) => {
        // TODO: should add error handling UX
        console.log('[useMutation] error => ', error);
      }
    }
  );

  const handleRetry = () => {
    if (!bridgeLoaded) {
      throw new Error('Bridge is not loaded!');
    }

    retryMutation.mutate(request);
  };

  const handleReimburse = () => {
    if (!bridgeLoaded) {
      throw new Error('Bridge is not loaded!');
    }

    reimburseMutation.mutate(request);
  };

  return (
    <RequestWrapper className='lg:px-12'>
      <div className='space-y-1'>
        <h2
          className={clsx(
            'text-lg',
            'font-medium',
            'text-interlayCalifornia',
            'flex',
            'justify-center',
            'items-center',
            'space-x-1'
          )}
        >
          <FaExclamationCircle />
          <span>{t('redeem_page.sorry_redeem_failed')}</span>
        </h2>
        <p
          className={clsx(
            { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
            { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA },
            'text-justify'
          )}
        >
          <span>{t('redeem_page.vault_did_not_send')}</span>
          <PrimaryColorSpan>
            &nbsp;{displayMonetaryAmount(punishmentCollateralTokenAmount)} {COLLATERAL_TOKEN_SYMBOL}
          </PrimaryColorSpan>
          <span>&nbsp;{`(≈ $ ${getUsdAmount(punishmentCollateralTokenAmount, prices.collateralToken?.usd)})`}</span>
          <span>
            &nbsp;
            {t('redeem_page.compensation', {
              collateralTokenSymbol: COLLATERAL_TOKEN_SYMBOL
            })}
          </span>
          .
        </p>
      </div>
      <div className='space-y-2'>
        <h5 className='font-medium'>
          {t('redeem_page.to_redeem_interbtc', {
            wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL
          })}
        </h5>
        <ul
          className={clsx(
            'space-y-3',
            'ml-6',
            { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
            { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
          )}
        >
          <li className='list-decimal'>
            <p className='text-justify'>
              <span>{t('redeem_page.receive_compensation')}</span>
              <PrimaryColorSpan>
                &nbsp;{displayMonetaryAmount(punishmentCollateralTokenAmount)} {COLLATERAL_TOKEN_SYMBOL}
              </PrimaryColorSpan>
              <span>
                &nbsp;
                {t('redeem_page.retry_with_another', {
                  compensationPrice: getUsdAmount(punishmentCollateralTokenAmount, prices.collateralToken?.usd)
                })}
              </span>
              .
            </p>
            <InterlayConiferOutlinedButton
              className='w-full'
              disabled={reimburseMutation.isLoading}
              pending={retryMutation.isLoading}
              onClick={handleRetry}
            >
              {t('retry')}
            </InterlayConiferOutlinedButton>
          </li>
          <li className='list-decimal'>
            <p className='text-justify'>
              <span>
                {t('redeem_page.burn_interbtc', {
                  wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL
                })}
              </span>
              <PrimaryColorSpan>
                &nbsp;{displayMonetaryAmount(collateralTokenAmount)} {COLLATERAL_TOKEN_SYMBOL}
              </PrimaryColorSpan>
              <span>
                &nbsp;
                {t('redeem_page.with_added', {
                  amountPrice: getUsdAmount(collateralTokenAmount, prices.collateralToken?.usd)
                })}
              </span>
              <PrimaryColorSpan>
                &nbsp;{displayMonetaryAmount(punishmentCollateralTokenAmount)} {COLLATERAL_TOKEN_SYMBOL}
              </PrimaryColorSpan>
              <span>
                &nbsp;
                {t('redeem_page.as_compensation_instead', {
                  compensationPrice: getUsdAmount(punishmentCollateralTokenAmount, prices.collateralToken?.usd)
                })}
              </span>
            </p>
            <InterlayDenimOrKintsugiMidnightOutlinedButton
              className='w-full'
              disabled={retryMutation.isLoading}
              pending={reimburseMutation.isLoading}
              onClick={handleReimburse}
            >
              {t('redeem_page.reimburse')}
            </InterlayDenimOrKintsugiMidnightOutlinedButton>
          </li>
        </ul>
      </div>
    </RequestWrapper>
  );
}
Example #7
Source File: PaymentView.tsx    From polkabtc-ui with Apache License 2.0 4 votes vote down vote up
PaymentView = ({
  request
}: Props): JSX.Element => {
  const { t } = useTranslation();
  const { prices } = useSelector((state: StoreType) => state.general);
  const { issuePeriod } = useSelector((state: StoreType) => state.issue);
  const amount = new Big(request.requestedAmountPolkaBTC).add(new Big(request.fee)).toString();
  const [initialLeftSeconds, setInitialLeftSeconds] = React.useState<number>();

  React.useEffect(() => {
    if (!request.timestamp) return;

    const requestTimestamp = Math.floor(new Date(Number(request.timestamp)).getTime() / 1000);
    const theInitialLeftSeconds = requestTimestamp + issuePeriod - Math.floor(Date.now() / 1000);
    setInitialLeftSeconds(theInitialLeftSeconds);
  }, [
    request.timestamp,
    issuePeriod
  ]);

  return (
    <div className='space-y-8'>
      <div
        className={clsx(
          'flex',
          'flex-col',
          'justify-center',
          'items-center'
        )}>
        <div
          className='text-xl'>
          {t('send')}
          <span className='text-interlayTreePoppy'>&nbsp;{request.amountBTC}&nbsp;</span>
          BTC
        </div>
        <span
          className={clsx(
            'text-textSecondary',
            'block'
          )}>
          {`≈ $ ${getUsdAmount(request.amountBTC, prices.bitcoin.usd)}`}
        </span>
      </div>
      <div>
        <p
          className={clsx(
            'text-center',
            'text-textSecondary'
          )}>
          {t('issue_page.single_transaction')}
        </p>
        {/* TODO: should improve the UX */}
        <Tooltip overlay={t('click_to_copy')}>
          <span
            className={clsx(
              'block',
              'p-2.5',
              'border-2',
              'font-medium',
              'rounded-lg',
              'cursor-pointer',
              'text-center'
            )}
            onClick={() => copyToClipboard(request.vaultBTCAddress)}>
            {request.vaultBTCAddress}
          </span>
        </Tooltip>
        <p
          className={clsx(
            'flex',
            'justify-center',
            'items-center',
            'space-x-1'
          )}>
          <span className='text-textSecondary'>{t('issue_page.within')}</span>
          {initialLeftSeconds && <Timer initialLeftSeconds={initialLeftSeconds} />}
        </p>
      </div>
      <p className='space-x-1'>
        <span
          className={clsx(
            'text-textSecondary',
            'break-all'
          )}>
          {t('issue_page.warning_mbtc_wallets')}
        </span>
        <span className='text-interlayTreePoppy'>
          {displayBtcAmount(new Big(request.amountBTC).mul(1000).toString())}&nbsp;mBTC
        </span>
      </p>
      <QRCode
        className='mx-auto'
        value={`bitcoin:${request.vaultBTCAddress}?amount=${amount}`} />
      <div
        className={clsx(
          'text-textSecondary'
        )}>
        <div
          className={clsx(
            'inline-flex',
            'items-center',
            'space-x-0.5',
            'mr-1'
          )}>
          <span>{t('note')}</span>
          <FaExclamationCircle />
          <span>:</span>
        </div>
        <span>{t('issue_page.waiting_deposit')}</span>
      </div>
    </div>
  );
}
Example #8
Source File: EnterAmountAndAddress.tsx    From polkabtc-ui with Apache License 2.0 4 votes vote down vote up
EnterAmountAndAddress = (): JSX.Element | null => {
  const dispatch = useDispatch();
  const { t } = useTranslation();

  const [status, setStatus] = React.useState(STATUSES.IDLE);
  const [error, setError] = React.useState<Error | null>(null);

  const usdPrice = useSelector((state: StoreType) => state.general.prices.bitcoin.usd);
  const {
    balancePolkaBTC,
    polkaBtcLoaded,
    address,
    bitcoinHeight,
    btcRelayHeight,
    prices,
    parachainStatus
  } = useSelector((state: StoreType) => state.general);
  const premiumRedeemSelected = useSelector((state: StoreType) => state.redeem.premiumRedeem);

  const {
    register,
    handleSubmit,
    formState: { errors },
    watch,
    setError: setFormError
  } = useForm<RedeemForm>({
    mode: 'onChange'
  });
  const polkaBTCAmount = watch(POLKA_BTC_AMOUNT);

  const [dustValue, setDustValue] = React.useState('0');
  const [redeemFee, setRedeemFee] = React.useState('0');
  const [redeemFeeRate, setRedeemFeeRate] = React.useState(new Big(0.005));
  const [btcToDotRate, setBtcToDotRate] = React.useState(new Big(0));
  const [premiumRedeemVaults, setPremiumRedeemVaults] = React.useState<Map<AccountId, Big>>(new Map());
  const [premiumRedeemFee, setPremiumRedeemFee] = React.useState(new Big(0));
  const [currentInclusionFee, setCurrentInclusionFee] = React.useState(new Big(0));

  const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE);
  const [submitError, setSubmitError] = React.useState<Error | null>(null);

  React.useEffect(() => {
    if (!polkaBtcLoaded) return;
    if (!polkaBTCAmount) return;
    if (!redeemFeeRate) return;

    const bigPolkaBTCAmount = new Big(polkaBTCAmount);
    const theRedeemFee = bigPolkaBTCAmount.mul(redeemFeeRate);
    setRedeemFee(theRedeemFee.toString());
  }, [
    polkaBtcLoaded,
    polkaBTCAmount,
    redeemFeeRate
  ]);

  React.useEffect(() => {
    if (!polkaBtcLoaded) return;

    (async () => {
      try {
        setStatus(STATUSES.PENDING);
        const [
          dustValueResult,
          premiumRedeemVaultsResult,
          premiumRedeemFeeResult,
          btcToDotRateResult,
          redeemFeeRateResult,
          currentInclusionFeeResult
        ] = await Promise.allSettled([
          window.polkaBTC.redeem.getDustValue(),
          window.polkaBTC.vaults.getPremiumRedeemVaults(),
          window.polkaBTC.redeem.getPremiumRedeemFee(),
          window.polkaBTC.oracle.getExchangeRate(),
          window.polkaBTC.redeem.getFeeRate(),
          window.polkaBTC.redeem.getCurrentInclusionFee()
        ]);

        if (dustValueResult.status === 'rejected') {
          throw new Error(dustValueResult.reason);
        }
        if (premiumRedeemFeeResult.status === 'rejected') {
          throw new Error(premiumRedeemFeeResult.reason);
        }
        if (btcToDotRateResult.status === 'rejected') {
          throw new Error(btcToDotRateResult.reason);
        }
        if (redeemFeeRateResult.status === 'rejected') {
          throw new Error(redeemFeeRateResult.reason);
        }
        if (currentInclusionFeeResult.status === 'rejected') {
          throw new Error(currentInclusionFeeResult.reason);
        }
        if (premiumRedeemVaultsResult.status === 'fulfilled') {
          setPremiumRedeemVaults(premiumRedeemVaultsResult.value);
        }

        setDustValue(dustValueResult.value.toString());
        setPremiumRedeemFee(new Big(premiumRedeemFeeResult.value));
        setBtcToDotRate(btcToDotRateResult.value);
        setRedeemFeeRate(redeemFeeRateResult.value);
        setCurrentInclusionFee(currentInclusionFeeResult.value);
        setStatus(STATUSES.RESOLVED);
      } catch (error) {
        setStatus(STATUSES.REJECTED);
        setError(error);
      }
    })();
  }, [polkaBtcLoaded]);

  if (status === STATUSES.REJECTED && error) {
    return (
      <ErrorHandler error={error} />
    );
  }

  if (status === STATUSES.IDLE || status === STATUSES.PENDING) {
    return (
      <div
        className={clsx(
          'flex',
          'justify-center'
        )}>
        <EllipsisLoader dotClassName='bg-interlayTreePoppy-400' />
      </div>
    );
  }

  const onSubmit = async (data: RedeemForm) => {
    try {
      setSubmitStatus(STATUSES.PENDING);
      const polkaBTCAmount = new Big(data[POLKA_BTC_AMOUNT]);

      // Differentiate between premium and regular redeem
      let vaultId;
      if (premiumRedeemSelected) {
        // Select a vault from the premium redeem vault list
        for (const [id, redeemableTokens] of premiumRedeemVaults) {
          if (redeemableTokens >= polkaBTCAmount) {
            vaultId = id;
            break;
          }
        }
        if (vaultId === undefined) {
          let maxAmount = new Big(0);
          for (const redeemableTokens of premiumRedeemVaults.values()) {
            if (maxAmount < redeemableTokens) {
              maxAmount = redeemableTokens;
            }
          }
          setFormError(POLKA_BTC_AMOUNT, {
            type: 'manual',
            message: t('redeem_page.error_max_premium_redeem', { maxPremiumRedeem: maxAmount.toString() })
          });

          return;
        }
      } else {
        const vaults = await window.polkaBTC.vaults.getVaultsWithRedeemableTokens();
        vaultId = getRandomVaultIdWithCapacity(Array.from(vaults || new Map()), polkaBTCAmount);
      }

      // FIXME: workaround to make premium redeem still possible
      const relevantVaults = new Map<AccountId, Big>();
      const id = window.polkaBTC.api.createType(ACCOUNT_ID_TYPE_NAME, vaultId);
      // FIXME: a bit of a dirty workaround with the capacity
      relevantVaults.set(id, polkaBTCAmount.mul(2));
      const result = await window.polkaBTC.redeem.request(polkaBTCAmount, data[BTC_ADDRESS], true, 0, relevantVaults);
      // TODO: handle redeem aggregator
      const redeemRequest = await parachainToUIRedeemRequest(result[0].id, result[0].redeemRequest);
      setSubmitStatus(STATUSES.RESOLVED);

      // Get the redeem id from the request redeem event
      const redeemId = stripHexPrefix(result[0].id.toString());
      dispatch(changeRedeemIdAction(redeemId));

      // Update the redeem status
      dispatch(updateBalancePolkaBTCAction(new Big(balancePolkaBTC).sub(new Big(data[POLKA_BTC_AMOUNT])).toString()));
      dispatch(addRedeemRequestAction(redeemRequest));
      dispatch(changeRedeemStepAction('REDEEM_INFO'));
    } catch (error) {
      setSubmitStatus(STATUSES.REJECTED);
      setSubmitError(error);
    }
  };

  const validatePolkaBTCAmount = (value: number): string | undefined => {
    const bigValue = new Big(value);
    const minValue = new Big(dustValue).add(currentInclusionFee).add(new Big(redeemFee));
    if (bigValue.gt(new Big(balancePolkaBTC))) {
      return `${t('redeem_page.current_balance')}${balancePolkaBTC}`;
    } else if (bigValue.lte(minValue)) {
      return `${t('redeem_page.amount_greater_dust_inclusion')}${minValue} BTC).`;
    }

    if (!address) {
      return t('redeem_page.must_select_account_warning');
    }

    if (!polkaBtcLoaded) {
      return 'PolkaBTC must be loaded!';
    }

    if (bitcoinHeight - btcRelayHeight > BLOCKS_BEHIND_LIMIT) {
      return t('issue_page.error_more_than_6_blocks_behind');
    }

    if (btcToSat(value.toString()) === undefined) {
      return 'Invalid PolkaBTC amount input!';
    }

    const polkaBTCAmountInteger = value.toString().split('.')[0];
    if (polkaBTCAmountInteger.length > BALANCE_MAX_INTEGER_LENGTH) {
      return 'Input value is too high!';
    }

    return undefined;
  };

  const handlePremiumRedeemToggle = () => {
    // TODO: should not use redux
    dispatch(togglePremiumRedeemAction(!premiumRedeemSelected));
  };

  const redeemFeeInBTC = displayBtcAmount(redeemFee);
  const redeemFeeInUSD = getUsdAmount(redeemFee, prices.bitcoin.usd);

  const totalBTC =
      polkaBTCAmount ?
        displayBtcAmount(new Big(polkaBTCAmount).sub(new Big(redeemFee)).sub(currentInclusionFee)) :
        '0';
  const totalBTCInUSD = getUsdAmount(totalBTC, prices.bitcoin.usd);

  const totalDOT =
    polkaBTCAmount ?
      new Big(polkaBTCAmount).mul(btcToDotRate).mul(premiumRedeemFee).toString() :
      '0';
  const totalDOTInUSD = getUsdAmount(totalDOT, prices.polkadot.usd);

  const bitcoinNetworkFeeInBTC = displayBtcAmount(currentInclusionFee);
  const bitcoinNetworkFeeInUSD = getUsdAmount(currentInclusionFee, prices.bitcoin.usd);

  if (status === STATUSES.RESOLVED) {
    return (
      <>
        <form
          className='space-y-8'
          onSubmit={handleSubmit(onSubmit)}>
          <h4
            className={clsx(
              'font-medium',
              'text-center',
              'text-interlayTreePoppy'
            )}>
            {t('redeem_page.you_will_receive')}
          </h4>
          <PolkaBTCField
            id='polka-btc-amount'
            name={POLKA_BTC_AMOUNT}
            type='number'
            label='PolkaBTC'
            step='any'
            placeholder='0.00'
            min={0}
            ref={register({
              required: {
                value: true,
                message: t('redeem_page.please_enter_amount')
              },
              validate: value => validatePolkaBTCAmount(value)
            })}
            approxUSD={`≈ $ ${getUsdAmount(polkaBTCAmount || '0', usdPrice)}`}
            error={!!errors[POLKA_BTC_AMOUNT]}
            helperText={errors[POLKA_BTC_AMOUNT]?.message} />
          <ParachainStatusInfo status={parachainStatus} />
          <TextField
            id='btc-address'
            name={BTC_ADDRESS}
            type='text'
            label='BTC Address'
            placeholder={t('enter_btc_address')}
            ref={register({
              required: {
                value: true,
                message: t('redeem_page.enter_btc')
              },
              pattern: {
                value: BTC_ADDRESS_REGEX, // TODO: regex need to depend on global mainnet | testnet parameter
                message: t('redeem_page.valid_btc_address')
              }
            })}
            error={!!errors[BTC_ADDRESS]}
            helperText={errors[BTC_ADDRESS]?.message} />
          {premiumRedeemVaults.size > 0 && (
            <div
              className={clsx(
                'flex',
                'justify-center',
                'items-center',
                'space-x-4'
              )}>
              <div
                className={clsx(
                  'flex',
                  'items-center',
                  'space-x-1'
                )}>
                <span>{t('redeem_page.premium_redeem')}</span>
                <Tooltip overlay={t('redeem_page.premium_redeem_info')}>
                  <FaExclamationCircle />
                </Tooltip>
              </div>
              <Toggle
                checked={premiumRedeemSelected}
                onChange={handlePremiumRedeemToggle} />
            </div>
          )}
          <PriceInfo
            title={
              <h5 className='text-textSecondary'>
                {t('bridge_fee')}
              </h5>
            }
            unitIcon={
              <BitcoinLogoIcon
                width={23}
                height={23} />
            }
            value={redeemFeeInBTC}
            unitName='BTC'
            approxUSD={redeemFeeInUSD} />
          <PriceInfo
            title={
              <h5 className='text-textSecondary'>
                {t('bitcoin_network_fee')}
              </h5>
            }
            unitIcon={
              <BitcoinLogoIcon
                width={23}
                height={23} />
            }
            value={bitcoinNetworkFeeInBTC}
            unitName='BTC'
            approxUSD={bitcoinNetworkFeeInUSD} />
          <hr
            className={clsx(
              'border-t-2',
              'my-2.5',
              'border-textSecondary'
            )} />
          <PriceInfo
            title={
              <h5 className='text-textPrimary'>
                {t('you_will_receive')}
              </h5>
            }
            unitIcon={
              <BitcoinLogoIcon
                width={23}
                height={23} />
            }
            value={totalBTC}
            unitName='BTC'
            approxUSD={totalBTCInUSD} />
          {premiumRedeemSelected && (
            <PriceInfo
              title={
                <h5 className='text-interlayMalachite'>
                  {t('redeem_page.earned_premium')}
                </h5>
              }
              unitIcon={
                <PolkadotLogoIcon
                  width={23}
                  height={23} />
              }
              value={totalDOT}
              unitName='DOT'
              approxUSD={totalDOTInUSD} />
          )}
          <InterlayButton
            type='submit'
            style={{ display: 'flex' }}
            className='mx-auto'
            variant='contained'
            color='primary'
            disabled={parachainStatus !== ParachainStatus.Running}
            pending={submitStatus === STATUSES.PENDING}>
            {t('confirm')}
          </InterlayButton>
        </form>
        {(submitStatus === STATUSES.REJECTED && submitError) && (
          <ErrorModal
            open={!!submitError}
            onClose={() => {
              setSubmitStatus(STATUSES.IDLE);
              setSubmitError(null);
            }}
            title='Error'
            description={
              typeof submitError === 'string' ?
                submitError :
                submitError.message
            } />
        )}
      </>
    );
  }

  return null;
}