react-error-boundary#useErrorHandler TypeScript Examples

The following examples show how to use react-error-boundary#useErrorHandler. 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: index.tsx    From interbtc-ui with Apache License 2.0 6 votes vote down vote up
TotalsUI = (): JSX.Element => {
  const { bridgeLoaded } = useSelector((state: StoreType) => state.general);

  const {
    isIdle: totalVotingSupplyIdle,
    isLoading: totalVotingSupplyLoading,
    data: totalVotingSupply,
    error: totalVotingSupplyError
  } = useQuery<VoteGovernanceTokenMonetaryAmount, Error>(
    [GENERIC_FETCHER, 'escrow', 'totalVotingSupply'],
    genericFetcher<VoteGovernanceTokenMonetaryAmount>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(totalVotingSupplyError);

  let totalStakedVoteGovernanceTokenAmountLabel;
  if (totalVotingSupplyIdle || totalVotingSupplyLoading) {
    totalStakedVoteGovernanceTokenAmountLabel = '-';
  } else {
    if (totalVotingSupply === undefined) {
      throw new Error('Something went wrong!');
    }
    totalStakedVoteGovernanceTokenAmountLabel = displayMonetaryAmount(totalVotingSupply);
  }

  return (
    <div>
      <InformationUI
        label={`Total ${VOTE_GOVERNANCE_TOKEN_SYMBOL}`}
        value={`${totalStakedVoteGovernanceTokenAmountLabel} ${VOTE_GOVERNANCE_TOKEN_SYMBOL}`}
      />
    </div>
  );
}
Example #2
Source File: index.tsx    From interbtc-ui with Apache License 2.0 5 votes vote down vote up
ActiveVaultsCard = ({ hasLinks }: Props): JSX.Element => {
  const { t } = useTranslation();

  const { isIdle: vaultsIdle, isLoading: vaultsLoading, data: vaults, error: vaultsError } = useQuery<
    GraphqlReturn<Array<VaultRegistration>>,
    Error
  >(
    [
      GRAPHQL_FETCHER,
      `query {
        vaults(orderBy: registrationTimestamp_ASC) {
          id
          registrationTimestamp
        }
      }`
    ],
    graphqlFetcher<Array<VaultRegistration>>()
  );
  useErrorHandler(vaultsError);

  const renderContent = () => {
    // TODO: should use skeleton loaders
    if (vaultsIdle || vaultsLoading) {
      return <>Loading...</>;
    }
    if (vaults === undefined) {
      throw new Error('Something went wrong!');
    }

    const vaultRegistrations = vaults.data.vaults;
    const graphData = graphTimestamps
      .slice(1)
      .map(
        (timestamp) =>
          vaultRegistrations.filter((registration) => new Date(registration.registrationTimestamp) <= timestamp).length
      );

    let chartLineColor;
    if (process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT) {
      chartLineColor = INTERLAY_DENIM[500];
      // MEMO: should check dark mode as well
    } else if (process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA) {
      chartLineColor = KINTSUGI_SUNDOWN[500];
    } else {
      throw new Error('Something went wrong!');
    }

    return (
      <>
        <Stats
          leftPart={
            <>
              <StatsDt>{t('dashboard.vault.active_vaults')}</StatsDt>
              <StatsDd>{vaultRegistrations.length}</StatsDd>
            </>
          }
          rightPart={<>{hasLinks && <StatsRouterLink to={PAGES.DASHBOARD_VAULTS}>View vaults</StatsRouterLink>}</>}
        />
        <LineChart
          wrapperClassName='h-full'
          colors={[chartLineColor]}
          labels={[t('dashboard.vault.total_vaults_chart')]}
          yLabels={graphTimestamps.slice(0, -1).map((date) => new Date(date).toISOString().substring(0, 10))}
          yAxes={[
            {
              ticks: {
                beginAtZero: true,
                precision: 0
              }
            }
          ]}
          datasets={[graphData]}
        />
      </>
    );
  };

  return <DashboardCard>{renderContent()}</DashboardCard>;
}
Example #3
Source File: index.tsx    From interbtc-ui with Apache License 2.0 5 votes vote down vote up
CollateralLockedCard = ({ hasLinks }: Props): JSX.Element => {
  const { prices } = useSelector((state: StoreType) => state.general);
  const { t } = useTranslation();

  const {
    isIdle: cumulativeCollateralPerDayIdle,
    isLoading: cumulativeCollateralPerDayLoading,
    data: cumulativeCollateralPerDay,
    error: cumulativeCollateralPerDayError
    // TODO: should type properly (`Relay`)
  } = useQuery<VolumeDataPoint<CollateralUnit>[], Error>(
    [
      CUMULATIVE_VOLUMES_FETCHER,
      'Collateral' as VolumeType,
      cutoffTimestamps,
      COLLATERAL_TOKEN, // returned amounts
      COLLATERAL_TOKEN, // filter by this collateral...
      WRAPPED_TOKEN //     and this backing currency
    ],
    cumulativeVolumesFetcher
  );
  useErrorHandler(cumulativeCollateralPerDayError);

  const renderContent = () => {
    // TODO: should use skeleton loaders
    if (cumulativeCollateralPerDayIdle || cumulativeCollateralPerDayLoading) {
      return <>Loading...</>;
    }
    if (cumulativeCollateralPerDay === undefined) {
      throw new Error('Something went wrong!');
    }
    const totalLockedCollateralTokenAmount = cumulativeCollateralPerDay.slice(-1)[0].amount;

    let chartLineColor;
    if (process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT) {
      chartLineColor = INTERLAY_DENIM[500];
      // MEMO: should check dark mode as well
    } else if (process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA) {
      chartLineColor = KINTSUGI_SUPERNOVA[500];
    } else {
      throw new Error('Something went wrong!');
    }

    return (
      <>
        <Stats
          leftPart={
            <>
              <StatsDt>{t('dashboard.vault.locked_collateral')}</StatsDt>
              <StatsDd>
                {displayMonetaryAmount(totalLockedCollateralTokenAmount)} {COLLATERAL_TOKEN_SYMBOL}
              </StatsDd>
              <StatsDd>${getUsdAmount(totalLockedCollateralTokenAmount, prices.collateralToken?.usd)}</StatsDd>
            </>
          }
          rightPart={<>{hasLinks && <StatsRouterLink to={PAGES.DASHBOARD_VAULTS}>View vaults</StatsRouterLink>}</>}
        />
        <LineChart
          wrapperClassName='h-full'
          colors={[chartLineColor]}
          labels={[t('dashboard.vault.total_collateral_locked')]}
          yLabels={cumulativeCollateralPerDay
            .slice(0, -1)
            .map((dataPoint) => dataPoint.tillTimestamp.toISOString().substring(0, 10))}
          yAxes={[
            {
              ticks: {
                beginAtZero: true,
                precision: 0
              }
            }
          ]}
          datasets={[cumulativeCollateralPerDay.slice(1).map((dataPoint) => displayMonetaryAmount(dataPoint.amount))]}
        />
      </>
    );
  };

  return <DashboardCard>{renderContent()}</DashboardCard>;
}
Example #4
Source File: index.tsx    From interbtc-ui with Apache License 2.0 5 votes vote down vote up
RedeemedChart = (): JSX.Element => {
  const { t } = useTranslation();

  const {
    isIdle: cumulativeRedeemsPerDayIdle,
    isLoading: cumulativeRedeemsPerDayLoading,
    data: cumulativeRedeemsPerDay,
    error: cumulativeRedeemsPerDayError
    // TODO: should type properly (`Relay`)
  } = useQuery<VolumeDataPoint<BitcoinUnit>[], Error>(
    [CUMULATIVE_VOLUMES_FETCHER, 'Redeemed' as VolumeType, cutoffTimestamps, WRAPPED_TOKEN],
    cumulativeVolumesFetcher
  );
  useErrorHandler(cumulativeRedeemsPerDayError);

  if (cumulativeRedeemsPerDayIdle || cumulativeRedeemsPerDayLoading) {
    return <>Loading...</>;
  }
  if (cumulativeRedeemsPerDay === undefined) {
    throw new Error('Something went wrong!');
  }

  const pointRedeemsPerDay = cumulativeRedeemsPerDay
    .map((dataPoint, i) => {
      if (i === 0) {
        return newMonetaryAmount(0, WRAPPED_TOKEN);
      } else {
        return dataPoint.amount.sub(cumulativeRedeemsPerDay[i - 1].amount);
      }
    })
    .slice(1); // cut off first 0 value

  let firstChartLineColor;
  let secondChartLineColor;
  if (process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT) {
    firstChartLineColor = INTERLAY_DENIM[500];
    secondChartLineColor = INTERLAY_MULBERRY[500];
    // MEMO: should check dark mode as well
  } else if (process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA) {
    firstChartLineColor = KINTSUGI_MIDNIGHT[200];
    secondChartLineColor = KINTSUGI_PRAIRIE_SAND[400];
  } else {
    throw new Error('Something went wrong!');
  }

  return (
    <LineChart
      wrapperClassName='h-full'
      colors={[firstChartLineColor, secondChartLineColor]}
      labels={[t('dashboard.redeem.total_redeemed_chart'), t('dashboard.redeem.per_day_redeemed_chart')]}
      yLabels={cutoffTimestamps.slice(0, -1).map((timestamp) => timestamp.toLocaleDateString())}
      yAxes={[
        {
          position: 'left',
          ticks: {
            beginAtZero: true,
            maxTicksLimit: 6
          }
        },
        {
          position: 'right',
          ticks: {
            beginAtZero: true,
            maxTicksLimit: 6
          }
        }
      ]}
      datasets={[
        cumulativeRedeemsPerDay.slice(1).map((dataPoint) => dataPoint.amount.str.BTC()),
        pointRedeemsPerDay.map((amount) => amount.str.BTC())
      ]}
    />
  );
}
Example #5
Source File: index.tsx    From interbtc-ui with Apache License 2.0 5 votes vote down vote up
BlockstreamCard = (): JSX.Element => {
  const { t } = useTranslation();
  const { bridgeLoaded, bitcoinHeight } = useSelector((state: StoreType) => state.general);

  const {
    isIdle: blockstreamTipIdle,
    isLoading: blockstreamTipLoading,
    data: blockstreamTip,
    error: blockstreamTipError
  } = useQuery<string, Error>([GENERIC_FETCHER, 'electrsAPI', 'getLatestBlock'], genericFetcher<string>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(blockstreamTipError);

  const renderContent = () => {
    // TODO: should use skeleton loaders
    if (blockstreamTipIdle || blockstreamTipLoading) {
      return <>Loading...</>;
    }

    return (
      <>
        <Stats
          rightPart={
            <>
              <ExternalLink
                href={`${BTC_EXPLORER_BLOCK_API}${blockstreamTip}`}
                className={clsx('text-sm', 'font-medium')}
              >
                {t('dashboard.relay.blockstream_verify_link')}
              </ExternalLink>
            </>
          }
        />
        <Ring64
          className={clsx(
            'mx-auto',
            { 'ring-interlayDenim': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
            { 'dark:ring-kintsugiSupernova': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
          )}
        >
          <Ring64Title
            className={clsx(
              { 'text-interlayDenim': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
              { 'dark:text-kintsugiSupernova': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
            )}
          >
            {t('blockstream')}
          </Ring64Title>
          <Ring64Value>{t('dashboard.relay.block_number', { number: bitcoinHeight })}</Ring64Value>
        </Ring64>
      </>
    );
  };

  return <DashboardCard>{renderContent()}</DashboardCard>;
}
Example #6
Source File: index.tsx    From interbtc-ui with Apache License 2.0 5 votes vote down vote up
UpperContent = (): JSX.Element => {
  const { totalWrappedTokenAmount, prices } = useSelector((state: StoreType) => state.general);
  const { t } = useTranslation();

  const {
    isIdle: totalSuccessfulIssuesIdle,
    isLoading: totalSuccessfulIssuesLoading,
    data: totalSuccessfulIssues,
    error: totalSuccessfulIssuesError
    // TODO: should type properly (`Relay`)
  } = useQuery<GraphqlReturn<any>, Error>(
    [GRAPHQL_FETCHER, issueCountQuery('status_eq: Completed')],
    graphqlFetcher<GraphqlReturn<any>>()
  );
  useErrorHandler(totalSuccessfulIssuesError);

  // TODO: should use skeleton loaders
  if (totalSuccessfulIssuesIdle || totalSuccessfulIssuesLoading) {
    return <>Loading...</>;
  }
  if (totalSuccessfulIssues === undefined) {
    throw new Error('Something went wrong!');
  }
  const totalSuccessfulIssueCount = totalSuccessfulIssues.data.issuesConnection.totalCount;

  return (
    <Panel className={clsx('grid', 'sm:grid-cols-2', 'gap-5', 'px-4', 'py-5')}>
      <Stats
        leftPart={
          <>
            <StatsDt
              className={clsx(
                { '!text-interlayDenim': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
                { 'dark:!text-kintsugiSupernova': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
              )}
            >
              {t('dashboard.issue.issued')}
            </StatsDt>
            <StatsDd>
              {t('dashboard.issue.total_interbtc', {
                amount: displayMonetaryAmount(totalWrappedTokenAmount),
                wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL
              })}
            </StatsDd>
            <StatsDd>${getUsdAmount(totalWrappedTokenAmount, prices.bitcoin?.usd).toLocaleString()}</StatsDd>
            <StatsDt className='!text-interlayConifer'>{t('dashboard.issue.issue_requests')}</StatsDt>
            <StatsDd>{totalSuccessfulIssueCount}</StatsDd>
          </>
        }
      />
      <IssuedChart />
    </Panel>
  );
}
Example #7
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
IssueRequestsTable = (): JSX.Element => {
  const dispatch = useDispatch();
  const { t } = useTranslation();

  const queryParams = useQueryParams();
  const selectedIssueRequestId = queryParams.get(QUERY_PARAMETERS.ISSUE_REQUEST_ID);
  const selectedPage = Number(queryParams.get(QUERY_PARAMETERS.ISSUE_REQUESTS_PAGE)) || 1;
  const selectedPageIndex = selectedPage - 1;
  const updateQueryParameters = useUpdateQueryParameters();

  const { address, extensions, bridgeLoaded } = useSelector((state: StoreType) => state.general);

  const {
    isIdle: btcConfirmationsIdle,
    isLoading: btcConfirmationsLoading,
    data: btcConfirmations,
    error: btcConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableBitcoinConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(btcConfirmationsError);

  const {
    isIdle: latestParachainActiveBlockIdle,
    isLoading: latestParachainActiveBlockLoading,
    data: latestParachainActiveBlock,
    error: latestParachainActiveBlockError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'system', 'getCurrentActiveBlockNumber'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(latestParachainActiveBlockError);

  const {
    isIdle: parachainConfirmationsIdle,
    isLoading: parachainConfirmationsLoading,
    data: parachainConfirmations,
    error: parachainConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableParachainConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(parachainConfirmationsError);

  const {
    isIdle: issueRequestsTotalCountIdle,
    isLoading: issueRequestsTotalCountLoading,
    data: issueRequestsTotalCount,
    error: issueRequestsTotalCountError
    // TODO: should type properly (`Relay`)
  } = useQuery<GraphqlReturn<any>, Error>(
    [GRAPHQL_FETCHER, issueCountQuery(`userParachainAddress_eq: "${address}"`)],
    graphqlFetcher<GraphqlReturn<any>>()
  );
  useErrorHandler(issueRequestsTotalCountError);

  const {
    isIdle: issueRequestsIdle,
    isLoading: issueRequestsLoading,
    data: issueRequests,
    error: issueRequestsError
    // TODO: should type properly (`Relay`)
  } = useQuery<any, Error>(
    [
      ISSUE_FETCHER,
      selectedPageIndex * TABLE_PAGE_LIMIT, // offset
      TABLE_PAGE_LIMIT, // limit
      `userParachainAddress_eq: "${address}"` // `WHERE` condition
    ],
    issueFetcher
  );
  useErrorHandler(issueRequestsError);

  const columns = React.useMemo(
    () => [
      {
        Header: t('issue_page.updated'),
        classNames: ['text-left'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          let date;
          if (issue.execution) {
            date = issue.execution.timestamp;
          } else if (issue.cancellation) {
            date = issue.cancellation.timestamp;
          } else {
            date = issue.request.timestamp;
          }

          return <>{formatDateTimePrecise(new Date(date))}</>;
        }
      },
      {
        Header: `${t('issue_page.amount')} (${WRAPPED_TOKEN_SYMBOL})`,
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          let wrappedTokenAmount;
          if (issue.execution) {
            wrappedTokenAmount = issue.execution.amountWrapped;
          } else {
            wrappedTokenAmount = issue.request.amountWrapped;
          }

          return <>{displayMonetaryAmount(wrappedTokenAmount)}</>;
        }
      },
      {
        Header: t('issue_page.btc_transaction'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issueRequest } }: any) {
          return (
            <>
              {issueRequest.backingPayment.btcTxId ? (
                <ExternalLink
                  href={`${BTC_EXPLORER_TRANSACTION_API}${issueRequest.backingPayment.btcTxId}`}
                  onClick={(event) => {
                    event.stopPropagation();
                  }}
                >
                  {shortTxId(issueRequest.backingPayment.btcTxId)}
                </ExternalLink>
              ) : issueRequest.status === IssueStatus.Expired || issueRequest.status === IssueStatus.Cancelled ? (
                t('redeem_page.failed')
              ) : (
                `${t('pending')}...`
              )}
            </>
          );
        }
      },
      {
        Header: t('issue_page.confirmations'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          const value = issue.backingPayment.confirmations;
          return <>{value === undefined ? t('not_applicable') : Math.max(value, 0)}</>;
        }
      },
      {
        Header: t('status'),
        accessor: 'status',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: IssueStatus }) {
          let icon;
          let notice;
          let colorClassName;
          switch (value) {
            case IssueStatus.RequestedRefund:
            case IssueStatus.Completed: {
              icon = <FaCheck />;
              notice = t('completed');
              colorClassName = 'text-interlayConifer';
              break;
            }
            case IssueStatus.Cancelled:
            case IssueStatus.Expired: {
              icon = <FaRegTimesCircle />;
              notice = t('cancelled');
              colorClassName = 'text-interlayCinnabar';
              break;
            }
            default: {
              icon = <FaRegClock />;
              notice = t('pending');
              colorClassName = 'text-interlayCalifornia';
              break;
            }
          }

          // TODO: double-check with `src\components\UI\InterlayTable\StatusCell\index.tsx`
          return (
            <div className={clsx('inline-flex', 'items-center', 'space-x-1.5', colorClassName)}>
              {icon}
              <span>{notice}</span>
            </div>
          );
        }
      }
    ],
    [t]
  );

  const data =
    issueRequests === undefined ||
    btcConfirmations === undefined ||
    parachainConfirmations === undefined ||
    latestParachainActiveBlock === undefined
      ? []
      : issueRequests.map(
          // TODO: should type properly (`Relay`)
          (issueRequest: any) =>
            getIssueWithStatus(issueRequest, btcConfirmations, parachainConfirmations, latestParachainActiveBlock)
        );

  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
    columns,
    data
  });

  if (
    btcConfirmationsIdle ||
    btcConfirmationsLoading ||
    parachainConfirmationsIdle ||
    parachainConfirmationsLoading ||
    latestParachainActiveBlockIdle ||
    latestParachainActiveBlockLoading ||
    issueRequestsTotalCountIdle ||
    issueRequestsTotalCountLoading ||
    issueRequestsIdle ||
    issueRequestsLoading
  ) {
    return <PrimaryColorEllipsisLoader />;
  }
  if (issueRequestsTotalCount === undefined) {
    throw new Error('Something went wrong!');
  }

  const handlePageChange = ({ selected: newSelectedPageIndex }: { selected: number }) => {
    updateQueryParameters({
      [QUERY_PARAMETERS.ISSUE_REQUESTS_PAGE]: (newSelectedPageIndex + 1).toString()
    });
  };

  const handleIssueModalClose = () => {
    updateQueryParameters({
      [QUERY_PARAMETERS.ISSUE_REQUEST_ID]: ''
    });
  };

  const handleRowClick = (requestId: string) => () => {
    if (extensions.length && address) {
      updateQueryParameters({
        [QUERY_PARAMETERS.ISSUE_REQUEST_ID]: requestId
      });
    } else {
      dispatch(showAccountModalAction(true));
    }
  };

  const totalSuccessfulIssueCount = issueRequestsTotalCount.data.issuesConnection.totalCount || 0;
  const pageCount = Math.ceil(totalSuccessfulIssueCount / TABLE_PAGE_LIMIT);
  // TODO: should type properly (`Relay`)
  const selectedIssueRequest = data.find((issueRequest: any) => issueRequest.id === selectedIssueRequestId);

  return (
    <>
      <InterlayTableContainer className={clsx('space-y-6', 'container', 'mx-auto')}>
        <SectionTitle>{t('issue_page.issue_requests')}</SectionTitle>
        <InterlayTable {...getTableProps()}>
          <InterlayThead>
            {/* TODO: should type properly */}
            {headerGroups.map((headerGroup: any) => (
              // eslint-disable-next-line react/jsx-key
              <InterlayTr {...headerGroup.getHeaderGroupProps()}>
                {/* TODO: should type properly */}
                {headerGroup.headers.map((column: any) => (
                  // eslint-disable-next-line react/jsx-key
                  <InterlayTh
                    {...column.getHeaderProps([
                      {
                        className: clsx(column.classNames),
                        style: column.style
                      }
                    ])}
                  >
                    {column.render('Header')}
                  </InterlayTh>
                ))}
              </InterlayTr>
            ))}
          </InterlayThead>
          <InterlayTbody {...getTableBodyProps()}>
            {/* TODO: should type properly */}
            {rows.map((row: any) => {
              prepareRow(row);

              const { className: rowClassName, ...restRowProps } = row.getRowProps();

              return (
                // eslint-disable-next-line react/jsx-key
                <InterlayTr
                  className={clsx(rowClassName, 'cursor-pointer')}
                  {...restRowProps}
                  onClick={handleRowClick(row.original.id)}
                >
                  {/* TODO: should type properly */}
                  {row.cells.map((cell: any) => {
                    return (
                      // eslint-disable-next-line react/jsx-key
                      <InterlayTd
                        {...cell.getCellProps([
                          {
                            className: clsx(cell.column.classNames),
                            style: cell.column.style
                          }
                        ])}
                      >
                        {cell.render('Cell')}
                      </InterlayTd>
                    );
                  })}
                </InterlayTr>
              );
            })}
          </InterlayTbody>
        </InterlayTable>
        {pageCount > 0 && (
          <div className={clsx('flex', 'justify-end')}>
            <InterlayPagination
              pageCount={pageCount}
              marginPagesDisplayed={2}
              pageRangeDisplayed={5}
              onPageChange={handlePageChange}
              forcePage={selectedPageIndex}
            />
          </div>
        )}
      </InterlayTableContainer>
      {selectedIssueRequest && (
        <IssueRequestModal
          open={!!selectedIssueRequest}
          onClose={handleIssueModalClose}
          request={selectedIssueRequest}
        />
      )}
    </>
  );
}
Example #8
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
VaultsTable = (): JSX.Element => {
  const { t } = useTranslation();
  const { bridgeLoaded } = useSelector((state: StoreType) => state.general);
  const history = useHistory();

  const {
    isIdle: currentActiveBlockNumberIdle,
    isLoading: currentActiveBlockNumberLoading,
    data: currentActiveBlockNumber,
    error: currentActiveBlockNumberError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'system', 'getCurrentActiveBlockNumber'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(currentActiveBlockNumberError);

  const {
    isIdle: secureCollateralThresholdIdle,
    isLoading: secureCollateralThresholdLoading,
    data: secureCollateralThreshold,
    error: secureCollateralThresholdError
  } = useQuery<Big, Error>(
    [GENERIC_FETCHER, 'vaults', 'getSecureCollateralThreshold', COLLATERAL_TOKEN],
    genericFetcher<Big>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(secureCollateralThresholdError);

  const {
    isIdle: liquidationCollateralThresholdIdle,
    isLoading: liquidationCollateralThresholdLoading,
    data: liquidationCollateralThreshold,
    error: liquidationCollateralThresholdError
  } = useQuery<Big, Error>(
    [GENERIC_FETCHER, 'vaults', 'getLiquidationCollateralThreshold', COLLATERAL_TOKEN],
    genericFetcher<Big>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(liquidationCollateralThresholdError);

  const {
    isIdle: btcToCollateralTokenRateIdle,
    isLoading: btcToCollateralTokenRateLoading,
    data: btcToCollateralTokenRate,
    error: btcToCollateralTokenRateError
  } = useQuery<BTCToCollateralTokenRate, Error>(
    [GENERIC_FETCHER, 'oracle', 'getExchangeRate', COLLATERAL_TOKEN],
    genericFetcher<BTCToCollateralTokenRate>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(btcToCollateralTokenRateError);

  const { isIdle: vaultsExtIdle, isLoading: vaultsExtLoading, data: vaultsExt, error: vaultsExtError } = useQuery<
    Array<VaultExt<BitcoinUnit>>,
    Error
  >([GENERIC_FETCHER, 'vaults', 'list'], genericFetcher<Array<VaultExt<BitcoinUnit>>>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(vaultsExtError);

  const columns = React.useMemo(
    () => [
      {
        Header: t('account_id'),
        accessor: 'vaultId',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: string }) {
          return <>{shortAddress(value)}</>;
        }
      },
      {
        Header: t('locked_dot', {
          collateralTokenSymbol: COLLATERAL_TOKEN_SYMBOL
        }),
        accessor: 'lockedDOT',
        classNames: ['text-right']
      },
      {
        Header: t('locked_btc'),
        accessor: 'lockedBTC',
        classNames: ['text-right']
      },
      {
        Header: t('pending_btc'),
        accessor: 'pendingBTC',
        classNames: ['text-right'],
        tooltip: t('vault.tip_pending_btc')
      },
      {
        Header: t('collateralization'),
        accessor: '',
        classNames: ['text-left'],
        tooltip: t('vault.tip_collateralization'),
        Cell: function FormattedCell({ row: { original } }: { row: { original: Vault } }) {
          if (original.unsettledCollateralization === undefined && original.settledCollateralization === undefined) {
            return <span>∞</span>;
          } else {
            return (
              <>
                {secureCollateralThreshold && (
                  <div>
                    <p
                      className={getCollateralizationColor(
                        original.settledCollateralization,
                        secureCollateralThreshold
                      )}
                    >
                      {original.settledCollateralization === undefined
                        ? '∞'
                        : roundTwoDecimals(original.settledCollateralization.toString()) + '%'}
                    </p>
                    <p className='text-xs'>
                      <span>{t('vault.pending_table_subcell')}</span>
                      <span
                        className={getCollateralizationColor(
                          original.unsettledCollateralization,
                          secureCollateralThreshold
                        )}
                      >
                        {original.unsettledCollateralization === undefined
                          ? '∞'
                          : roundTwoDecimals(original.unsettledCollateralization.toString()) + '%'}
                      </span>
                    </p>
                  </div>
                )}
              </>
            );
          }
        }
      },
      {
        Header: t('status'),
        accessor: 'status',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: string }) {
          let statusClasses;
          if (value === constants.VAULT_STATUS_ACTIVE) {
            statusClasses = clsx('text-interlayConifer', 'font-medium');
          }
          if (value === constants.VAULT_STATUS_UNDER_COLLATERALIZED) {
            statusClasses = clsx('text-interlayCalifornia', 'font-medium');
          }
          if (value === constants.VAULT_STATUS_THEFT || value === constants.VAULT_STATUS_LIQUIDATED) {
            statusClasses = clsx('text-interlayCinnabar', 'font-medium');
          }

          return <span className={statusClasses}>{value}</span>;
        }
      }
    ],
    [t, secureCollateralThreshold]
  );

  const vaults: Array<Vault> | undefined = React.useMemo(() => {
    if (
      vaultsExt &&
      btcToCollateralTokenRate &&
      liquidationCollateralThreshold &&
      secureCollateralThreshold &&
      currentActiveBlockNumber
    ) {
      const rawVaults = vaultsExt.map((vaultExt) => {
        const statusLabel = getVaultStatusLabel(
          vaultExt,
          currentActiveBlockNumber,
          liquidationCollateralThreshold,
          secureCollateralThreshold,
          btcToCollateralTokenRate,
          t
        );

        const vaultCollateral = vaultExt.backingCollateral;
        const settledTokens = vaultExt.issuedTokens;
        const settledCollateralization = getCollateralization(vaultCollateral, settledTokens, btcToCollateralTokenRate);
        const unsettledTokens = vaultExt.toBeIssuedTokens;
        const unsettledCollateralization = getCollateralization(
          vaultCollateral,
          unsettledTokens.add(settledTokens),
          btcToCollateralTokenRate
        );

        return {
          vaultId: vaultExt.id.accountId.toString(),
          // TODO: fetch collateral reserved
          lockedBTC: displayMonetaryAmount(settledTokens),
          lockedDOT: displayMonetaryAmount(vaultCollateral),
          pendingBTC: displayMonetaryAmount(unsettledTokens),
          status: statusLabel,
          unsettledCollateralization: unsettledCollateralization?.toString(),
          settledCollateralization: settledCollateralization?.toString()
        };
      });

      const sortedVaults = rawVaults.sort((vaultA, vaultB) => {
        const vaultALockedBTC = vaultA.lockedBTC;
        const vaultBLockedBTC = vaultB.lockedBTC;
        return vaultALockedBTC < vaultBLockedBTC ? 1 : vaultALockedBTC > vaultBLockedBTC ? -1 : 0;
      });

      return sortedVaults;
    }
  }, [
    btcToCollateralTokenRate,
    currentActiveBlockNumber,
    liquidationCollateralThreshold,
    secureCollateralThreshold,
    t,
    vaultsExt
  ]);

  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
    columns,
    data: vaults ?? []
  });

  const renderTable = () => {
    if (
      currentActiveBlockNumberIdle ||
      currentActiveBlockNumberLoading ||
      secureCollateralThresholdIdle ||
      secureCollateralThresholdLoading ||
      liquidationCollateralThresholdIdle ||
      liquidationCollateralThresholdLoading ||
      btcToCollateralTokenRateIdle ||
      btcToCollateralTokenRateLoading ||
      vaultsExtIdle ||
      vaultsExtLoading
    ) {
      return <PrimaryColorEllipsisLoader />;
    }

    const handleRowClick = (vaultId: string) => () => {
      history.push(PAGES.VAULTS.replace(`:${URL_PARAMETERS.VAULT.ACCOUNT}`, vaultId));
    };

    return (
      <InterlayTable {...getTableProps()}>
        <InterlayThead>
          {/* TODO: should type properly */}
          {headerGroups.map((headerGroup: any) => (
            // eslint-disable-next-line react/jsx-key
            <InterlayTr {...headerGroup.getHeaderGroupProps()}>
              {/* TODO: should type properly */}
              {headerGroup.headers.map((column: any) => (
                // eslint-disable-next-line react/jsx-key
                <InterlayTh
                  {...column.getHeaderProps([
                    {
                      className: clsx(column.classNames),
                      style: column.style
                    }
                  ])}
                >
                  {column.render('Header')}
                  {column.tooltip && (
                    <InformationTooltip className={clsx('inline-block', 'ml-1')} label={column.tooltip} />
                  )}
                </InterlayTh>
              ))}
            </InterlayTr>
          ))}
        </InterlayThead>
        <InterlayTbody {...getTableBodyProps()}>
          {/* TODO: should type properly */}
          {rows.map((row: any) => {
            prepareRow(row);

            const { key, className: rowClassName, ...restRowProps } = row.getRowProps();

            return (
              <InterlayTr
                key={key}
                className={clsx(rowClassName, 'cursor-pointer')}
                {...restRowProps}
                onClick={handleRowClick(row.original.vaultId)}
              >
                {/* TODO: should type properly */}
                {row.cells.map((cell: any) => {
                  return (
                    // eslint-disable-next-line react/jsx-key
                    <InterlayTd
                      {...cell.getCellProps([
                        {
                          className: clsx(cell.column.classNames),
                          style: cell.column.style
                        }
                      ])}
                    >
                      {cell.render('Cell')}
                    </InterlayTd>
                  );
                })}
              </InterlayTr>
            );
          })}
        </InterlayTbody>
      </InterlayTable>
    );
  };

  // TODO: should add pagination
  return (
    <InterlayTableContainer className='space-y-6'>
      <SectionTitle>{t('dashboard.vault.active_vaults')}</SectionTitle>
      {renderTable()}
    </InterlayTableContainer>
  );
}
Example #9
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
DefaultRedeemRequest = ({ request }: Props): JSX.Element => {
  const { t } = useTranslation();
  const { bridgeLoaded } = useSelector((state: StoreType) => state.general);

  const {
    isIdle: stableBitcoinConfirmationsIdle,
    isLoading: stableBitcoinConfirmationsLoading,
    data: stableBitcoinConfirmations = 1, // TODO: double-check
    error: stableBitcoinConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableBitcoinConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(stableBitcoinConfirmationsError);

  const {
    isIdle: stableParachainConfirmationsIdle,
    isLoading: stableParachainConfirmationsLoading,
    data: stableParachainConfirmations = 100, // TODO: double-check
    error: stableParachainConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableParachainConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(stableParachainConfirmationsError);

  const {
    isIdle: parachainHeightIdle,
    isLoading: parachainHeightLoading,
    data: parachainHeight = 0, // TODO: double-check
    error: parachainHeightError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'system', 'getCurrentActiveBlockNumber'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(parachainHeightError);

  // TODO: should use skeleton loaders
  if (
    stableBitcoinConfirmationsIdle ||
    stableBitcoinConfirmationsLoading ||
    stableParachainConfirmationsIdle ||
    stableParachainConfirmationsLoading ||
    parachainHeightIdle ||
    parachainHeightLoading
  ) {
    return <>Loading...</>;
  }

  const requestConfirmations = request.backingPayment.includedAtParachainActiveBlock
    ? parachainHeight - request.backingPayment.includedAtParachainActiveBlock
    : 0;

  return (
    <RequestWrapper>
      <h2 className={clsx('text-3xl', 'font-medium')}>{t('received')}</h2>
      <Ring48 className='ring-interlayCalifornia'>
        <Ring48Title>{t('redeem_page.waiting_for')}</Ring48Title>
        <Ring48Title>{t('confirmations')}</Ring48Title>
        <Ring48Value className='text-interlayConifer'>
          {`${request.backingPayment.confirmations || 0}/${stableBitcoinConfirmations}`}
        </Ring48Value>
        <Ring48Value className='text-interlayConifer'>
          {`${requestConfirmations}/${stableParachainConfirmations}`}
        </Ring48Value>
      </Ring48>
      <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 }
          )}
        >
          {t('issue_page.btc_transaction')}:
        </span>
        <span className='font-medium'>{shortAddress(request.backingPayment.btcTxId || '')}</span>
      </p>
    </RequestWrapper>
  );
}
Example #10
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 #11
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
RedeemRequestsTable = (): JSX.Element => {
  const { t } = useTranslation();

  const queryParams = useQueryParams();
  const selectedRedeemRequestId = queryParams.get(QUERY_PARAMETERS.REDEEM_REQUEST_ID);
  const selectedPage = Number(queryParams.get(QUERY_PARAMETERS.REDEEM_REQUESTS_PAGE)) || 1;
  const selectedPageIndex = selectedPage - 1;
  const updateQueryParameters = useUpdateQueryParameters();

  const { address, bridgeLoaded } = useSelector((state: StoreType) => state.general);

  const {
    isIdle: btcConfirmationsIdle,
    isLoading: btcConfirmationsLoading,
    data: btcConfirmations,
    error: btcConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableBitcoinConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(btcConfirmationsError);

  const {
    isIdle: latestParachainActiveBlockIdle,
    isLoading: latestParachainActiveBlockLoading,
    data: latestParachainActiveBlock,
    error: latestParachainActiveBlockError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'system', 'getCurrentActiveBlockNumber'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(latestParachainActiveBlockError);

  const {
    isIdle: parachainConfirmationsIdle,
    isLoading: parachainConfirmationsLoading,
    data: parachainConfirmations,
    error: parachainConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableParachainConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(parachainConfirmationsError);

  const {
    isIdle: redeemRequestsTotalCountIdle,
    isLoading: redeemRequestsTotalCountLoading,
    data: redeemRequestsTotalCount,
    error: redeemRequestsTotalCountError
    // TODO: should type properly (`Relay`)
  } = useQuery<GraphqlReturn<any>, Error>(
    [GRAPHQL_FETCHER, redeemCountQuery(`userParachainAddress_eq: "${address}"`)],
    graphqlFetcher<GraphqlReturn<any>>()
  );
  useErrorHandler(redeemRequestsTotalCountError);

  const {
    isIdle: redeemRequestsIdle,
    isLoading: redeemRequestsLoading,
    data: redeemRequests,
    error: redeemRequestsError
    // TODO: should type properly (`Relay`)
  } = useQuery<any, Error>(
    [
      REDEEM_FETCHER,
      selectedPageIndex * TABLE_PAGE_LIMIT, // offset
      TABLE_PAGE_LIMIT, // limit
      `userParachainAddress_eq: "${address}"` // WHERE condition
    ],
    redeemFetcher
  );
  useErrorHandler(redeemRequestsError);

  const columns = React.useMemo(
    () => [
      {
        Header: t('issue_page.updated'),
        classNames: ['text-left'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          let date;
          if (redeem.execution) {
            date = redeem.execution.timestamp;
          } else if (redeem.cancellation) {
            date = redeem.cancellation.timestamp;
          } else {
            date = redeem.request.timestamp;
          }

          return <>{formatDateTimePrecise(new Date(date))}</>;
        }
      },
      {
        Header: `${t('redeem_page.amount')} (${WRAPPED_TOKEN_SYMBOL})`,
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          return <>{displayMonetaryAmount(redeem.request.requestedAmountBacking)}</>;
        }
      },
      {
        Header: t('issue_page.btc_transaction'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeemRequest } }: any) {
          return (
            <>
              {redeemRequest.status === RedeemStatus.Expired ||
              redeemRequest.status === RedeemStatus.Retried ||
              redeemRequest.status === RedeemStatus.Reimbursed ? (
                t('redeem_page.failed')
              ) : (
                <>
                  {redeemRequest.backingPayment.btcTxId ? (
                    <ExternalLink
                      href={`${BTC_EXPLORER_TRANSACTION_API}${redeemRequest.backingPayment.btcTxId}`}
                      onClick={(event) => {
                        event.stopPropagation();
                      }}
                    >
                      {shortTxId(redeemRequest.backingPayment.btcTxId)}
                    </ExternalLink>
                  ) : (
                    `${t('pending')}...`
                  )}
                </>
              )}
            </>
          );
        }
      },
      {
        Header: t('issue_page.confirmations'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          const value = redeem.backingPayment.confirmations;
          return <>{value === undefined ? t('not_applicable') : Math.max(value, 0)}</>;
        }
      },
      {
        Header: t('status'),
        accessor: 'status',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: RedeemStatus }) {
          let icon;
          let notice;
          let colorClassName;
          switch (value) {
            case RedeemStatus.Reimbursed: {
              icon = <FaCheck />; // TODO: should update according to the design
              notice = t('redeem_page.reimbursed');
              colorClassName = 'text-interlayConifer'; // TODO: should update according to the design
              break;
            }
            case RedeemStatus.Expired: {
              icon = <FaRegTimesCircle />;
              notice = t('redeem_page.recover');
              colorClassName = 'text-interlayCinnabar';
              break;
            }
            case RedeemStatus.Retried: {
              icon = <FaCheck />;
              notice = t('redeem_page.retried');
              colorClassName = 'text-interlayConifer';
              break;
            }
            case RedeemStatus.Completed: {
              icon = <FaCheck />;
              notice = t('completed');
              colorClassName = 'text-interlayConifer';
              break;
            }
            default: {
              icon = <FaRegClock />;
              notice = t('pending');
              colorClassName = 'text-interlayCalifornia';
              break;
            }
          }

          return (
            <div className={clsx('inline-flex', 'items-center', 'space-x-1.5', colorClassName)}>
              {icon}
              <span>{notice}</span>
            </div>
          );
        }
      }
    ],
    [t]
  );

  const data =
    redeemRequests === undefined ||
    btcConfirmations === undefined ||
    parachainConfirmations === undefined ||
    latestParachainActiveBlock === undefined
      ? []
      : redeemRequests.map(
          // TODO: should type properly (`Relay`)
          (redeem: any) =>
            getRedeemWithStatus(redeem, btcConfirmations, parachainConfirmations, latestParachainActiveBlock)
        );

  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
    columns,
    data
  });

  if (
    btcConfirmationsIdle ||
    btcConfirmationsLoading ||
    parachainConfirmationsIdle ||
    parachainConfirmationsLoading ||
    latestParachainActiveBlockIdle ||
    latestParachainActiveBlockLoading ||
    redeemRequestsIdle ||
    redeemRequestsLoading ||
    redeemRequestsTotalCountIdle ||
    redeemRequestsTotalCountLoading
  ) {
    return <PrimaryColorEllipsisLoader />;
  }
  if (redeemRequestsTotalCount === undefined) {
    throw new Error('Something went wrong!');
  }

  const handlePageChange = ({ selected: newSelectedPageIndex }: { selected: number }) => {
    updateQueryParameters({
      [QUERY_PARAMETERS.REDEEM_REQUESTS_PAGE]: (newSelectedPageIndex + 1).toString()
    });
  };

  const handleRedeemModalClose = () => {
    updateQueryParameters({
      [QUERY_PARAMETERS.REDEEM_REQUEST_ID]: ''
    });
  };

  const handleRowClick = (requestId: string) => () => {
    updateQueryParameters({
      [QUERY_PARAMETERS.REDEEM_REQUEST_ID]: requestId
    });
  };

  const totalSuccessfulRedeemCount = redeemRequestsTotalCount.data.redeemsConnection.totalCount || 0;
  const pageCount = Math.ceil(totalSuccessfulRedeemCount / TABLE_PAGE_LIMIT);
  const selectedRedeemRequest = data.find((redeemRequest: any) => redeemRequest.id === selectedRedeemRequestId);

  return (
    <>
      <InterlayTableContainer className={clsx('space-y-6', 'container', 'mx-auto')}>
        <SectionTitle>{t('redeem_requests')}</SectionTitle>
        <InterlayTable {...getTableProps()}>
          <InterlayThead>
            {/* TODO: should type properly */}
            {headerGroups.map((headerGroup: any) => (
              // eslint-disable-next-line react/jsx-key
              <InterlayTr {...headerGroup.getHeaderGroupProps()}>
                {/* TODO: should type properly */}
                {headerGroup.headers.map((column: any) => (
                  // eslint-disable-next-line react/jsx-key
                  <InterlayTh
                    {...column.getHeaderProps([
                      {
                        className: clsx(column.classNames),
                        style: column.style
                      }
                    ])}
                  >
                    {column.render('Header')}
                  </InterlayTh>
                ))}
              </InterlayTr>
            ))}
          </InterlayThead>
          <InterlayTbody {...getTableBodyProps()}>
            {/* TODO: should type properly */}
            {rows.map((row: any) => {
              prepareRow(row);

              const { className: rowClassName, ...restRowProps } = row.getRowProps();

              return (
                // eslint-disable-next-line react/jsx-key
                <InterlayTr
                  className={clsx(rowClassName, 'cursor-pointer')}
                  {...restRowProps}
                  onClick={handleRowClick(row.original.id)}
                >
                  {/* TODO: should type properly */}
                  {row.cells.map((cell: any) => {
                    return (
                      // eslint-disable-next-line react/jsx-key
                      <InterlayTd
                        {...cell.getCellProps([
                          {
                            className: clsx(cell.column.classNames),
                            style: cell.column.style
                          }
                        ])}
                      >
                        {cell.render('Cell')}
                      </InterlayTd>
                    );
                  })}
                </InterlayTr>
              );
            })}
          </InterlayTbody>
        </InterlayTable>
        {pageCount > 0 && (
          <div className={clsx('flex', 'justify-end')}>
            <InterlayPagination
              pageCount={pageCount}
              marginPagesDisplayed={2}
              pageRangeDisplayed={5}
              onPageChange={handlePageChange}
              forcePage={selectedPageIndex}
            />
          </div>
        )}
      </InterlayTableContainer>
      {selectedRedeemRequest && (
        <RedeemRequestModal
          open={!!selectedRedeemRequest}
          onClose={handleRedeemModalClose}
          request={selectedRedeemRequest}
        />
      )}
    </>
  );
}
Example #12
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
CrossChainTransferForm = (): JSX.Element => {
  // TODO: review how we're handling the relay chain api - for now it can
  // be scoped to this component, but long term it needs to be handled at
  // the application level.
  const [api, setApi] = React.useState<RelayChainApi | undefined>(undefined);
  const [relayChainBalance, setRelayChainBalance] = React.useState<RelayChainMonetaryAmount | undefined>(undefined);
  const [fromChain, setFromChain] = React.useState<ChainType | undefined>(ChainType.RelayChain);
  const [toChain, setToChain] = React.useState<ChainType | undefined>(ChainType.Parachain);
  const [destination, setDestination] = React.useState<InjectedAccountWithMeta | undefined>(undefined);
  const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE);
  const [submitError, setSubmitError] = React.useState<Error | null>(null);
  // TODO: this could be removed form state using React hook form getValue/watch
  const [approxUsdValue, setApproxUsdValue] = React.useState<string>('0');

  const dispatch = useDispatch();
  const { t } = useTranslation();
  const handleError = useErrorHandler();

  const {
    register,
    handleSubmit,
    formState: { errors },
    reset
  } = useForm<CrossChainTransferFormData>({
    mode: 'onChange'
  });

  const { address, collateralTokenTransferableBalance, parachainStatus, prices } = useSelector(
    (state: StoreType) => state.general
  );

  const onSubmit = async (data: CrossChainTransferFormData) => {
    if (!address) return;
    if (!destination) return;

    try {
      setSubmitStatus(STATUSES.PENDING);

      if (!api) return;

      // We can use if else here as we only support two chains
      if (fromChain === ChainType.RelayChain) {
        await transferToParachain(
          api,
          address,
          destination.address,
          newMonetaryAmount(data[TRANSFER_AMOUNT], COLLATERAL_TOKEN, true)
        );
      } else {
        await transferToRelayChain(
          window.bridge.api,
          address,
          destination.address,
          newMonetaryAmount(data[TRANSFER_AMOUNT], COLLATERAL_TOKEN, true)
        );
      }

      setSubmitStatus(STATUSES.RESOLVED);
    } catch (error) {
      setSubmitStatus(STATUSES.REJECTED);
      setSubmitError(error);
    }
  };

  const handleConfirmClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    if (!address) {
      dispatch(showAccountModalAction(true));
      event.preventDefault();
    }
  };

  const handleUpdateUsdAmount = (event: any) => {
    if (!event.target.value) return;

    const value = newMonetaryAmount(event.target.value, COLLATERAL_TOKEN, true);
    const usd = getUsdAmount(value, prices.collateralToken?.usd);

    setApproxUsdValue(usd);
  };

  const validateRelayChainTransferAmount = (value: number): string | undefined => {
    const transferAmount = newMonetaryAmount(value, COLLATERAL_TOKEN, true);

    return relayChainBalance?.lt(transferAmount) ? t('insufficient_funds') : undefined;
  };

  const validateParachainTransferAmount = (value: number): string | undefined => {
    const transferAmount = newMonetaryAmount(value, COLLATERAL_TOKEN, true);

    // TODO: this api check won't be necessary when the api call is moved out of
    // the component
    const existentialDeposit = api
      ? newMonetaryAmount(getExistentialDeposit(api), COLLATERAL_TOKEN)
      : newMonetaryAmount('0', COLLATERAL_TOKEN);

    // TODO: we need to handle and validate transfer fees properly. Implemented here initially
    // because it was an issue during testing.
    if (collateralTokenTransferableBalance.lt(transferAmount)) {
      return t('insufficient_funds');
      // Check transferred amount won't be below existential deposit when fees are deducted
      // This check is redundant if the relay chain balance is above zero
    } else if (relayChainBalance?.isZero() && transferAmount.sub(transferFee).lt(existentialDeposit)) {
      return t('transfer_page.cross_chain_transfer_form.insufficient_funds_to_maintain_existential_depoit');
      // Check the transfer amount is more than the fee
    } else if (transferAmount.lte(transferFee)) {
      return t('transfer_page.cross_chain_transfer_form.insufficient_funds_to_pay_fees', {
        transferFee: `${displayMonetaryAmount(transferFee)} ${COLLATERAL_TOKEN_SYMBOL}`
      });
    } else {
      return undefined;
    }
  };

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

    const initialiseApi = async () => {
      try {
        const api = await createRelayChainApi();
        setApi(api);
      } catch (error) {
        handleError(error);
      }
    };

    initialiseApi();
  }, [api, handleError]);

  React.useEffect(() => {
    if (!api) return;
    if (!handleError) return;
    if (relayChainBalance) return;

    const fetchRelayChainBalance = async () => {
      try {
        const balance: any = await getRelayChainBalance(api, address);
        setRelayChainBalance(balance.sub(transferFee));
      } catch (error) {
        handleError(error);
      }
    };

    fetchRelayChainBalance();
  }, [api, address, handleError, relayChainBalance]);

  const handleSetFromChain = (chain: ChainOption) => {
    setFromChain(chain.type);

    // prevent toChain having the same value as fromChain
    if (chain.type === toChain) {
      setToChain(chain.type === ChainType.Parachain ? ChainType.RelayChain : ChainType.Parachain);
    }
  };

  const handleSetToChain = (chain: ChainOption) => {
    setToChain(chain.type);

    // prevent fromChain having the same value as toChain
    if (chain.type === fromChain) {
      setFromChain(chain.type === ChainType.Parachain ? ChainType.RelayChain : ChainType.Parachain);
    }
  };

  // This ensures that triggering the notification and clearing
  // the form happen at the same time.
  React.useEffect(() => {
    if (submitStatus !== STATUSES.RESOLVED) return;

    toast.success(t('transfer_page.successfully_transferred'));

    reset({
      [TRANSFER_AMOUNT]: ''
    });
  }, [submitStatus, reset, t]);

  return (
    <>
      {api && (
        <>
          <form className='space-y-8' onSubmit={handleSubmit(onSubmit)}>
            <FormTitle>{t('transfer_page.cross_chain_transfer_form.title')}</FormTitle>
            <div>
              {fromChain === ChainType.RelayChain ? (
                <AvailableBalanceUI
                  label={t('transfer_page.cross_chain_transfer_form.relay_chain_balance')}
                  balance={displayMonetaryAmount(relayChainBalance)}
                  tokenSymbol={COLLATERAL_TOKEN_SYMBOL}
                />
              ) : (
                <AvailableBalanceUI
                  label={t('transfer_page.cross_chain_transfer_form.parachain_balance')}
                  balance={displayMonetaryAmount(collateralTokenTransferableBalance)}
                  tokenSymbol={COLLATERAL_TOKEN_SYMBOL}
                />
              )}
              <TokenField
                onChange={handleUpdateUsdAmount}
                id={TRANSFER_AMOUNT}
                name={TRANSFER_AMOUNT}
                ref={register({
                  required: {
                    value: true,
                    message: t('transfer_page.cross_chain_transfer_form.please_enter_amount')
                  },
                  validate: (value) =>
                    fromChain === ChainType.RelayChain
                      ? validateRelayChainTransferAmount(value)
                      : validateParachainTransferAmount(value)
                })}
                error={!!errors[TRANSFER_AMOUNT]}
                helperText={errors[TRANSFER_AMOUNT]?.message}
                label={COLLATERAL_TOKEN_SYMBOL}
                approxUSD={`≈ $ ${approxUsdValue}`}
              />
            </div>
            <Chains
              label={t('transfer_page.cross_chain_transfer_form.from_chain')}
              selectedChain={fromChain}
              onChange={handleSetFromChain}
            />
            <Chains
              label={t('transfer_page.cross_chain_transfer_form.to_chain')}
              selectedChain={toChain}
              onChange={handleSetToChain}
            />
            <Accounts
              label={t('transfer_page.cross_chain_transfer_form.target_account')}
              callbackFunction={setDestination}
            />
            <SubmitButton
              disabled={parachainStatus === (ParachainStatus.Loading || ParachainStatus.Shutdown)}
              pending={submitStatus === STATUSES.PENDING}
              onClick={handleConfirmClick}
            >
              {address ? t('transfer') : t('connect_wallet')}
            </SubmitButton>
          </form>
          {submitStatus === STATUSES.REJECTED && submitError && (
            <ErrorModal
              open={!!submitError}
              onClose={() => {
                setSubmitStatus(STATUSES.IDLE);
                setSubmitError(null);
              }}
              title='Error'
              description={typeof submitError === 'string' ? submitError : submitError.message}
            />
          )}
        </>
      )}
    </>
  );
}
Example #13
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
ClaimRewardsButton = ({
  vaultAccountId,
  ...rest
}: CustomProps & InterlayDenimOrKintsugiMidnightContainedButtonProps): JSX.Element => {
  const { t } = useTranslation();
  const { bridgeLoaded } = useSelector((state: StoreType) => state.general);

  const {
    isIdle: governanceTokenRewardIdle,
    isLoading: governanceTokenRewardLoading,
    data: governanceTokenReward,
    error: governanceTokenRewardError,
    refetch: governanceTokenRewardRefetch
  } = useQuery<GovernanceTokenMonetaryAmount, Error>(
    [
      GENERIC_FETCHER,
      'vaults',
      'getGovernanceReward',
      vaultAccountId,
      COLLATERAL_TOKEN_ID_LITERAL,
      GOVERNANCE_TOKEN_SYMBOL
    ],
    genericFetcher<GovernanceTokenMonetaryAmount>(),
    {
      enabled: !!bridgeLoaded && !!vaultAccountId
    }
  );
  useErrorHandler(governanceTokenRewardError);

  const claimRewardsMutation = useMutation<void, Error, void>(
    () => {
      if (vaultAccountId === undefined) {
        throw new Error('Something went wrong!');
      }

      const vaultId = newVaultId(
        window.bridge.api,
        vaultAccountId.toString(),
        COLLATERAL_TOKEN as CollateralCurrency,
        WRAPPED_TOKEN as WrappedCurrency
      );

      return window.bridge.rewards.withdrawRewards(vaultId);
    },
    {
      onSuccess: () => {
        governanceTokenRewardRefetch();
      }
    }
  );

  const handleClaimRewards = () => {
    claimRewardsMutation.mutate();
  };

  const initializing = governanceTokenRewardIdle || governanceTokenRewardLoading || !vaultAccountId;
  let governanceTokenAmountLabel;
  if (initializing) {
    governanceTokenAmountLabel = '-';
  } else {
    if (governanceTokenReward === undefined) {
      throw new Error('Something went wrong!');
    }

    governanceTokenAmountLabel = displayMonetaryAmount(governanceTokenReward);
  }

  const buttonDisabled = governanceTokenReward?.lte(ZERO_GOVERNANCE_TOKEN_AMOUNT);

  return (
    <>
      <InterlayDenimOrKintsugiSupernovaContainedButton
        disabled={initializing || buttonDisabled}
        onClick={handleClaimRewards}
        pending={claimRewardsMutation.isLoading}
        {...rest}
      >
        {t('vault.claim_governance_token_rewards', {
          governanceTokenAmount: governanceTokenAmountLabel,
          governanceTokenSymbol: GOVERNANCE_TOKEN_SYMBOL
        })}
      </InterlayDenimOrKintsugiSupernovaContainedButton>
      {claimRewardsMutation.isError && (
        <ErrorModal
          open={claimRewardsMutation.isError}
          onClose={() => {
            claimRewardsMutation.reset();
          }}
          title='Error'
          description={claimRewardsMutation.error?.message || ''}
        />
      )}
    </>
  );
}
Example #14
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
ReplaceTable = ({ vaultAddress }: Props): JSX.Element => {
  const { t } = useTranslation();
  const { bridgeLoaded } = useSelector((state: StoreType) => state.general);

  const vaultId = window.bridge?.api.createType(ACCOUNT_ID_TYPE_NAME, vaultAddress);
  const {
    isIdle: replaceRequestsIdle,
    isLoading: replaceRequestsLoading,
    data: replaceRequests,
    error: replaceRequestsError
  } = useQuery<Map<H256, ReplaceRequestExt>, Error>(
    [GENERIC_FETCHER, 'replace', 'mapReplaceRequests', vaultId],
    genericFetcher<Map<H256, ReplaceRequestExt>>(),
    {
      enabled: !!bridgeLoaded,
      refetchInterval: 10000
    }
  );
  useErrorHandler(replaceRequestsError);

  const columns = React.useMemo(
    () => [
      {
        Header: 'ID',
        accessor: 'id',
        classNames: ['text-center'],
        Cell: function FormattedCell({ value }: { value: H256 }) {
          return <>{stripHexPrefix(value.toString())}</>;
        }
      },
      {
        Header: t('vault.creation_block'),
        accessor: 'btcHeight',
        classNames: ['text-center']
      },
      {
        Header: t('vault.old_vault'),
        accessor: 'oldVault',
        classNames: ['text-center'],
        Cell: function FormattedCell({ value }: { value: InterbtcPrimitivesVaultId }) {
          return <>{shortAddress(value.accountId.toString())}</>;
        }
      },
      {
        Header: t('vault.new_vault'),
        accessor: 'newVault',
        classNames: ['text-center'],
        Cell: function FormattedCell({ value }: { value: InterbtcPrimitivesVaultId }) {
          return <>{shortAddress(value.accountId.toString())}</>;
        }
      },
      {
        Header: t('btc_address'),
        accessor: 'btcAddress',
        classNames: ['text-center'],
        Cell: function FormattedCell({ value }: { value: string }) {
          return <>{shortAddress(value)}</>;
        }
      },
      {
        Header: WRAPPED_TOKEN_SYMBOL,
        accessor: 'amount',
        classNames: ['text-right'],
        Cell: function FormattedCell({ value }: { value: MonetaryAmount<WrappedCurrency, BitcoinUnit> }) {
          return <>{displayMonetaryAmount(value)}</>;
        }
      },
      {
        Header: t('griefing_collateral'),
        accessor: 'collateral',
        classNames: ['text-right'],
        Cell: function FormattedCell({ value }: { value: MonetaryAmount<Currency<CollateralUnit>, CollateralUnit> }) {
          return <>{displayMonetaryAmount(value)}</>;
        }
      },
      {
        Header: t('status'),
        accessor: 'status',
        classNames: ['text-center'],
        Cell: function FormattedCell({ value }: { value: ReplaceRequestStatus }) {
          let label;
          if (value.isPending) {
            label = t('pending');
          } else if (value.isCompleted) {
            label = t('completed');
          } else if (value.isCancelled) {
            label = t('cancelled');
          } else {
            label = t('loading_ellipsis');
          }
          return <>{label}</>;
        }
      }
    ],
    [t]
  );

  const data = replaceRequests
    ? [...replaceRequests.entries()].map(([key, value]) => ({
        id: key,
        ...value
      }))
    : [];
  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
    columns,
    data
  });

  if (replaceRequestsIdle || replaceRequestsLoading) {
    return <PrimaryColorEllipsisLoader />;
  }
  if (replaceRequests === undefined) {
    throw new Error('Something went wrong!');
  }

  return (
    <InterlayTableContainer className={clsx('space-y-6', 'container', 'mx-auto')}>
      <SectionTitle>{t('vault.replace_requests')}</SectionTitle>
      <InterlayTable {...getTableProps()}>
        <InterlayThead>
          {headerGroups.map((headerGroup) => (
            // eslint-disable-next-line react/jsx-key
            <InterlayTr {...headerGroup.getHeaderGroupProps()}>
              {headerGroup.headers.map((column) => (
                // eslint-disable-next-line react/jsx-key
                <InterlayTh
                  {...column.getHeaderProps([
                    {
                      className: clsx(column.classNames),
                      style: column.style
                    }
                  ])}
                >
                  {column.render('Header')}
                </InterlayTh>
              ))}
            </InterlayTr>
          ))}
        </InterlayThead>
        <InterlayTbody {...getTableBodyProps()}>
          {rows.map((row) => {
            prepareRow(row);

            return (
              // eslint-disable-next-line react/jsx-key
              <InterlayTr {...row.getRowProps()}>
                {row.cells.map((cell) => {
                  return (
                    // eslint-disable-next-line react/jsx-key
                    <InterlayTd
                      {...cell.getCellProps([
                        {
                          className: clsx(cell.column.classNames),
                          style: cell.column.style
                        }
                      ])}
                    >
                      {cell.render('Cell')}
                    </InterlayTd>
                  );
                })}
              </InterlayTr>
            );
          })}
        </InterlayTbody>
      </InterlayTable>
    </InterlayTableContainer>
  );
}
Example #15
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
RequestIssueModal = ({ onClose, open, vaultAddress }: Props): JSX.Element => {
  const { register, handleSubmit, errors, watch, trigger } = useForm<RequestIssueFormData>({ mode: 'onChange' });
  const btcAmount = watch(WRAPPED_TOKEN_AMOUNT) || '0';

  const [status, setStatus] = React.useState(STATUSES.IDLE);
  const [vaultCapacity, setVaultCapacity] = React.useState(BitcoinAmount.zero);
  const [feeRate, setFeeRate] = React.useState(new Big(0.005)); // Set default to 0.5%
  const [depositRate, setDepositRate] = React.useState(new Big(0.00005)); // Set default to 0.005%
  const [btcToGovernanceTokenRate, setBTCToGovernanceTokenRate] = React.useState(
    new ExchangeRate<Bitcoin, BitcoinUnit, Currency<GovernanceUnit>, GovernanceUnit>(
      Bitcoin,
      GOVERNANCE_TOKEN,
      new Big(0)
    )
  );
  const [dustValue, setDustValue] = React.useState(BitcoinAmount.zero);
  const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE);
  const [submitError, setSubmitError] = React.useState<Error | null>(null);
  const [submittedRequest, setSubmittedRequest] = React.useState<Issue>();

  const { t } = useTranslation();
  const focusRef = React.useRef(null);

  const handleError = useErrorHandler();

  const {
    bridgeLoaded,
    address,
    bitcoinHeight,
    btcRelayHeight,
    prices,
    governanceTokenBalance,
    parachainStatus
  } = useSelector((state: StoreType) => state.general);

  const vaultAccountId = React.useMemo(() => {
    if (!bridgeLoaded) return;
    return newAccountId(window.bridge.api, vaultAddress);
  }, [bridgeLoaded, vaultAddress]);

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

    (async () => {
      try {
        setStatus(STATUSES.PENDING);
        const [
          theFeeRate,
          theDepositRate,
          theDustValue,
          theBtcToGovernanceToken,
          issuableAmount
        ] = await Promise.allSettled([
          // Loading this data is not strictly required as long as the constantly set values did
          // not change. However, you will not see the correct value for the security deposit.
          window.bridge.fee.getIssueFee(),
          window.bridge.fee.getIssueGriefingCollateralRate(),
          window.bridge.issue.getDustValue(),
          window.bridge.oracle.getExchangeRate(GOVERNANCE_TOKEN),
          // MEMO: this always uses KSM as collateral token
          window.bridge.issue.getVaultIssuableAmount(vaultAccountId, COLLATERAL_TOKEN_ID_LITERAL)
        ]);
        setStatus(STATUSES.RESOLVED);
        if (theFeeRate.status === 'fulfilled') {
          setFeeRate(theFeeRate.value);
        }
        if (theDepositRate.status === 'fulfilled') {
          setDepositRate(theDepositRate.value);
        }
        if (theDustValue.status === 'fulfilled') {
          setDustValue(theDustValue.value);
        }
        if (theBtcToGovernanceToken.status === 'fulfilled') {
          setBTCToGovernanceTokenRate(theBtcToGovernanceToken.value);
        }
        if (issuableAmount.status === 'fulfilled') {
          setVaultCapacity(issuableAmount.value);
        }
      } catch (error) {
        setStatus(STATUSES.REJECTED);
        handleError(error);
      }
    })();
  }, [bridgeLoaded, handleError, vaultAccountId]);

  if (status === STATUSES.IDLE || status === STATUSES.PENDING || vaultAccountId === undefined) {
    return <PrimaryColorEllipsisLoader />;
  }
  const onSubmit = async (data: RequestIssueFormData) => {
    try {
      setSubmitStatus(STATUSES.PENDING);
      await trigger(WRAPPED_TOKEN_AMOUNT);

      const wrappedTokenAmount = BitcoinAmount.from.BTC(data[WRAPPED_TOKEN_AMOUNT] || '0');

      const vaults = await window.bridge.vaults.getVaultsWithIssuableTokens();

      const result = await window.bridge.issue.request(
        wrappedTokenAmount,
        vaultAccountId,
        COLLATERAL_TOKEN_ID_LITERAL,
        false, // default
        0, // default
        vaults
      );

      const issueRequest = result[0];
      handleSubmittedRequestModalOpen(issueRequest);
    } catch (error) {
      setSubmitStatus(STATUSES.REJECTED);
    }
    setSubmitStatus(STATUSES.RESOLVED);
  };

  const validateForm = (value: string): string | undefined => {
    const numericValue = Number(value || '0');
    const btcAmount = BitcoinAmount.from.BTC(numericValue);

    const securityDeposit = btcToGovernanceTokenRate.toCounter(btcAmount).mul(depositRate);
    const minRequiredGovernanceTokenAmount = extraRequiredCollateralTokenAmount.add(securityDeposit);
    if (governanceTokenBalance.lte(minRequiredGovernanceTokenAmount)) {
      return t('insufficient_funds_governance_token', {
        governanceTokenSymbol: GOVERNANCE_TOKEN_SYMBOL
      });
    }

    if (btcAmount.lt(dustValue)) {
      return `${t('issue_page.validation_min_value')}${displayMonetaryAmount(dustValue)} BTC).`;
    }

    if (btcAmount.gt(vaultCapacity)) {
      return t('issue_page.maximum_in_single_request_error', {
        maxAmount: displayMonetaryAmount(vaultCapacity),
        wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL
      });
    }

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

    if (!bridgeLoaded) {
      return 'Bridge must be loaded!';
    }

    if (btcAmount === undefined) {
      return 'Invalid BTC amount input!';
    }

    return undefined;
  };

  const handleSubmittedRequestModalOpen = (newSubmittedRequest: Issue) => {
    setSubmittedRequest(newSubmittedRequest);
  };
  const handleSubmittedRequestModalClose = () => {
    setSubmittedRequest(undefined);
  };

  const parsedBTCAmount = BitcoinAmount.from.BTC(btcAmount);
  const bridgeFee = parsedBTCAmount.mul(feeRate);
  const securityDeposit = btcToGovernanceTokenRate.toCounter(parsedBTCAmount).mul(depositRate);
  const wrappedTokenAmount = parsedBTCAmount.sub(bridgeFee);

  return (
    <>
      <InterlayModal initialFocus={focusRef} open={open} onClose={onClose}>
        <InterlayModalInnerWrapper className={clsx('p-6', 'max-w-lg')}>
          <InterlayModalTitle as='h3' className={clsx('text-lg', 'font-medium', 'mb-6')}>
            {t('vault.request_issue')}
          </InterlayModalTitle>
          <CloseIconButton ref={focusRef} onClick={onClose} />
          <form className='space-y-4' onSubmit={handleSubmit(onSubmit)}>
            <p>{t('vault.issue_description')}</p>
            <p>
              {t('vault.max_capacity')} <strong>{displayMonetaryAmount(vaultCapacity)} BTC</strong>
            </p>
            <p>{t('vault.issue_amount')}</p>
            <div>
              <TokenField
                id={WRAPPED_TOKEN_AMOUNT}
                name={WRAPPED_TOKEN_AMOUNT}
                label='BTC'
                min={0}
                ref={register({
                  required: {
                    value: true,
                    message: t('issue_page.enter_valid_amount')
                  },
                  validate: (value) => validateForm(value)
                })}
                approxUSD={`≈ $ ${getUsdAmount(parsedBTCAmount || BitcoinAmount.zero, prices.bitcoin?.usd)}`}
                error={!!errors[WRAPPED_TOKEN_AMOUNT]}
                helperText={errors[WRAPPED_TOKEN_AMOUNT]?.message}
              />
            </div>
            <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('bridge_fee')}
                </h5>
              }
              unitIcon={<BitcoinLogoIcon width={23} height={23} />}
              value={displayMonetaryAmount(bridgeFee)}
              unitName='BTC'
              approxUSD={getUsdAmount(bridgeFee, prices.bitcoin?.usd)}
              tooltip={
                <InformationTooltip
                  className={clsx(
                    { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
                    { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
                  )}
                  label={t('issue_page.tooltip_bridge_fee')}
                />
              }
            />
            <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('issue_page.security_deposit')}
                </h5>
              }
              unitIcon={<GovernanceTokenLogoIcon width={20} />}
              value={displayMonetaryAmount(securityDeposit)}
              unitName={GOVERNANCE_TOKEN_SYMBOL}
              approxUSD={getUsdAmount(securityDeposit, prices.governanceToken?.usd)}
              tooltip={
                <InformationTooltip
                  className={clsx(
                    { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
                    { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
                  )}
                  label={t('issue_page.tooltip_security_deposit')}
                />
              }
            />
            <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('issue_page.transaction_fee')}
                </h5>
              }
              unitIcon={<GovernanceTokenLogoIcon width={20} />}
              value={displayMonetaryAmount(extraRequiredCollateralTokenAmount)}
              unitName={GOVERNANCE_TOKEN_SYMBOL}
              approxUSD={getUsdAmount(extraRequiredCollateralTokenAmount, prices.governanceToken?.usd)}
              tooltip={
                <InformationTooltip
                  className={clsx(
                    { 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
                    { 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
                  )}
                  label={t('issue_page.tooltip_transaction_fee')}
                />
              }
            />
            <Hr2 className={clsx('border-t-2', 'my-2.5')} />
            <PriceInfo
              title={
                <h5
                  className={clsx(
                    { 'text-interlayTextPrimaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
                    { 'dark:text-kintsugiTextPrimaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
                  )}
                >
                  {t('you_will_receive')}
                </h5>
              }
              unitIcon={<WrappedTokenLogoIcon width={20} />}
              value={displayMonetaryAmount(wrappedTokenAmount)}
              unitName={WRAPPED_TOKEN_SYMBOL}
              approxUSD={getUsdAmount(wrappedTokenAmount, prices.bitcoin?.usd)}
            />
            <SubmitButton
              disabled={
                // TODO: `parachainStatus` and `address` should be checked at upper levels
                parachainStatus !== ParachainStatus.Running || !address
              }
              pending={submitStatus === STATUSES.PENDING}
            >
              {t('confirm')}
            </SubmitButton>
          </form>
        </InterlayModalInnerWrapper>
      </InterlayModal>
      {submitStatus === STATUSES.REJECTED && submitError && (
        <ErrorModal
          open={!!submitError}
          onClose={() => {
            setSubmitStatus(STATUSES.IDLE);
            setSubmitError(null);
          }}
          title='Error'
          description={typeof submitError === 'string' ? submitError : submitError.message}
        />
      )}
      {submittedRequest && (
        <SubmittedIssueRequestModal
          open={!!submittedRequest}
          onClose={handleSubmittedRequestModalClose}
          request={submittedRequest}
        />
      )}
    </>
  );
}
Example #16
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
UpdateCollateralModal = ({
  open,
  onClose,
  collateralUpdateStatus,
  vaultAddress,
  hasLockedBTC
}: Props): JSX.Element => {
  const { bridgeLoaded, collateralTokenBalance } = useSelector((state: StoreType) => state.general);
  // Denoted in collateral token
  const currentTotalCollateralTokenAmount = useSelector((state: StoreType) => state.vault.collateral);

  const {
    register,
    handleSubmit,
    formState: { errors },
    watch
  } = useForm<UpdateCollateralFormData>({
    mode: 'onChange'
  });
  const strCollateralTokenAmount = watch(COLLATERAL_TOKEN_AMOUNT);

  const dispatch = useDispatch();
  const { t } = useTranslation();
  const focusRef = React.useRef(null);
  const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE);
  const handleError = useErrorHandler();

  const vaultId = window.bridge.api.createType(ACCOUNT_ID_TYPE_NAME, vaultAddress);

  const {
    isIdle: requiredCollateralTokenAmountIdle,
    isLoading: requiredCollateralTokenAmountLoading,
    data: requiredCollateralTokenAmount,
    error: requiredCollateralTokenAmountError
  } = useQuery<MonetaryAmount<Currency<CollateralUnit>, CollateralUnit>, Error>(
    [GENERIC_FETCHER, 'vaults', 'getRequiredCollateralForVault', vaultId, COLLATERAL_TOKEN],
    genericFetcher<MonetaryAmount<Currency<CollateralUnit>, CollateralUnit>>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(requiredCollateralTokenAmountError);

  const collateralTokenAmount = newMonetaryAmount(strCollateralTokenAmount || '0', COLLATERAL_TOKEN, true);
  let newCollateralTokenAmount: MonetaryAmount<Currency<CollateralUnit>, CollateralUnit>;
  let labelText;
  let collateralUpdateStatusText: string;
  if (collateralUpdateStatus === CollateralUpdateStatus.Deposit) {
    collateralUpdateStatusText = t('vault.deposit_collateral');
    newCollateralTokenAmount = currentTotalCollateralTokenAmount.add(collateralTokenAmount);
    labelText = 'Deposit Collateral';
  } else if (collateralUpdateStatus === CollateralUpdateStatus.Withdraw) {
    collateralUpdateStatusText = t('vault.withdraw_collateral');
    newCollateralTokenAmount = currentTotalCollateralTokenAmount.sub(collateralTokenAmount);
    labelText = 'Withdraw Collateral';
  } else {
    throw new Error('Something went wrong!');
  }

  const {
    isIdle: vaultCollateralizationIdle,
    isLoading: vaultCollateralizationLoading,
    data: vaultCollateralization,
    error: vaultCollateralizationError
  } = useQuery<Big, Error>(
    [
      GENERIC_FETCHER,
      'vaults',
      'getVaultCollateralization',
      vaultId,
      COLLATERAL_TOKEN_ID_LITERAL,
      newCollateralTokenAmount
    ],
    genericFetcher<Big>(),
    {
      enabled: bridgeLoaded && hasLockedBTC
    }
  );
  useErrorHandler(vaultCollateralizationError);

  const onSubmit = async (data: UpdateCollateralFormData) => {
    if (!bridgeLoaded) return;

    try {
      setSubmitStatus(STATUSES.PENDING);
      const collateralTokenAmount = newMonetaryAmount(data[COLLATERAL_TOKEN_AMOUNT], COLLATERAL_TOKEN, true);
      if (collateralUpdateStatus === CollateralUpdateStatus.Deposit) {
        await window.bridge.vaults.depositCollateral(collateralTokenAmount);
      } else if (collateralUpdateStatus === CollateralUpdateStatus.Withdraw) {
        await window.bridge.vaults.withdrawCollateral(collateralTokenAmount);
      } else {
        throw new Error('Something went wrong!');
      }

      const balanceLockedDOT = (await window.bridge.tokens.balance(COLLATERAL_TOKEN, vaultId)).reserved;
      dispatch(updateCollateralAction(balanceLockedDOT));

      if (vaultCollateralization === undefined) {
        dispatch(updateCollateralizationAction('∞'));
      } else {
        // The vault API returns collateralization as a regular number rather than a percentage
        const strVaultCollateralizationPercentage = vaultCollateralization.mul(100).toString();
        dispatch(updateCollateralizationAction(strVaultCollateralizationPercentage));
      }

      toast.success(t('vault.successfully_updated_collateral'));
      setSubmitStatus(STATUSES.RESOLVED);
      onClose();
    } catch (error) {
      toast.error(error.message);
      handleError(error);
      setSubmitStatus(STATUSES.REJECTED);
    }
  };

  const validateCollateralTokenAmount = (value: string): string | undefined => {
    const collateralTokenAmount = newMonetaryAmount(value || '0', COLLATERAL_TOKEN, true);

    // Collateral update only allowed if above required collateral
    if (collateralUpdateStatus === CollateralUpdateStatus.Withdraw && requiredCollateralTokenAmount) {
      const maxWithdrawableCollateralTokenAmount = currentTotalCollateralTokenAmount.sub(requiredCollateralTokenAmount);

      return collateralTokenAmount.gt(maxWithdrawableCollateralTokenAmount)
        ? t('vault.collateral_below_threshold')
        : undefined;
    }

    if (collateralTokenAmount.lte(newMonetaryAmount(0, COLLATERAL_TOKEN, true))) {
      return t('vault.collateral_higher_than_0');
    }

    if (collateralTokenAmount.toBig(collateralTokenAmount.currency.rawBase).lte(1)) {
      return 'Please enter an amount greater than 1 Planck';
    }

    if (collateralTokenAmount.gt(collateralTokenBalance)) {
      return t(`Must be less than ${COLLATERAL_TOKEN_SYMBOL} balance!`);
    }

    if (!bridgeLoaded) {
      return 'Bridge must be loaded!';
    }

    return undefined;
  };

  const renderSubmitButton = () => {
    const initializing =
      requiredCollateralTokenAmountIdle ||
      requiredCollateralTokenAmountLoading ||
      (vaultCollateralizationIdle && hasLockedBTC) ||
      vaultCollateralizationLoading;
    const buttonText = initializing ? 'Loading...' : collateralUpdateStatusText;

    return (
      <InterlayDefaultContainedButton
        type='submit'
        className={clsx('!flex', 'mx-auto')}
        disabled={initializing}
        pending={submitStatus === STATUSES.PENDING}
      >
        {buttonText}
      </InterlayDefaultContainedButton>
    );
  };

  const renderNewCollateralizationLabel = () => {
    if (vaultCollateralizationLoading) {
      // TODO: should use skeleton loaders
      return '-';
    }

    if (!hasLockedBTC) {
      return '∞';
    }

    // The vault API returns collateralization as a regular number rather than a percentage
    const strVaultCollateralizationPercentage = vaultCollateralization?.mul(100).toString();
    if (Number(strVaultCollateralizationPercentage) > 1000) {
      return 'more than 1000%';
    } else {
      return `${roundTwoDecimals(strVaultCollateralizationPercentage || '0')}%`;
    }
  };

  const getMinRequiredCollateralTokenAmount = () => {
    if (requiredCollateralTokenAmountIdle || requiredCollateralTokenAmountLoading) {
      return '-';
    }

    if (requiredCollateralTokenAmount === undefined) {
      throw new Error('Something went wrong');
    }
    return displayMonetaryAmount(requiredCollateralTokenAmount);
  };

  const getMaxWithdrawableCollateralTokenAmount = () => {
    if (requiredCollateralTokenAmountIdle || requiredCollateralTokenAmountLoading) {
      return '-';
    }

    if (requiredCollateralTokenAmount === undefined) {
      throw new Error('Something went wrong');
    }

    const maxWithdrawableCollateralTokenAmount = currentTotalCollateralTokenAmount.sub(requiredCollateralTokenAmount);
    return displayMonetaryAmount(maxWithdrawableCollateralTokenAmount);
  };

  return (
    <InterlayModal initialFocus={focusRef} open={open} onClose={onClose}>
      <InterlayModalInnerWrapper className={clsx('p-6', 'max-w-lg')}>
        <InterlayModalTitle as='h3' className={clsx('text-lg', 'font-medium', 'mb-6')}>
          {collateralUpdateStatusText}
        </InterlayModalTitle>
        <CloseIconButton ref={focusRef} onClick={onClose} />
        <form onSubmit={handleSubmit(onSubmit)} className='space-y-4'>
          <p>
            {t('vault.current_total_collateral', {
              currentCollateral: displayMonetaryAmount(currentTotalCollateralTokenAmount),
              collateralTokenSymbol: COLLATERAL_TOKEN_SYMBOL
            })}
          </p>
          <p>
            {t('vault.minimum_required_collateral', {
              currentCollateral: getMinRequiredCollateralTokenAmount(),
              collateralTokenSymbol: COLLATERAL_TOKEN_SYMBOL
            })}
          </p>
          <p>
            {t('vault.maximum_withdrawable_collateral', {
              currentCollateral: getMaxWithdrawableCollateralTokenAmount(),
              collateralTokenSymbol: COLLATERAL_TOKEN_SYMBOL
            })}
          </p>
          <div className='space-y-1'>
            <label htmlFor={COLLATERAL_TOKEN_AMOUNT} className='text-sm'>
              {labelText}
            </label>
            <NumberInput
              id={COLLATERAL_TOKEN_AMOUNT}
              name={COLLATERAL_TOKEN_AMOUNT}
              min={0}
              ref={register({
                required: {
                  value: true,
                  message: t('vault.collateral_is_required')
                },
                validate: (value) => validateCollateralTokenAmount(value)
              })}
            />
            <ErrorMessage className='h-9'>{errors[COLLATERAL_TOKEN_AMOUNT]?.message}</ErrorMessage>
          </div>
          <p>
            {t('vault.new_collateralization')}
            &nbsp;
            {renderNewCollateralizationLabel()}
          </p>
          {renderSubmitButton()}
        </form>
      </InterlayModalInnerWrapper>
    </InterlayModal>
  );
}
Example #17
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
VaultIssueRequestsTable = ({ vaultAddress }: Props): JSX.Element | null => {
  const queryParams = useQueryParams();
  const selectedPage = Number(queryParams.get(QUERY_PARAMETERS.PAGE)) || 1;
  const selectedPageIndex = selectedPage - 1;
  const updateQueryParameters = useUpdateQueryParameters();
  const { bridgeLoaded } = useSelector((state: StoreType) => state.general);
  const { t } = useTranslation();

  const {
    isIdle: btcConfirmationsIdle,
    isLoading: btcConfirmationsLoading,
    data: btcConfirmations,
    error: btcConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableBitcoinConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(btcConfirmationsError);

  const {
    isIdle: latestParachainActiveBlockIdle,
    isLoading: latestParachainActiveBlockLoading,
    data: latestParachainActiveBlock,
    error: latestParachainActiveBlockError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'system', 'getCurrentActiveBlockNumber'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(latestParachainActiveBlockError);

  const {
    isIdle: parachainConfirmationsIdle,
    isLoading: parachainConfirmationsLoading,
    data: parachainConfirmations,
    error: parachainConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableParachainConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(parachainConfirmationsError);

  const {
    isIdle: issueRequestsTotalCountIdle,
    isLoading: issueRequestsTotalCountLoading,
    data: issueRequestsTotalCount,
    error: issueRequestsTotalCountError
    // TODO: should type properly (`Relay`)
  } = useQuery<GraphqlReturn<any>, Error>(
    [GRAPHQL_FETCHER, issueCountQuery(`vault: {accountId_eq: "${vaultAddress}"}`)],
    graphqlFetcher<GraphqlReturn<any>>()
  );
  useErrorHandler(issueRequestsTotalCountError);

  const {
    isIdle: issueRequestsIdle,
    isLoading: issueRequestsLoading,
    data: issueRequests,
    error: issueRequestsError
    // TODO: should type properly (`Relay`)
  } = useQuery<any, Error>(
    [
      ISSUE_FETCHER,
      selectedPageIndex * TABLE_PAGE_LIMIT, // offset
      TABLE_PAGE_LIMIT, // limit
      `vault: {accountId_eq: "${vaultAddress}"}` // `WHERE` condition
    ],
    issueFetcher
  );
  useErrorHandler(issueRequestsError);

  const columns = React.useMemo(
    () => [
      {
        Header: t('id'),
        accessor: 'id',
        classNames: ['text-center']
      },
      {
        Header: t('date_created'),
        classNames: ['text-left'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          return <>{formatDateTimePrecise(new Date(issue.request.timestamp))}</>;
        }
      },
      {
        Header: t('vault.creation_block'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          return <>{issue.request.height.active}</>;
        }
      },
      {
        Header: t('last_update'),
        classNames: ['text-left'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          let date;
          if (issue.execution) {
            date = issue.execution.timestamp;
          } else if (issue.cancellation) {
            date = issue.cancellation.timestamp;
          } else {
            date = issue.request.timestamp;
          }

          return <>{formatDateTimePrecise(new Date(date))}</>;
        }
      },
      {
        Header: t('last_update_block'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          let height;
          if (issue.execution) {
            height = issue.execution.height.active;
          } else if (issue.cancellation) {
            height = issue.cancellation.height.active;
          } else {
            height = issue.request.height.active;
          }

          return <>{height}</>;
        }
      },
      {
        Header: t('user'),
        accessor: 'userParachainAddress',
        classNames: ['text-center'],
        Cell: function FormattedCell({ value }: { value: string }) {
          return <>{shortAddress(value)}</>;
        }
      },
      {
        Header: t('issue_page.amount'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          let wrappedTokenAmount;
          if (issue.execution) {
            wrappedTokenAmount = issue.execution.amountWrapped;
          } else {
            wrappedTokenAmount = issue.request.amountWrapped;
          }

          return <>{displayMonetaryAmount(wrappedTokenAmount)}</>;
        }
      },
      {
        Header: t('griefing_collateral'),
        accessor: 'griefingCollateral',
        classNames: ['text-right'],
        Cell: function FormattedCell({ value }: { value: any }) {
          return <>{displayMonetaryAmount(value)}</>;
        }
      },
      {
        Header: t('issue_page.vault_btc_address'),
        accessor: 'vaultBackingAddress',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: string }) {
          return <ExternalLink href={`${BTC_EXPLORER_ADDRESS_API}${value}`}>{shortAddress(value)}</ExternalLink>;
        }
      },
      {
        Header: t('status'),
        accessor: 'status',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: IssueStatus }) {
          return (
            <StatusCell
              status={{
                completed: value === IssueStatus.Completed,
                cancelled: value === IssueStatus.Cancelled,
                isExpired: value === IssueStatus.Expired,
                reimbursed: false
              }}
            />
          );
        }
      }
    ],
    [t]
  );

  const data =
    issueRequests === undefined ||
    btcConfirmations === undefined ||
    parachainConfirmations === undefined ||
    latestParachainActiveBlock === undefined
      ? []
      : issueRequests.map(
          // TODO: should type properly (`Relay`)
          (issueRequest: any) =>
            getIssueWithStatus(issueRequest, btcConfirmations, parachainConfirmations, latestParachainActiveBlock)
        );

  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
    columns,
    data
  });

  if (
    btcConfirmationsIdle ||
    btcConfirmationsLoading ||
    parachainConfirmationsIdle ||
    parachainConfirmationsLoading ||
    latestParachainActiveBlockIdle ||
    latestParachainActiveBlockLoading ||
    issueRequestsTotalCountIdle ||
    issueRequestsTotalCountLoading ||
    issueRequestsIdle ||
    issueRequestsLoading
  ) {
    return <PrimaryColorEllipsisLoader />;
  }
  if (issueRequestsTotalCount === undefined) {
    throw new Error('Something went wrong!');
  }

  const handlePageChange = ({ selected: newSelectedPageIndex }: { selected: number }) => {
    updateQueryParameters({
      [QUERY_PARAMETERS.PAGE]: (newSelectedPageIndex + 1).toString()
    });
  };

  const totalSuccessfulIssueCount = issueRequestsTotalCount.data.issuesConnection.totalCount || 0;
  const pageCount = Math.ceil(totalSuccessfulIssueCount / TABLE_PAGE_LIMIT);

  return (
    <InterlayTableContainer className='space-y-6'>
      <SectionTitle>{t('issue_requests')}</SectionTitle>
      <InterlayTable {...getTableProps()}>
        <InterlayThead>
          {headerGroups.map((headerGroup: any) => (
            // eslint-disable-next-line react/jsx-key
            <InterlayTr {...headerGroup.getHeaderGroupProps()}>
              {headerGroup.headers.map((column: any) => (
                // eslint-disable-next-line react/jsx-key
                <InterlayTh
                  {...column.getHeaderProps([
                    {
                      className: clsx(column.classNames),
                      style: column.style
                    }
                  ])}
                >
                  {column.render('Header')}
                </InterlayTh>
              ))}
            </InterlayTr>
          ))}
        </InterlayThead>
        <InterlayTbody {...getTableBodyProps()}>
          {rows.map((row: any) => {
            prepareRow(row);

            return (
              // eslint-disable-next-line react/jsx-key
              <InterlayTr {...row.getRowProps()}>
                {row.cells.map((cell: any) => {
                  return (
                    // eslint-disable-next-line react/jsx-key
                    <InterlayTd
                      {...cell.getCellProps([
                        {
                          className: clsx(cell.column.classNames),
                          style: cell.column.style
                        }
                      ])}
                    >
                      {cell.render('Cell')}
                    </InterlayTd>
                  );
                })}
              </InterlayTr>
            );
          })}
        </InterlayTbody>
      </InterlayTable>
      {pageCount > 0 && (
        <div className={clsx('flex', 'justify-end')}>
          <InterlayPagination
            pageCount={pageCount}
            marginPagesDisplayed={2}
            pageRangeDisplayed={5}
            onPageChange={handlePageChange}
            forcePage={selectedPageIndex}
          />
        </div>
      )}
    </InterlayTableContainer>
  );
}
Example #18
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
VaultRedeemRequestsTable = ({ vaultAddress }: Props): JSX.Element | null => {
  const queryParams = useQueryParams();
  const { bridgeLoaded } = useSelector((state: StoreType) => state.general);
  const selectedPage = Number(queryParams.get(QUERY_PARAMETERS.PAGE)) || 1;
  const selectedPageIndex = selectedPage - 1;
  const updateQueryParameters = useUpdateQueryParameters();
  const { t } = useTranslation();

  const {
    isIdle: btcConfirmationsIdle,
    isLoading: btcConfirmationsLoading,
    data: btcConfirmations,
    error: btcConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableBitcoinConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(btcConfirmationsError);

  const {
    isIdle: latestParachainActiveBlockIdle,
    isLoading: latestParachainActiveBlockLoading,
    data: latestParachainActiveBlock,
    error: latestParachainActiveBlockError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'system', 'getCurrentActiveBlockNumber'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(latestParachainActiveBlockError);

  const {
    isIdle: parachainConfirmationsIdle,
    isLoading: parachainConfirmationsLoading,
    data: parachainConfirmations,
    error: parachainConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableParachainConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(parachainConfirmationsError);

  const {
    isIdle: redeemRequestsTotalCountIdle,
    isLoading: redeemRequestsTotalCountLoading,
    data: redeemRequestsTotalCount,
    error: redeemRequestsTotalCountError
    // TODO: should type properly (`Relay`)
  } = useQuery<GraphqlReturn<any>, Error>(
    [GRAPHQL_FETCHER, redeemCountQuery(`vault: {accountId_eq: "${vaultAddress}"}`)],
    graphqlFetcher<GraphqlReturn<any>>()
  );
  useErrorHandler(redeemRequestsTotalCountError);

  const {
    isIdle: redeemRequestsIdle,
    isLoading: redeemRequestsLoading,
    data: redeemRequests,
    error: redeemRequestsError
    // TODO: should type properly (`Relay`)
  } = useQuery<any, Error>(
    [
      REDEEM_FETCHER,
      selectedPageIndex * TABLE_PAGE_LIMIT, // offset
      TABLE_PAGE_LIMIT, // limit
      `vault: {accountId_eq: "${vaultAddress}"}` // `WHERE` condition
    ],
    redeemFetcher
  );
  useErrorHandler(redeemRequestsError);

  const columns = React.useMemo(
    () => [
      {
        Header: t('id'),
        accessor: 'id',
        classNames: ['text-center']
      },
      {
        Header: t('date_created'),
        classNames: ['text-left'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          return <>{formatDateTimePrecise(new Date(redeem.request.timestamp))}</>;
        }
      },
      {
        Header: t('vault.creation_block'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          return <>{redeem.request.height.active}</>;
        }
      },
      {
        Header: t('last_update'),
        classNames: ['text-left'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          let date;
          if (redeem.execution) {
            date = redeem.execution.timestamp;
          } else if (redeem.cancellation) {
            date = redeem.cancellation.timestamp;
          } else {
            date = redeem.request.timestamp;
          }

          return <>{formatDateTimePrecise(new Date(date))}</>;
        }
      },
      {
        Header: t('last_update_block'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          let height;
          if (issue.execution) {
            height = issue.execution.height.active;
          } else if (issue.cancellation) {
            height = issue.cancellation.height.active;
          } else {
            height = issue.request.height.active;
          }

          return <>{height}</>;
        }
      },
      {
        Header: t('user'),
        accessor: 'userParachainAddress',
        classNames: ['text-center'],
        Cell: function FormattedCell({ value }: { value: string }) {
          return <>{shortAddress(value)}</>;
        }
      },
      {
        Header: t('issue_page.amount'),
        accessor: 'amountBTC',
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          return <>{displayMonetaryAmount(redeem.request.requestedAmountBacking)}</>;
        }
      },
      {
        Header: t('redeem_page.btc_destination_address'),
        accessor: 'userBackingAddress',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: string }) {
          return <ExternalLink href={`${BTC_EXPLORER_ADDRESS_API}${value}`}>{shortAddress(value)}</ExternalLink>;
        }
      },
      {
        Header: t('issue_page.btc_transaction'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeemRequest } }: any) {
          return (
            <>
              {redeemRequest.status === RedeemStatus.Expired ||
              redeemRequest.status === RedeemStatus.Retried ||
              redeemRequest.status === RedeemStatus.Reimbursed ? (
                t('redeem_page.failed')
              ) : (
                <>
                  {redeemRequest.backingPayment.btcTxId ? (
                    <ExternalLink
                      href={`${BTC_EXPLORER_TRANSACTION_API}${redeemRequest.backingPayment.btcTxId}`}
                      onClick={(event) => {
                        event.stopPropagation();
                      }}
                    >
                      {shortTxId(redeemRequest.backingPayment.btcTxId)}
                    </ExternalLink>
                  ) : (
                    `${t('pending')}...`
                  )}
                </>
              )}
            </>
          );
        }
      },
      {
        Header: t('issue_page.confirmations'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          const value = redeem.backingPayment.confirmations;
          return <>{value === undefined ? t('not_applicable') : Math.max(value, 0)}</>;
        }
      },
      {
        Header: t('status'),
        accessor: 'status',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: RedeemStatus }) {
          return (
            <StatusCell
              status={{
                completed: value === RedeemStatus.Completed,
                cancelled: value === RedeemStatus.Retried,
                isExpired: value === RedeemStatus.Expired,
                reimbursed: value === RedeemStatus.Reimbursed
              }}
            />
          );
        }
      }
    ],
    [t]
  );

  const data =
    redeemRequests === undefined ||
    btcConfirmations === undefined ||
    parachainConfirmations === undefined ||
    latestParachainActiveBlock === undefined
      ? []
      : redeemRequests.map(
          // TODO: should type properly (`Relay`)
          (redeemRequest: any) =>
            getRedeemWithStatus(redeemRequest, btcConfirmations, parachainConfirmations, latestParachainActiveBlock)
        );

  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
    columns,
    data
  });

  if (
    btcConfirmationsIdle ||
    btcConfirmationsLoading ||
    parachainConfirmationsIdle ||
    parachainConfirmationsLoading ||
    latestParachainActiveBlockIdle ||
    latestParachainActiveBlockLoading ||
    redeemRequestsTotalCountIdle ||
    redeemRequestsTotalCountLoading ||
    redeemRequestsIdle ||
    redeemRequestsLoading
  ) {
    return <PrimaryColorEllipsisLoader />;
  }
  if (redeemRequestsTotalCount === undefined) {
    throw new Error('Something went wrong!');
  }

  const handlePageChange = ({ selected: newSelectedPageIndex }: { selected: number }) => {
    updateQueryParameters({
      [QUERY_PARAMETERS.PAGE]: (newSelectedPageIndex + 1).toString()
    });
  };

  const totalSuccessfulRedeemCount = redeemRequestsTotalCount.data.redeemsConnection.totalCount || 0;
  const pageCount = Math.ceil(totalSuccessfulRedeemCount / TABLE_PAGE_LIMIT);

  return (
    <InterlayTableContainer className='space-y-6'>
      <SectionTitle>{t('redeem_requests')}</SectionTitle>
      <InterlayTable {...getTableProps()}>
        <InterlayThead>
          {headerGroups.map((headerGroup: any) => (
            // eslint-disable-next-line react/jsx-key
            <InterlayTr {...headerGroup.getHeaderGroupProps()}>
              {headerGroup.headers.map((column: any) => (
                // eslint-disable-next-line react/jsx-key
                <InterlayTh
                  {...column.getHeaderProps([
                    {
                      className: clsx(column.classNames),
                      style: column.style
                    }
                  ])}
                >
                  {column.render('Header')}
                </InterlayTh>
              ))}
            </InterlayTr>
          ))}
        </InterlayThead>
        <InterlayTbody {...getTableBodyProps()}>
          {rows.map((row: any) => {
            prepareRow(row);

            return (
              // eslint-disable-next-line react/jsx-key
              <InterlayTr {...row.getRowProps()}>
                {row.cells.map((cell: any) => {
                  return (
                    // eslint-disable-next-line react/jsx-key
                    <InterlayTd
                      {...cell.getCellProps([
                        {
                          className: clsx(cell.column.classNames),
                          style: cell.column.style
                        }
                      ])}
                    >
                      {cell.render('Cell')}
                    </InterlayTd>
                  );
                })}
              </InterlayTr>
            );
          })}
        </InterlayTbody>
      </InterlayTable>
      {pageCount > 0 && (
        <div className={clsx('flex', 'justify-end')}>
          <InterlayPagination
            pageCount={pageCount}
            marginPagesDisplayed={2}
            pageRangeDisplayed={5}
            onPageChange={handlePageChange}
            forcePage={selectedPageIndex}
          />
        </div>
      )}
    </InterlayTableContainer>
  );
}
Example #19
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
VaultStatusStatPanel = ({ vaultAccountId }: Props): JSX.Element => {
  const { t } = useTranslation();
  const { bridgeLoaded } = useSelector((state: StoreType) => state.general);

  const { isIdle: vaultExtIdle, isLoading: vaultExtLoading, data: vaultExt, error: vaultExtError } = useQuery<
    VaultExt<BitcoinUnit>,
    Error
  >(
    [GENERIC_FETCHER, 'vaults', 'get', vaultAccountId, COLLATERAL_TOKEN_ID_LITERAL],
    genericFetcher<VaultExt<BitcoinUnit>>(),
    {
      enabled: !!bridgeLoaded && !!vaultAccountId
    }
  );
  useErrorHandler(vaultExtError);

  const {
    isIdle: currentActiveBlockNumberIdle,
    isLoading: currentActiveBlockNumberLoading,
    data: currentActiveBlockNumber,
    error: currentActiveBlockNumberError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'system', 'getCurrentActiveBlockNumber'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(currentActiveBlockNumberError);

  const {
    isIdle: liquidationCollateralThresholdIdle,
    isLoading: liquidationCollateralThresholdLoading,
    data: liquidationCollateralThreshold,
    error: liquidationCollateralThresholdError
  } = useQuery<Big, Error>(
    [GENERIC_FETCHER, 'vaults', 'getLiquidationCollateralThreshold', COLLATERAL_TOKEN],
    genericFetcher<Big>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(liquidationCollateralThresholdError);

  const {
    isIdle: secureCollateralThresholdIdle,
    isLoading: secureCollateralThresholdLoading,
    data: secureCollateralThreshold,
    error: secureCollateralThresholdError
  } = useQuery<Big, Error>(
    [GENERIC_FETCHER, 'vaults', 'getSecureCollateralThreshold', COLLATERAL_TOKEN],
    genericFetcher<Big>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(secureCollateralThresholdError);

  const {
    isIdle: btcToCollateralTokenRateIdle,
    isLoading: btcToCollateralTokenRateLoading,
    data: btcToCollateralTokenRate,
    error: btcToCollateralTokenRateError
  } = useQuery<BTCToCollateralTokenRate, Error>(
    [GENERIC_FETCHER, 'oracle', 'getExchangeRate', COLLATERAL_TOKEN],
    genericFetcher<BTCToCollateralTokenRate>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(btcToCollateralTokenRateError);

  let statusLabel: string;
  if (
    vaultExtIdle ||
    vaultExtLoading ||
    currentActiveBlockNumberIdle ||
    currentActiveBlockNumberLoading ||
    liquidationCollateralThresholdIdle ||
    liquidationCollateralThresholdLoading ||
    secureCollateralThresholdIdle ||
    secureCollateralThresholdLoading ||
    btcToCollateralTokenRateIdle ||
    btcToCollateralTokenRateLoading
  ) {
    statusLabel = '-';
  } else {
    if (
      vaultExt === undefined ||
      currentActiveBlockNumber === undefined ||
      liquidationCollateralThreshold === undefined ||
      secureCollateralThreshold === undefined ||
      btcToCollateralTokenRate === undefined
    ) {
      throw new Error('Something went wrong!');
    }

    statusLabel = getVaultStatusLabel(
      vaultExt,
      currentActiveBlockNumber,
      liquidationCollateralThreshold,
      secureCollateralThreshold,
      btcToCollateralTokenRate,
      t
    );
  }

  return <StatPanel label={t('vault.status')} value={statusLabel} />;
}
Example #20
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
Vault = (): JSX.Element => {
  const [collateralUpdateStatus, setCollateralUpdateStatus] = React.useState(CollateralUpdateStatus.Close);
  const [requestReplaceModalOpen, setRequestReplaceModalOpen] = React.useState(false);
  const [requestRedeemModalOpen, setRequestRedeemModalOpen] = React.useState(false);
  const [requestIssueModalOpen, setRequestIssueModalOpen] = React.useState(false);
  const [capacity, setCapacity] = React.useState(BitcoinAmount.zero);
  const [feesEarnedInterBTC, setFeesEarnedInterBTC] = React.useState(BitcoinAmount.zero);

  const { vaultClientLoaded, bridgeLoaded, address } = useSelector((state: StoreType) => state.general);
  const { collateralization, collateral, lockedBTC, apy } = useSelector((state: StoreType) => state.vault);

  const dispatch = useDispatch();
  const { t } = useTranslation();

  const {
    [URL_PARAMETERS.VAULT.ACCOUNT]: selectedVaultAccountAddress,
    [URL_PARAMETERS.VAULT.COLLATERAL]: vaultCollateral
  } = useParams<Record<string, string>>();

  const handleUpdateCollateralModalClose = () => {
    setCollateralUpdateStatus(CollateralUpdateStatus.Close);
  };
  const handleDepositCollateralModalOpen = () => {
    setCollateralUpdateStatus(CollateralUpdateStatus.Deposit);
  };
  const handleWithdrawCollateralModalOpen = () => {
    setCollateralUpdateStatus(CollateralUpdateStatus.Withdraw);
  };
  const handleRequestReplaceModalClose = () => {
    setRequestReplaceModalOpen(false);
  };
  const handleRequestReplaceModalOpen = () => {
    setRequestReplaceModalOpen(true);
  };
  const handleRequestRedeemModalClose = () => {
    setRequestRedeemModalOpen(false);
  };
  const handleRequestRedeemModalOpen = () => {
    setRequestRedeemModalOpen(true);
  };
  const handleRequestIssueModalClose = () => {
    setRequestIssueModalOpen(false);
  };
  const handleRequestIssueModalOpen = () => {
    setRequestIssueModalOpen(true);
  };

  const vaultAccountId = React.useMemo(() => {
    // eslint-disable-next-line max-len
    // TODO: should correct loading procedure according to https://kentcdodds.com/blog/application-state-management-with-react
    if (!bridgeLoaded) return;

    return newAccountId(window.bridge.api, selectedVaultAccountAddress);
  }, [bridgeLoaded, selectedVaultAccountAddress]);

  const vaultCollateralIdLiteral = React.useMemo(
    () => tickerToCurrencyIdLiteral(vaultCollateral) as CollateralIdLiteral,
    [vaultCollateral]
  );

  React.useEffect(() => {
    (async () => {
      if (!bridgeLoaded) return;
      if (!vaultAccountId) return;
      if (!vaultCollateralIdLiteral) return;

      try {
        // TODO: should update using `react-query`
        const [feesPolkaBTC, lockedAmountBTC, collateralization, apyScore, issuableAmount] = await Promise.allSettled([
          window.bridge.vaults.getWrappedReward(vaultAccountId, vaultCollateralIdLiteral, WRAPPED_TOKEN_ID_LITERAL),
          window.bridge.vaults.getIssuedAmount(vaultAccountId, vaultCollateralIdLiteral),
          window.bridge.vaults.getVaultCollateralization(vaultAccountId, vaultCollateralIdLiteral),
          window.bridge.vaults.getAPY(vaultAccountId, vaultCollateralIdLiteral),
          window.bridge.issue.getVaultIssuableAmount(vaultAccountId, vaultCollateralIdLiteral)
        ]);

        if (feesPolkaBTC.status === 'fulfilled') {
          setFeesEarnedInterBTC(feesPolkaBTC.value);
        }

        if (lockedAmountBTC.status === 'fulfilled') {
          dispatch(updateLockedBTCAction(lockedAmountBTC.value));
        }

        if (collateralization.status === 'fulfilled') {
          dispatch(updateCollateralizationAction(collateralization.value?.mul(100).toString()));
        }

        if (apyScore.status === 'fulfilled') {
          dispatch(updateAPYAction(apyScore.value.toString()));
        }

        if (issuableAmount.status === 'fulfilled') {
          setCapacity(issuableAmount.value);
        }
      } catch (error) {
        console.log('[Vault React.useEffect] error.message => ', error.message);
      }
    })();
  }, [vaultCollateralIdLiteral, bridgeLoaded, dispatch, vaultAccountId]);

  const { data: governanceTokenReward, error: governanceTokenRewardError } = useQuery<
    GovernanceTokenMonetaryAmount,
    Error
  >(
    [
      GENERIC_FETCHER,
      'vaults',
      'getGovernanceReward',
      vaultAccountId,
      vaultCollateralIdLiteral,
      GOVERNANCE_TOKEN_SYMBOL
    ],
    genericFetcher<GovernanceTokenMonetaryAmount>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(governanceTokenRewardError);

  const { data: vaultExt, error: vaultExtError } = useQuery<VaultExt<BitcoinUnit>, Error>(
    [GENERIC_FETCHER, 'vaults', 'get', vaultAccountId, vaultCollateralIdLiteral],
    genericFetcher<VaultExt<BitcoinUnit>>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(vaultExtError);
  React.useEffect(() => {
    if (vaultExt === undefined) return;
    if (!dispatch) return;

    dispatch(updateCollateralAction(vaultExt.backingCollateral));
  }, [vaultExt, dispatch]);

  const vaultItems = React.useMemo(() => {
    const governanceRewardLabel =
      governanceTokenReward === undefined ? '-' : displayMonetaryAmount(governanceTokenReward);

    return [
      {
        title: t('collateralization'),
        value:
          collateralization === '∞' ? collateralization : `${safeRoundTwoDecimals(collateralization?.toString(), '∞')}%`
      },
      {
        title: t('vault.fees_earned_interbtc', {
          wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL
        }),
        value: displayMonetaryAmount(feesEarnedInterBTC)
      },
      {
        title: t('vault.locked_dot', {
          // TODO: when updating kint and adding the vault collateral as config,
          // this will need to be changed to use the symbol not the id literal.
          collateralTokenSymbol: vaultCollateralIdLiteral
        }),
        value: displayMonetaryAmount(collateral)
      },
      {
        title: t('locked_btc'),
        value: displayMonetaryAmount(lockedBTC),
        color: 'text-interlayCalifornia-700'
      },
      {
        title: t('vault.remaining_capacity', {
          wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL
        }),
        value: displayMonetaryAmount(capacity)
      },
      {
        title: t('apy'),
        value: `≈${safeRoundTwoDecimals(apy)}%`
      },
      {
        title: t('vault.rewards_earned_governance_token_symbol', {
          governanceTokenSymbol: GOVERNANCE_TOKEN_SYMBOL
        }),
        value: governanceRewardLabel
      }
    ];
  }, [
    governanceTokenReward,
    t,
    collateralization,
    feesEarnedInterBTC,
    vaultCollateralIdLiteral,
    collateral,
    lockedBTC,
    capacity,
    apy
  ]);

  const hasLockedBTC = lockedBTC.gt(BitcoinAmount.zero);

  const isIssuingDisabled = vaultExt?.status !== VaultStatusExt.Active || capacity.lte(BitcoinAmount.zero);

  const issueButtonTooltip = (() => {
    if (vaultExt?.status !== VaultStatusExt.Active) {
      return t('vault.tooltip_issuing_deactivated');
    }
    if (capacity.lte(BitcoinAmount.zero)) {
      return t('vault.tooltip_issue_capacity_zero');
    }
    return t('vault.issue_vault');
  })();

  return (
    <>
      <MainContainer className='fade-in-animation'>
        <VaultsHeader title={t('vault.vault_dashboard')} accountAddress={selectedVaultAccountAddress} />
        <div className='space-y-6'>
          <SectionTitle>Vault Stats</SectionTitle>
          <div className={clsx('grid', 'md:grid-cols-3', 'lg:grid-cols-4', 'gap-5', '2xl:gap-6')}>
            {vaultItems.map((item) => (
              <StatPanel key={item.title} label={item.title} value={item.value} />
            ))}
            <VaultStatusStatPanel vaultAccountId={vaultAccountId} />
          </div>
        </div>
        {/* Check interaction with the vault */}
        {vaultClientLoaded && address === selectedVaultAccountAddress && (
          <div className={clsx('grid', hasLockedBTC ? 'grid-cols-6' : 'grid-cols-4', 'gap-5')}>
            <InterlayDenimOrKintsugiSupernovaContainedButton onClick={handleDepositCollateralModalOpen}>
              {t('vault.deposit_collateral')}
            </InterlayDenimOrKintsugiSupernovaContainedButton>
            <InterlayDefaultContainedButton onClick={handleWithdrawCollateralModalOpen}>
              {t('vault.withdraw_collateral')}
            </InterlayDefaultContainedButton>
            <ClaimRewardsButton vaultAccountId={vaultAccountId} />
            <InterlayTooltip label={issueButtonTooltip}>
              {/* Button wrapped in div to enable tooltip on disabled button. */}
              <div className='grid'>
                <InterlayCaliforniaContainedButton onClick={handleRequestIssueModalOpen} disabled={isIssuingDisabled}>
                  {t('vault.issue_vault')}
                </InterlayCaliforniaContainedButton>
              </div>
            </InterlayTooltip>
            {hasLockedBTC && (
              <InterlayCaliforniaContainedButton onClick={handleRequestReplaceModalOpen}>
                {t('vault.replace_vault')}
              </InterlayCaliforniaContainedButton>
            )}
            {hasLockedBTC && (
              <InterlayCaliforniaContainedButton onClick={handleRequestRedeemModalOpen}>
                {t('vault.redeem_vault')}
              </InterlayCaliforniaContainedButton>
            )}
          </div>
        )}
        <VaultIssueRequestsTable vaultAddress={selectedVaultAccountAddress} />
        <VaultRedeemRequestsTable vaultAddress={selectedVaultAccountAddress} />
        <ReplaceTable vaultAddress={selectedVaultAccountAddress} />
      </MainContainer>
      {collateralUpdateStatus !== CollateralUpdateStatus.Close && (
        <UpdateCollateralModal
          open={
            collateralUpdateStatus === CollateralUpdateStatus.Deposit ||
            collateralUpdateStatus === CollateralUpdateStatus.Withdraw
          }
          onClose={handleUpdateCollateralModalClose}
          collateralUpdateStatus={collateralUpdateStatus}
          vaultAddress={selectedVaultAccountAddress}
          hasLockedBTC={hasLockedBTC}
        />
      )}
      <RequestReplacementModal
        onClose={handleRequestReplaceModalClose}
        open={requestReplaceModalOpen}
        vaultAddress={selectedVaultAccountAddress}
      />
      <RequestRedeemModal
        onClose={handleRequestRedeemModalClose}
        open={requestRedeemModalOpen}
        vaultAddress={selectedVaultAccountAddress}
      />
      <RequestIssueModal
        onClose={handleRequestIssueModalClose}
        open={requestIssueModalOpen}
        vaultAddress={selectedVaultAccountAddress}
      />
    </>
  );
}
Example #21
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
ReceivedIssueRequest = ({ request }: Props): JSX.Element => {
  const { t } = useTranslation();
  const { bridgeLoaded } = useSelector((state: StoreType) => state.general);

  const {
    isIdle: stableBitcoinConfirmationsIdle,
    isLoading: stableBitcoinConfirmationsLoading,
    data: stableBitcoinConfirmations = 1, // TODO: double-check
    error: stableBitcoinConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableBitcoinConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(stableBitcoinConfirmationsError);

  const {
    isIdle: stableParachainConfirmationsIdle,
    isLoading: stableParachainConfirmationsLoading,
    data: stableParachainConfirmations = 100, // TODO: double-check
    error: stableParachainConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableParachainConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(stableParachainConfirmationsError);

  const {
    isIdle: parachainHeightIdle,
    isLoading: parachainHeightLoading,
    data: parachainHeight = 0, // TODO: double-check
    error: parachainHeightError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'system', 'getCurrentActiveBlockNumber'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(parachainHeightError);

  // TODO: should use skeleton loaders
  if (stableBitcoinConfirmationsIdle || stableBitcoinConfirmationsLoading) {
    return <>Loading...</>;
  }
  if (stableParachainConfirmationsIdle || stableParachainConfirmationsLoading) {
    return <>Loading...</>;
  }
  if (parachainHeightIdle || parachainHeightLoading) {
    return <>Loading...</>;
  }

  const requestConfirmations = request.backingPayment.includedAtParachainActiveBlock
    ? parachainHeight - request.backingPayment.includedAtParachainActiveBlock
    : 0;

  return (
    <RequestWrapper>
      <h2 className={clsx('text-3xl', 'font-medium')}>{t('received')}</h2>
      <Ring48 className='ring-interlayConifer'>
        <Ring48Title>{t('issue_page.waiting_for')}</Ring48Title>
        <Ring48Title>{t('confirmations')}</Ring48Title>
        <Ring48Value className='text-interlayConifer'>
          {`${request.backingPayment.confirmations ?? 0}/${stableBitcoinConfirmations}`}
        </Ring48Value>
        <Ring48Value className='text-interlayConifer'>
          {`${requestConfirmations}/${stableParachainConfirmations}`}
        </Ring48Value>
      </Ring48>
      <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 }
          )}
        >
          {t('issue_page.btc_transaction')}:
        </span>
        <span className='font-medium'>{shortAddress(request.backingPayment.btcTxId || '')}</span>
      </p>
      <ExternalLink className='text-sm' href={`${BTC_EXPLORER_TRANSACTION_API}${request.backingPayment.btcTxId}`}>
        {t('issue_page.view_on_block_explorer')}
      </ExternalLink>
    </RequestWrapper>
  );
}
Example #22
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
Staking = (): JSX.Element => {
  const [blockLockTimeExtension, setBlockLockTimeExtension] = React.useState<number>(0);

  const dispatch = useDispatch();
  const { t } = useTranslation();

  const { governanceTokenBalance, bridgeLoaded, address, prices } = useSelector((state: StoreType) => state.general);

  const {
    register,
    handleSubmit,
    watch,
    reset,
    formState: { errors },
    trigger
  } = useForm<StakingFormData>({
    mode: 'onChange', // 'onBlur'
    defaultValues: {
      [LOCKING_AMOUNT]: '0',
      [LOCK_TIME]: '0'
    }
  });
  const lockingAmount = watch(LOCKING_AMOUNT) || '0';
  const lockTime = watch(LOCK_TIME) || '0';

  const {
    isIdle: currentBlockNumberIdle,
    isLoading: currentBlockNumberLoading,
    data: currentBlockNumber,
    error: currentBlockNumberError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'system', 'getCurrentBlockNumber'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(currentBlockNumberError);

  const {
    isIdle: voteGovernanceTokenBalanceIdle,
    isLoading: voteGovernanceTokenBalanceLoading,
    data: voteGovernanceTokenBalance,
    error: voteGovernanceTokenBalanceError,
    refetch: voteGovernanceTokenBalanceRefetch
  } = useQuery<VoteGovernanceTokenMonetaryAmount, Error>(
    [GENERIC_FETCHER, 'escrow', 'votingBalance', address],
    genericFetcher<VoteGovernanceTokenMonetaryAmount>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(voteGovernanceTokenBalanceError);

  // My currently claimable rewards
  const {
    isIdle: claimableRewardAmountIdle,
    isLoading: claimableRewardAmountLoading,
    data: claimableRewardAmount,
    error: claimableRewardAmountError,
    refetch: claimableRewardAmountRefetch
  } = useQuery<GovernanceTokenMonetaryAmount, Error>(
    [GENERIC_FETCHER, 'escrow', 'getRewards', address],
    genericFetcher<GovernanceTokenMonetaryAmount>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(claimableRewardAmountError);

  // Projected governance token rewards
  const {
    isIdle: projectedRewardAmountAndAPYIdle,
    isLoading: projectedRewardAmountAndAPYLoading,
    data: projectedRewardAmountAndAPY,
    error: rewardAmountAndAPYError,
    refetch: rewardAmountAndAPYRefetch
  } = useQuery<EstimatedRewardAmountAndAPY, Error>(
    [GENERIC_FETCHER, 'escrow', 'getRewardEstimate', address],
    genericFetcher<EstimatedRewardAmountAndAPY>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(rewardAmountAndAPYError);

  // Estimated governance token Rewards & APY
  const monetaryLockingAmount = newMonetaryAmount(lockingAmount, GOVERNANCE_TOKEN, true);
  const {
    isIdle: estimatedRewardAmountAndAPYIdle,
    isLoading: estimatedRewardAmountAndAPYLoading,
    data: estimatedRewardAmountAndAPY,
    error: estimatedRewardAmountAndAPYError
  } = useQuery<EstimatedRewardAmountAndAPY, Error>(
    [GENERIC_FETCHER, 'escrow', 'getRewardEstimate', address, monetaryLockingAmount, blockLockTimeExtension],
    genericFetcher<EstimatedRewardAmountAndAPY>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(estimatedRewardAmountAndAPYError);

  const {
    isIdle: stakedAmountAndEndBlockIdle,
    isLoading: stakedAmountAndEndBlockLoading,
    data: stakedAmountAndEndBlock,
    error: stakedAmountAndEndBlockError,
    refetch: stakedAmountAndEndBlockRefetch
  } = useQuery<StakedAmountAndEndBlock, Error>(
    [GENERIC_FETCHER, 'escrow', 'getStakedBalance', address],
    genericFetcher<StakedAmountAndEndBlock>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(stakedAmountAndEndBlockError);

  const initialStakeMutation = useMutation<void, Error, LockingAmountAndTime>(
    (variables: LockingAmountAndTime) => {
      if (currentBlockNumber === undefined) {
        throw new Error('Something went wrong!');
      }
      const unlockHeight = currentBlockNumber + convertWeeksToBlockNumbers(variables.time);

      return window.bridge.escrow.createLock(variables.amount, unlockHeight);
    },
    {
      onSuccess: () => {
        voteGovernanceTokenBalanceRefetch();
        stakedAmountAndEndBlockRefetch();
        claimableRewardAmountRefetch();
        rewardAmountAndAPYRefetch();
        reset({
          [LOCKING_AMOUNT]: '0.0',
          [LOCK_TIME]: '0'
        });
      }
    }
  );

  const moreStakeMutation = useMutation<void, Error, LockingAmountAndTime>(
    (variables: LockingAmountAndTime) => {
      return (async () => {
        if (stakedAmountAndEndBlock === undefined) {
          throw new Error('Something went wrong!');
        }

        if (checkIncreaseLockAmountAndExtendLockTime(variables.time, variables.amount)) {
          const unlockHeight = stakedAmountAndEndBlock.endBlock + convertWeeksToBlockNumbers(variables.time);

          const txs = [
            window.bridge.api.tx.escrow.increaseAmount(variables.amount.toString(variables.amount.currency.rawBase)),
            window.bridge.api.tx.escrow.increaseUnlockHeight(unlockHeight)
          ];
          const batch = window.bridge.api.tx.utility.batchAll(txs);
          await DefaultTransactionAPI.sendLogged(
            window.bridge.api,
            window.bridge.account as AddressOrPair,
            batch,
            undefined, // don't await success event
            true // don't wait for finalized blocks
          );
        } else if (checkOnlyIncreaseLockAmount(variables.time, variables.amount)) {
          return await window.bridge.escrow.increaseAmount(variables.amount);
        } else if (checkOnlyExtendLockTime(variables.time, variables.amount)) {
          const unlockHeight = stakedAmountAndEndBlock.endBlock + convertWeeksToBlockNumbers(variables.time);

          return await window.bridge.escrow.increaseUnlockHeight(unlockHeight);
        } else {
          throw new Error('Something went wrong!');
        }
      })();
    },
    {
      onSuccess: () => {
        voteGovernanceTokenBalanceRefetch();
        stakedAmountAndEndBlockRefetch();
        claimableRewardAmountRefetch();
        rewardAmountAndAPYRefetch();
        reset({
          [LOCKING_AMOUNT]: '0.0',
          [LOCK_TIME]: '0'
        });
      }
    }
  );

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

    const lockTimeValue = Number(lockTime);
    setBlockLockTimeExtension(convertWeeksToBlockNumbers(lockTimeValue));
  }, [lockTime]);

  React.useEffect(() => {
    reset({
      [LOCKING_AMOUNT]: '',
      [LOCK_TIME]: ''
    });
  }, [address, reset]);

  const votingBalanceGreaterThanZero = voteGovernanceTokenBalance?.gt(ZERO_VOTE_GOVERNANCE_TOKEN_AMOUNT);

  const extendLockTimeSet = votingBalanceGreaterThanZero && parseInt(lockTime) > 0;
  const increaseLockingAmountSet =
    votingBalanceGreaterThanZero && monetaryLockingAmount.gt(ZERO_GOVERNANCE_TOKEN_AMOUNT);

  React.useEffect(() => {
    if (extendLockTimeSet) {
      trigger(LOCKING_AMOUNT);
    }
  }, [lockTime, extendLockTimeSet, trigger]);

  React.useEffect(() => {
    if (increaseLockingAmountSet) {
      trigger(LOCK_TIME);
    }
  }, [lockingAmount, increaseLockingAmountSet, trigger]);

  const getStakedAmount = () => {
    if (stakedAmountAndEndBlockIdle || stakedAmountAndEndBlockLoading) {
      return undefined;
    }
    if (stakedAmountAndEndBlock === undefined) {
      throw new Error('Something went wrong!');
    }

    return stakedAmountAndEndBlock.amount;
  };
  const stakedAmount = getStakedAmount();

  const availableBalance = React.useMemo(() => {
    if (!governanceTokenBalance || stakedAmountAndEndBlockIdle || stakedAmountAndEndBlockLoading) return;
    if (stakedAmount === undefined) {
      throw new Error('Something went wrong!');
    }

    return governanceTokenBalance.sub(stakedAmount).sub(TRANSACTION_FEE_AMOUNT);
  }, [governanceTokenBalance, stakedAmountAndEndBlockIdle, stakedAmountAndEndBlockLoading, stakedAmount]);

  const onSubmit = (data: StakingFormData) => {
    if (!bridgeLoaded) return;
    if (currentBlockNumber === undefined) {
      throw new Error('Something went wrong!');
    }

    const lockingAmountWithFallback = data[LOCKING_AMOUNT] || '0';
    const lockTimeWithFallback = data[LOCK_TIME] || '0'; // Weeks

    const monetaryAmount = newMonetaryAmount(lockingAmountWithFallback, GOVERNANCE_TOKEN, true);
    const numberTime = parseInt(lockTimeWithFallback);

    if (votingBalanceGreaterThanZero) {
      moreStakeMutation.mutate({
        amount: monetaryAmount,
        time: numberTime
      });
    } else {
      initialStakeMutation.mutate({
        amount: monetaryAmount,
        time: numberTime
      });
    }
  };

  const validateLockingAmount = (value: string): string | undefined => {
    const valueWithFallback = value || '0';
    const monetaryLockingAmount = newMonetaryAmount(valueWithFallback, GOVERNANCE_TOKEN, true);

    if (!extendLockTimeSet && monetaryLockingAmount.lte(ZERO_GOVERNANCE_TOKEN_AMOUNT)) {
      return 'Locking amount must be greater than zero!';
    }

    if (availableBalance === undefined) {
      throw new Error('Something went wrong!');
    }
    if (monetaryLockingAmount.gt(availableBalance)) {
      return 'Locking amount must not be greater than available balance!';
    }

    const planckLockingAmount = monetaryLockingAmount.to.Planck();
    const lockBlocks = convertWeeksToBlockNumbers(parseInt(lockTime));
    // This is related to the on-chain implementation where currency values are integers.
    // So less tokens than the period would likely round to 0.
    // So on the UI, as long as you require more planck to be locked than the number of blocks the user locks for,
    // it should be good.
    if (!extendLockTimeSet && planckLockingAmount.lte(Big(lockBlocks))) {
      return 'Planck to be locked must be greater than the number of blocks you lock for!';
    }

    return undefined;
  };

  const validateLockTime = (value: string): string | undefined => {
    const valueWithFallback = value || '0';
    const numericValue = parseInt(valueWithFallback);

    if (votingBalanceGreaterThanZero && numericValue === 0 && monetaryLockingAmount.gt(ZERO_GOVERNANCE_TOKEN_AMOUNT)) {
      return undefined;
    }

    if (availableLockTime === undefined) {
      throw new Error('Something went wrong!');
    }
    if (numericValue < STAKE_LOCK_TIME.MIN || numericValue > availableLockTime) {
      return `Please enter a number between ${STAKE_LOCK_TIME.MIN}-${availableLockTime}.`;
    }

    return undefined;
  };

  const renderVoteStakedAmountLabel = () => {
    if (voteGovernanceTokenBalanceIdle || voteGovernanceTokenBalanceLoading) {
      return '-';
    }
    if (voteGovernanceTokenBalance === undefined) {
      throw new Error('Something went wrong!');
    }

    return displayMonetaryAmount(voteGovernanceTokenBalance);
  };

  const renderProjectedRewardAmountLabel = () => {
    if (projectedRewardAmountAndAPYIdle || projectedRewardAmountAndAPYLoading) {
      return '-';
    }
    if (projectedRewardAmountAndAPY === undefined) {
      throw new Error('Something went wrong!');
    }

    return displayMonetaryAmount(projectedRewardAmountAndAPY.amount);
  };

  const renderStakedAmountLabel = () => {
    return stakedAmount === undefined ? '-' : displayMonetaryAmount(stakedAmount);
  };

  const hasStakedAmount = stakedAmount?.gt(ZERO_GOVERNANCE_TOKEN_AMOUNT);

  const getRemainingBlockNumbersToUnstake = () => {
    if (
      stakedAmountAndEndBlockIdle ||
      stakedAmountAndEndBlockLoading ||
      currentBlockNumberIdle ||
      currentBlockNumberLoading
    ) {
      return undefined;
    }
    if (stakedAmountAndEndBlock === undefined) {
      throw new Error('Something went wrong!');
    }
    if (currentBlockNumber === undefined) {
      throw new Error('Something went wrong!');
    }

    return hasStakedAmount
      ? stakedAmountAndEndBlock.endBlock - currentBlockNumber // If the user has staked
      : null; // If the user has not staked
  };
  const remainingBlockNumbersToUnstake = getRemainingBlockNumbersToUnstake();

  const getAvailableLockTime = () => {
    if (remainingBlockNumbersToUnstake === undefined) {
      return undefined;
    }

    // If the user has staked
    if (hasStakedAmount) {
      if (remainingBlockNumbersToUnstake === null) {
        throw new Error('Something went wrong!');
      }
      const remainingWeeksToUnstake = convertBlockNumbersToWeeks(remainingBlockNumbersToUnstake);

      return Math.floor(STAKE_LOCK_TIME.MAX - remainingWeeksToUnstake);
      // If the user has not staked
    } else {
      return STAKE_LOCK_TIME.MAX;
    }
  };
  const availableLockTime = getAvailableLockTime();

  const renderAvailableBalanceLabel = () => {
    return availableBalance === undefined ? '-' : displayMonetaryAmount(availableBalance);
  };

  const renderUnlockDateLabel = () => {
    if (errors[LOCK_TIME]) {
      return '-';
    }

    const unlockDate = add(new Date(), {
      weeks: parseInt(lockTime)
    });

    return format(unlockDate, YEAR_MONTH_DAY_PATTERN);
  };

  const renderNewUnlockDateLabel = () => {
    if (remainingBlockNumbersToUnstake === undefined) {
      return '-';
    }
    if (errors[LOCK_TIME]) {
      return '-';
    }

    let remainingLockSeconds;
    if (hasStakedAmount) {
      if (remainingBlockNumbersToUnstake === null) {
        throw new Error('Something went wrong!');
      }

      remainingLockSeconds = remainingBlockNumbersToUnstake * BLOCK_TIME;
    } else {
      remainingLockSeconds = 0;
    }
    const unlockDate = add(new Date(), {
      weeks: parseInt(lockTime),
      seconds: remainingLockSeconds
    });

    return format(unlockDate, YEAR_MONTH_DAY_PATTERN);
  };

  const renderNewVoteGovernanceTokenGainedLabel = () => {
    const newTotalStakeAmount = getNewTotalStake();
    if (voteGovernanceTokenBalance === undefined || newTotalStakeAmount === undefined) {
      return '-';
    }

    const newVoteGovernanceTokenAmountGained = newTotalStakeAmount.sub(voteGovernanceTokenBalance);
    const rounded = newVoteGovernanceTokenAmountGained.toBig(VOTE_GOVERNANCE_TOKEN.base).round(5);
    const typed = newMonetaryAmount(rounded, VOTE_GOVERNANCE_TOKEN, true);

    return `${displayMonetaryAmount(typed)} ${VOTE_GOVERNANCE_TOKEN_SYMBOL}`;
  };

  const getNewTotalStake = () => {
    if (remainingBlockNumbersToUnstake === undefined || stakedAmount === undefined) {
      return undefined;
    }

    const extendingLockTime = parseInt(lockTime); // Weeks

    let newLockTime: number;
    let newLockingAmount: GovernanceTokenMonetaryAmount;
    if (remainingBlockNumbersToUnstake === null) {
      // If the user has not staked
      newLockTime = extendingLockTime;
      newLockingAmount = monetaryLockingAmount;
    } else {
      // If the user has staked
      const currentLockTime = convertBlockNumbersToWeeks(remainingBlockNumbersToUnstake); // Weeks

      // New lock-time that is applied to the entire staked governance token
      newLockTime = currentLockTime + extendingLockTime; // Weeks

      // New total staked governance token
      newLockingAmount = monetaryLockingAmount.add(stakedAmount);
    }

    // Multiplying the new total staked governance token with the staking time divided by the maximum lock time
    return newLockingAmount.mul(newLockTime).div(STAKE_LOCK_TIME.MAX);
  };

  const renderNewTotalStakeLabel = () => {
    const newTotalStakeAmount = getNewTotalStake();
    if (newTotalStakeAmount === undefined) {
      return '-';
    }

    return `${displayMonetaryAmount(newTotalStakeAmount)} ${VOTE_GOVERNANCE_TOKEN_SYMBOL}`;
  };

  const renderEstimatedAPYLabel = () => {
    if (
      estimatedRewardAmountAndAPYIdle ||
      estimatedRewardAmountAndAPYLoading ||
      errors[LOCK_TIME] ||
      errors[LOCKING_AMOUNT]
    ) {
      return '-';
    }
    if (estimatedRewardAmountAndAPY === undefined) {
      throw new Error('Something went wrong!');
    }

    return `${safeRoundTwoDecimals(estimatedRewardAmountAndAPY.apy.toString())} %`;
  };

  const renderEstimatedRewardAmountLabel = () => {
    if (
      estimatedRewardAmountAndAPYIdle ||
      estimatedRewardAmountAndAPYLoading ||
      errors[LOCK_TIME] ||
      errors[LOCKING_AMOUNT]
    ) {
      return '-';
    }
    if (estimatedRewardAmountAndAPY === undefined) {
      throw new Error('Something went wrong!');
    }

    return `${displayMonetaryAmount(estimatedRewardAmountAndAPY.amount)} ${GOVERNANCE_TOKEN_SYMBOL}`;
  };

  const renderClaimableRewardAmountLabel = () => {
    if (claimableRewardAmountIdle || claimableRewardAmountLoading) {
      return '-';
    }
    if (claimableRewardAmount === undefined) {
      throw new Error('Something went wrong!');
    }

    return displayMonetaryAmount(claimableRewardAmount);
  };

  const handleConfirmClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    // TODO: should be handled based on https://kentcdodds.com/blog/application-state-management-with-react
    if (!accountSet) {
      dispatch(showAccountModalAction(true));
      event.preventDefault();
    }
  };

  const valueInUSDOfLockingAmount = getUsdAmount(monetaryLockingAmount, prices.governanceToken?.usd);

  const claimRewardsButtonEnabled = claimableRewardAmount?.gt(ZERO_GOVERNANCE_TOKEN_AMOUNT);

  const unlockFirst =
    hasStakedAmount &&
    // eslint-disable-next-line max-len
    // `remainingBlockNumbersToUnstake !== null` is redundant because if `hasStakedAmount` is truthy `remainingBlockNumbersToUnstake` cannot be null
    remainingBlockNumbersToUnstake !== null &&
    remainingBlockNumbersToUnstake !== undefined &&
    remainingBlockNumbersToUnstake <= 0;

  const accountSet = !!address;

  const lockTimeFieldDisabled =
    votingBalanceGreaterThanZero === undefined ||
    remainingBlockNumbersToUnstake === undefined ||
    availableLockTime === undefined ||
    availableLockTime <= 0 ||
    unlockFirst;

  const lockingAmountFieldDisabled = availableBalance === undefined;

  const initializing =
    currentBlockNumberIdle ||
    currentBlockNumberLoading ||
    voteGovernanceTokenBalanceIdle ||
    voteGovernanceTokenBalanceLoading ||
    claimableRewardAmountIdle ||
    claimableRewardAmountLoading ||
    projectedRewardAmountAndAPYIdle ||
    projectedRewardAmountAndAPYLoading ||
    estimatedRewardAmountAndAPYIdle ||
    estimatedRewardAmountAndAPYLoading ||
    stakedAmountAndEndBlockIdle ||
    stakedAmountAndEndBlockLoading;

  let submitButtonLabel: string;
  if (initializing) {
    submitButtonLabel = 'Loading...';
  } else {
    if (accountSet) {
      // TODO: should improve readability by handling nested conditions
      if (votingBalanceGreaterThanZero) {
        const numericLockTime = parseInt(lockTime);
        if (checkIncreaseLockAmountAndExtendLockTime(numericLockTime, monetaryLockingAmount)) {
          submitButtonLabel = 'Add more stake and extend lock time';
        } else if (checkOnlyIncreaseLockAmount(numericLockTime, monetaryLockingAmount)) {
          submitButtonLabel = 'Add more stake';
        } else if (checkOnlyExtendLockTime(numericLockTime, monetaryLockingAmount)) {
          submitButtonLabel = 'Extend lock time';
        } else {
          submitButtonLabel = 'Stake';
        }
      } else {
        submitButtonLabel = 'Stake';
      }
    } else {
      submitButtonLabel = t('connect_wallet');
    }
  }

  return (
    <>
      <MainContainer>
        {process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA && (
          <WarningBanner
            className={SHARED_CLASSES}
            message='Block times are currently higher than expected. Lock times may be longer than expected.'
          />
        )}
        <Panel className={SHARED_CLASSES}>
          <form className={clsx('p-8', 'space-y-8')} onSubmit={handleSubmit(onSubmit)}>
            <TitleWithUnderline text={`Stake ${GOVERNANCE_TOKEN_SYMBOL}`} />
            <BalancesUI
              stakedAmount={renderStakedAmountLabel()}
              voteStakedAmount={renderVoteStakedAmountLabel()}
              projectedRewardAmount={renderProjectedRewardAmountLabel()}
            />
            <ClaimRewardsButton
              claimableRewardAmount={renderClaimableRewardAmountLabel()}
              disabled={claimRewardsButtonEnabled === false}
            />
            {/* eslint-disable-next-line max-len */}
            {/* `remainingBlockNumbersToUnstake !== null` is redundant because if `hasStakedAmount` is truthy `remainingBlockNumbersToUnstake` cannot be null */}
            {hasStakedAmount && remainingBlockNumbersToUnstake !== null && (
              <WithdrawButton
                stakedAmount={renderStakedAmountLabel()}
                remainingBlockNumbersToUnstake={remainingBlockNumbersToUnstake}
              />
            )}
            <TotalsUI />
            <div className='space-y-2'>
              <AvailableBalanceUI
                label='Available balance'
                balance={renderAvailableBalanceLabel()}
                tokenSymbol={GOVERNANCE_TOKEN_SYMBOL}
              />
              <TokenField
                id={LOCKING_AMOUNT}
                name={LOCKING_AMOUNT}
                label={GOVERNANCE_TOKEN_SYMBOL}
                min={0}
                ref={register({
                  required: {
                    value: extendLockTimeSet ? false : true,
                    message: 'This field is required!'
                  },
                  validate: (value) => validateLockingAmount(value)
                })}
                approxUSD={`≈ $ ${valueInUSDOfLockingAmount}`}
                error={!!errors[LOCKING_AMOUNT]}
                helperText={errors[LOCKING_AMOUNT]?.message}
                disabled={lockingAmountFieldDisabled}
              />
            </div>
            <LockTimeField
              id={LOCK_TIME}
              name={LOCK_TIME}
              min={0}
              ref={register({
                required: {
                  value: votingBalanceGreaterThanZero ? false : true,
                  message: 'This field is required!'
                },
                validate: (value) => validateLockTime(value)
              })}
              error={!!errors[LOCK_TIME]}
              helperText={errors[LOCK_TIME]?.message}
              optional={votingBalanceGreaterThanZero}
              disabled={lockTimeFieldDisabled}
            />
            {votingBalanceGreaterThanZero ? (
              <InformationUI
                label='New unlock Date'
                value={renderNewUnlockDateLabel()}
                tooltip='Your original lock date plus the extended lock time.'
              />
            ) : (
              <InformationUI
                label='Unlock Date'
                value={renderUnlockDateLabel()}
                tooltip='Your staked amount will be locked until this date.'
              />
            )}
            <InformationUI
              label={t('staking_page.new_vote_governance_token_gained', {
                voteGovernanceTokenSymbol: VOTE_GOVERNANCE_TOKEN_SYMBOL
              })}
              value={renderNewVoteGovernanceTokenGainedLabel()}
              tooltip={t('staking_page.the_increase_in_your_vote_governance_token_balance', {
                voteGovernanceTokenSymbol: VOTE_GOVERNANCE_TOKEN_SYMBOL
              })}
            />
            {votingBalanceGreaterThanZero && (
              <InformationUI
                label='New total Stake'
                value={`${renderNewTotalStakeLabel()}`}
                tooltip='Your total stake after this transaction'
              />
            )}
            <InformationUI
              label='Estimated APY'
              value={renderEstimatedAPYLabel()}
              tooltip={`The APY may change as the amount of total ${VOTE_GOVERNANCE_TOKEN_SYMBOL} changes.`}
            />
            <InformationUI
              label={`Estimated ${GOVERNANCE_TOKEN_SYMBOL} Rewards`}
              value={renderEstimatedRewardAmountLabel()}
              tooltip={t('staking_page.the_estimated_amount_of_governance_token_you_will_receive_as_rewards', {
                governanceTokenSymbol: GOVERNANCE_TOKEN_SYMBOL,
                voteGovernanceTokenSymbol: VOTE_GOVERNANCE_TOKEN_SYMBOL
              })}
            />
            <SubmitButton
              disabled={initializing || unlockFirst}
              pending={initialStakeMutation.isLoading || moreStakeMutation.isLoading}
              onClick={handleConfirmClick}
              endIcon={
                unlockFirst ? (
                  <InformationTooltip label='Please unstake first.' forDisabledAction={unlockFirst} />
                ) : null
              }
            >
              {submitButtonLabel}
            </SubmitButton>
          </form>
        </Panel>
      </MainContainer>
      {(initialStakeMutation.isError || moreStakeMutation.isError) && (
        <ErrorModal
          open={initialStakeMutation.isError || moreStakeMutation.isError}
          onClose={() => {
            initialStakeMutation.reset();
            moreStakeMutation.reset();
          }}
          title='Error'
          description={initialStakeMutation.error?.message || moreStakeMutation.error?.message || ''}
        />
      )}
    </>
  );
}
Example #23
Source File: App.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
App = (): JSX.Element => {
  const {
    bridgeLoaded,
    address,
    wrappedTokenBalance,
    wrappedTokenTransferableBalance,
    collateralTokenBalance,
    collateralTokenTransferableBalance,
    governanceTokenBalance,
    governanceTokenTransferableBalance,
    prices
  } = useSelector((state: StoreType) => state.general);
  // eslint-disable-next-line max-len
  const [bridgeStatus, setBridgeStatus] = React.useState(STATUSES.IDLE); // TODO: `bridgeLoaded` should be based on enum instead of boolean
  const dispatch = useDispatch();

  // Loads the main bridge API - connection to the bridge
  const loadBridge = React.useCallback(async (): Promise<void> => {
    try {
      setBridgeStatus(STATUSES.PENDING);
      window.bridge = await createInterBtcApi(constants.PARACHAIN_URL, constants.BITCOIN_NETWORK);
      dispatch(isBridgeLoaded(true));
      setBridgeStatus(STATUSES.RESOLVED);
    } catch (error) {
      toast.warn('Unable to connect to the BTC-Parachain.');
      console.log('[loadBridge] error.message => ', error.message);
      setBridgeStatus(STATUSES.REJECTED);
    }
  }, [dispatch]);

  // Loads the connection to the faucet - only for testnet purposes
  const loadFaucet = React.useCallback(async (): Promise<void> => {
    try {
      window.faucet = new FaucetClient(window.bridge.api, constants.FAUCET_URL);
      dispatch(isFaucetLoaded(true));
    } catch (error) {
      console.log('[loadFaucet] error.message => ', error.message);
    }
  }, [dispatch]);

  // Loads the bridge
  React.useEffect(() => {
    if (bridgeLoaded) return; // Not necessary but for more clarity
    if (bridgeStatus !== STATUSES.IDLE) return;

    (async () => {
      try {
        await loadBridge();
      } catch (error) {
        console.log('[App React.useEffect 7] error.message => ', error.message);
      }
    })();
  }, [loadBridge, bridgeLoaded, bridgeStatus]);

  // Loads the faucet
  React.useEffect(() => {
    if (!bridgeLoaded) return;
    if (process.env.REACT_APP_BITCOIN_NETWORK === BitcoinNetwork.Mainnet) return;

    (async () => {
      try {
        await loadFaucet();
      } catch (error) {
        console.log('[App React.useEffect 8] error.message => ', error.message);
      }
    })();
  }, [bridgeLoaded, loadFaucet]);

  // Maybe loads the vault client - only if the current address is also registered as a vault
  React.useEffect(() => {
    if (!bridgeLoaded) return;
    if (!address) return;

    const id = window.bridge.api.createType(ACCOUNT_ID_TYPE_NAME, address);

    (async () => {
      try {
        dispatch(isVaultClientLoaded(false));
        const vault = await window.bridge.vaults.get(id, COLLATERAL_TOKEN_ID_LITERAL);
        dispatch(isVaultClientLoaded(!!vault));
      } catch (error) {
        // TODO: should add error handling
        console.log('[App React.useEffect 1] error.message => ', error.message);
      }
    })();
  }, [bridgeLoaded, address, dispatch]);

  // Initializes data on app bootstrap
  React.useEffect(() => {
    if (!dispatch) return;
    if (!bridgeLoaded) return;

    (async () => {
      try {
        const [
          totalWrappedTokenAmount,
          totalLockedCollateralTokenAmount,
          totalGovernanceTokenAmount,
          btcRelayHeight,
          bitcoinHeight,
          state
        ] = await Promise.all([
          window.bridge.tokens.total(WRAPPED_TOKEN),
          window.bridge.tokens.total(COLLATERAL_TOKEN),
          window.bridge.tokens.total(GOVERNANCE_TOKEN),
          window.bridge.btcRelay.getLatestBlockHeight(),
          window.bridge.electrsAPI.getLatestBlockHeight(),
          window.bridge.system.getStatusCode()
        ]);

        const parachainStatus = (state: SecurityStatusCode) => {
          if (state.isError) {
            return ParachainStatus.Error;
          } else if (state.isRunning) {
            return ParachainStatus.Running;
          } else if (state.isShutdown) {
            return ParachainStatus.Shutdown;
          } else {
            return ParachainStatus.Loading;
          }
        };

        dispatch(
          initGeneralDataAction(
            totalWrappedTokenAmount,
            totalLockedCollateralTokenAmount,
            totalGovernanceTokenAmount,
            Number(btcRelayHeight),
            bitcoinHeight,
            parachainStatus(state)
          )
        );
      } catch (error) {
        // TODO: should add error handling
        console.log('[App React.useEffect 2] error.message => ', error.message);
      }
    })();
  }, [dispatch, bridgeLoaded]);

  // Loads the address for the currently selected account
  React.useEffect(() => {
    if (!dispatch) return;
    if (!bridgeLoaded) return;

    const trySetDefaultAccount = () => {
      if (constants.DEFAULT_ACCOUNT_SEED) {
        const keyring = new Keyring({ type: 'sr25519', ss58Format: constants.SS58_FORMAT });
        const defaultAccountKeyring = keyring.addFromUri(constants.DEFAULT_ACCOUNT_SEED as string);
        window.bridge.setAccount(defaultAccountKeyring);
        dispatch(changeAddressAction(defaultAccountKeyring.address));
      }
    };

    (async () => {
      try {
        const theExtensions = await web3Enable(APP_NAME);
        if (theExtensions.length === 0) {
          trySetDefaultAccount();
          return;
        }

        dispatch(setInstalledExtensionAction(theExtensions.map((extension) => extension.name)));

        // TODO: load accounts just once
        const accounts = await web3Accounts({ ss58Format: constants.SS58_FORMAT });
        const matchedAccount = accounts.find((account) => account.address === address);

        if (matchedAccount) {
          const { signer } = await web3FromAddress(address);
          window.bridge.setAccount(address, signer);
          dispatch(changeAddressAction(address));
        } else {
          dispatch(changeAddressAction(''));
          window.bridge.removeAccount();
        }
      } catch (error) {
        // TODO: should add error handling
        console.log('[App React.useEffect 3] error.message => ', error.message);
      }
    })();
  }, [address, bridgeLoaded, dispatch]);

  // Subscribes to balances
  React.useEffect(() => {
    if (!dispatch) return;
    if (!bridgeLoaded) return;
    if (!address) return;

    let unsubscribeFromCollateral: () => void;
    let unsubscribeFromWrapped: () => void;
    let unsubscribeFromGovernance: () => void;

    (async () => {
      try {
        unsubscribeFromCollateral = await window.bridge.tokens.subscribeToBalance(
          COLLATERAL_TOKEN,
          address,
          (_: string, balance: ChainBalance<CollateralUnit>) => {
            if (!balance.free.eq(collateralTokenBalance)) {
              dispatch(updateCollateralTokenBalanceAction(balance.free));
            }
            if (!balance.transferable.eq(collateralTokenTransferableBalance)) {
              dispatch(updateCollateralTokenTransferableBalanceAction(balance.transferable));
            }
          }
        );
      } catch (error) {
        console.log('[App React.useEffect 4] error.message => ', error.message);
      }
    })();

    (async () => {
      try {
        unsubscribeFromWrapped = await window.bridge.tokens.subscribeToBalance(
          WRAPPED_TOKEN,
          address,
          (_: string, balance: ChainBalance<BitcoinUnit>) => {
            if (!balance.free.eq(wrappedTokenBalance)) {
              dispatch(updateWrappedTokenBalanceAction(balance.free));
            }
            if (!balance.transferable.eq(wrappedTokenTransferableBalance)) {
              dispatch(updateWrappedTokenTransferableBalanceAction(balance.transferable));
            }
          }
        );
      } catch (error) {
        console.log('[App React.useEffect 5] error.message => ', error.message);
      }
    })();

    (async () => {
      try {
        unsubscribeFromGovernance = await window.bridge.tokens.subscribeToBalance(
          GOVERNANCE_TOKEN,
          address,
          (_: string, balance: ChainBalance<GovernanceUnit>) => {
            if (!balance.free.eq(governanceTokenBalance)) {
              dispatch(updateGovernanceTokenBalanceAction(balance.free));
            }
            if (!balance.transferable.eq(governanceTokenTransferableBalance)) {
              dispatch(updateGovernanceTokenTransferableBalanceAction(balance.transferable));
            }
          }
        );
      } catch (error) {
        console.log('[App React.useEffect 6] error.message => ', error.message);
      }
    })();

    return () => {
      if (unsubscribeFromCollateral) {
        unsubscribeFromCollateral();
      }
      if (unsubscribeFromWrapped) {
        unsubscribeFromWrapped();
      }
      if (unsubscribeFromGovernance) {
        unsubscribeFromGovernance();
      }
    };
  }, [
    dispatch,
    bridgeLoaded,
    address,
    wrappedTokenBalance,
    wrappedTokenTransferableBalance,
    collateralTokenBalance,
    collateralTokenTransferableBalance,
    governanceTokenBalance,
    governanceTokenTransferableBalance
  ]);

  // Color schemes according to Interlay vs. Kintsugi
  React.useEffect(() => {
    if (process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT) {
      document.documentElement.classList.add(CLASS_NAMES.LIGHT);
      document.documentElement.classList.remove(CLASS_NAMES.DARK);
      document.body.classList.add('text-interlayTextPrimaryInLightMode');
      document.body.classList.add('bg-interlayHaiti-50');
      document.body.classList.add('theme-interlay');
    }

    // MEMO: should check dark mode as well
    if (process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA) {
      // MEMO: inspired by https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually
      document.documentElement.classList.add(CLASS_NAMES.DARK);
      document.documentElement.classList.remove(CLASS_NAMES.LIGHT);
      document.body.classList.add('dark:text-kintsugiTextPrimaryInDarkMode');
      document.body.classList.add('dark:bg-kintsugiMidnight-900');
      document.body.classList.add('theme-kintsugi');
    }
  }, []);

  // Keeps fetching live data prices
  const { error: pricesError } = useQuery(
    PRICES_URL,
    async () => {
      const response = await fetch(PRICES_URL);
      if (!response.ok) {
        throw new Error('Network response for prices was not ok.');
      }

      const newPrices = await response.json();
      // Update the store only if the price is actually changed
      if (
        newPrices.bitcoin?.usd !== prices.bitcoin?.usd ||
        newPrices[RELAY_CHAIN_NAME]?.usd !== prices.collateralToken?.usd ||
        newPrices[BRIDGE_PARACHAIN_NAME]?.usd !== prices.governanceToken?.usd
      ) {
        dispatch(
          updateOfPricesAction({
            bitcoin: newPrices.bitcoin,
            collateralToken: newPrices[RELAY_CHAIN_NAME],
            governanceToken: newPrices[BRIDGE_PARACHAIN_NAME]
          })
        );
      }
    },
    { refetchInterval: 60000 }
  );
  useErrorHandler(pricesError);

  return (
    <>
      <InterlayHelmet />
      <ToastContainer position='top-right' autoClose={5000} hideProgressBar={false} />
      <Layout>
        <Route
          render={({ location }) => (
            <React.Suspense fallback={<FullLoadingSpinner />}>
              <Switch location={location}>
                <Route exact path={PAGES.VAULTS}>
                  <Vaults />
                </Route>
                <Route exact path={PAGES.VAULT}>
                  <Vault />
                </Route>
                <Route path={PAGES.VAULT}>
                  <Vaults />
                </Route>
                <Route path={PAGES.DASHBOARD}>
                  <Dashboard />
                </Route>
                <Route path={PAGES.STAKING}>
                  <Staking />
                </Route>
                <Route path={PAGES.TRANSACTIONS}>
                  <Transactions />
                </Route>
                <Route path={PAGES.BRIDGE}>
                  <Bridge />
                </Route>
                <Route path={PAGES.TRANSFER}>
                  <Transfer />
                </Route>
                <Redirect exact from={PAGES.HOME} to={PAGES.BRIDGE} />
                <Route path='*'>
                  <NoMatch />
                </Route>
              </Switch>
            </React.Suspense>
          )}
        />
      </Layout>
    </>
  );
}
Example #24
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
UpperContent = (): JSX.Element => {
  const { prices } = useSelector((state: StoreType) => state.general);
  const { t } = useTranslation();

  const {
    isIdle: totalSuccessfulRedeemsIdle,
    isLoading: totalSuccessfulRedeemsLoading,
    data: totalSuccessfulRedeems,
    error: totalSuccessfulRedeemsError
  } = useQuery<GraphqlReturn<any>, Error>(
    [GRAPHQL_FETCHER, redeemCountQuery('status_eq: Completed')],
    graphqlFetcher<GraphqlReturn<any>>()
  );
  useErrorHandler(totalSuccessfulRedeemsError);

  const {
    isIdle: cumulativeRedeemsPerDayIdle,
    isLoading: cumulativeRedeemsPerDayLoading,
    data: cumulativeRedeemsPerDay,
    error: cumulativeRedeemsPerDayError
    // TODO: should type properly (`Relay`)
  } = useQuery<VolumeDataPoint<BitcoinUnit>[], Error>(
    [CUMULATIVE_VOLUMES_FETCHER, 'Redeemed' as VolumeType, [nowAtfirstLoad], WRAPPED_TOKEN],
    cumulativeVolumesFetcher
  );
  useErrorHandler(cumulativeRedeemsPerDayError);

  // TODO: should use skeleton loaders
  if (
    totalSuccessfulRedeemsIdle ||
    totalSuccessfulRedeemsLoading ||
    cumulativeRedeemsPerDayIdle ||
    cumulativeRedeemsPerDayLoading
  ) {
    return <>Loading...</>;
  }
  if (cumulativeRedeemsPerDay === undefined) {
    throw new Error('Something went wrong!');
  }
  if (totalSuccessfulRedeems === undefined) {
    throw new Error('Something went wrong!');
  }
  const totalSuccessfulRedeemCount = totalSuccessfulRedeems.data.redeemsConnection.totalCount;
  const totalRedeemedAmount = cumulativeRedeemsPerDay[0].amount;

  // TODO: add this again when the network is stable
  // const redeemSuccessRate = totalSuccessfulRedeems / totalRedeemRequests;

  return (
    <Panel className={clsx('grid', 'sm:grid-cols-2', 'gap-5', 'px-4', 'py-5')}>
      <Stats
        leftPart={
          <>
            <StatsDt
              className={clsx(
                { '!text-interlayDenim': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
                { 'dark:!text-kintsugiSupernova': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
              )}
            >
              {t('dashboard.redeem.total_redeemed')}
            </StatsDt>
            <StatsDd>
              {totalRedeemedAmount.str.BTC()}
              &nbsp;BTC
            </StatsDd>
            <StatsDd>
              {/* eslint-disable-next-line max-len */}$
              {prices.bitcoin === undefined
                ? '—'
                : (prices.bitcoin.usd * Number(totalRedeemedAmount.str.BTC())).toLocaleString()}
            </StatsDd>
            <StatsDt className='!text-interlayConifer'>{t('dashboard.redeem.total_redeems')}</StatsDt>
            <StatsDd>{totalSuccessfulRedeemCount}</StatsDd>
            {/* TODO: add this again when the network is stable */}
            {/* <StatsDt className='!text-interlayConifer'>
              {t('dashboard.redeem.success_rate')}
            </StatsDt>
            <StatsDd>
              {totalRedeemRequests ? (redeemSuccessRate * 100).toFixed(2) + '%' : t('no_data')}
            </StatsDd> */}
          </>
        }
      />
      <RedeemedChart />
    </Panel>
  );
}
Example #25
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
RedeemRequestsTable = (): JSX.Element => {
  const queryParams = useQueryParams();
  const { bridgeLoaded } = useSelector((state: StoreType) => state.general);
  const selectedPage = Number(queryParams.get(QUERY_PARAMETERS.PAGE)) || 1;
  const updateQueryParameters = useUpdateQueryParameters();
  const { t } = useTranslation();

  const columns = React.useMemo(
    () => [
      {
        Header: t('date_created'),
        classNames: ['text-left'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          return <>{formatDateTimePrecise(new Date(redeem.request.timestamp))}</>;
        }
      },
      {
        Header: t('last_update'),
        classNames: ['text-left'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          let date;
          if (redeem.execution) {
            date = redeem.execution.timestamp;
          } else if (redeem.cancellation) {
            date = redeem.cancellation.timestamp;
          } else {
            date = redeem.request.timestamp;
          }

          return <>{formatDateTimePrecise(new Date(date))}</>;
        }
      },
      {
        Header: t('parachain_block'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          let height;
          if (redeem.execution) {
            height = redeem.execution.height.active;
          } else if (redeem.cancellation) {
            height = redeem.cancellation.height.active;
          } else {
            height = redeem.request.height.active;
          }

          return <>{height}</>;
        }
      },
      {
        Header: t('redeem_page.amount'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: redeem } }: any) {
          return <>{displayMonetaryAmount(redeem.request.requestedAmountBacking)}</>;
        }
      },
      {
        Header: t('issue_page.vault_dot_address'),
        accessor: 'vault',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: any }) {
          return <>{shortAddress(value.accountId)}</>;
        }
      },
      {
        Header: t('redeem_page.output_BTC_address'),
        accessor: 'userBackingAddress',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: string }) {
          return <ExternalLink href={`${BTC_EXPLORER_ADDRESS_API}${value}`}>{shortAddress(value)}</ExternalLink>;
        }
      },
      {
        Header: t('status'),
        accessor: 'status',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: RedeemStatus }) {
          return (
            <StatusCell
              status={{
                completed: value === RedeemStatus.Completed,
                cancelled: value === RedeemStatus.Retried,
                isExpired: value === RedeemStatus.Expired,
                reimbursed: value === RedeemStatus.Reimbursed
              }}
            />
          );
        }
      }
    ],
    [t]
  );

  const {
    isIdle: stableBtcConfirmationsIdle,
    isLoading: stableBtcConfirmationsLoading,
    data: stableBtcConfirmations,
    error: stableBtcConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableBitcoinConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(stableBtcConfirmationsError);

  const {
    isIdle: latestParachainActiveBlockIdle,
    isLoading: latestParachainActiveBlockLoading,
    data: latestParachainActiveBlock,
    error: latestParachainActiveBlockError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'system', 'getCurrentActiveBlockNumber'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(latestParachainActiveBlockError);

  const {
    isIdle: stableParachainConfirmationsIdle,
    isLoading: stableParachainConfirmationsLoading,
    data: stableParachainConfirmations,
    error: stableParachainConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableParachainConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(stableParachainConfirmationsError);

  const selectedPageIndex = selectedPage - 1;

  const {
    isIdle: redeemsIdle,
    isLoading: redeemsLoading,
    data: redeems,
    error: redeemsError
    // TODO: should type properly (`Relay`)
  } = useQuery<any, Error>(
    [
      REDEEM_FETCHER,
      selectedPageIndex * TABLE_PAGE_LIMIT, // offset
      TABLE_PAGE_LIMIT // limit
    ],
    redeemFetcher
  );
  useErrorHandler(redeemsError);

  const {
    isIdle: redeemsCountIdle,
    isLoading: redeemsCountLoading,
    data: redeemsCount,
    error: redeemsCountError
    // TODO: should type properly (`Relay`)
  } = useQuery<GraphqlReturn<any>, Error>([GRAPHQL_FETCHER, redeemCountQuery()], graphqlFetcher<GraphqlReturn<any>>());
  useErrorHandler(redeemsCountError);

  const data =
    redeems === undefined ||
    stableBtcConfirmations === undefined ||
    stableParachainConfirmations === undefined ||
    latestParachainActiveBlock === undefined
      ? []
      : redeems.map(
          // TODO: should type properly (`Relay`)
          (redeem: any) =>
            getRedeemWithStatus(
              redeem,
              stableBtcConfirmations,
              stableParachainConfirmations,
              latestParachainActiveBlock
            )
        );

  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
    columns,
    data
  });

  const renderContent = () => {
    if (
      stableBtcConfirmationsIdle ||
      stableBtcConfirmationsLoading ||
      stableParachainConfirmationsIdle ||
      stableParachainConfirmationsLoading ||
      latestParachainActiveBlockIdle ||
      latestParachainActiveBlockLoading ||
      redeemsIdle ||
      redeemsLoading ||
      redeemsCountIdle ||
      redeemsCountLoading
    ) {
      return <PrimaryColorEllipsisLoader />;
    }
    if (redeemsCount === undefined) {
      throw new Error('Something went wrong!');
    }

    const handlePageChange = ({ selected: newSelectedPageIndex }: { selected: number }) => {
      updateQueryParameters({
        [QUERY_PARAMETERS.PAGE]: (newSelectedPageIndex + 1).toString()
      });
    };

    const totalRedeemCount = redeemsCount.data.redeemsConnection.totalCount || 0;
    const pageCount = Math.ceil(totalRedeemCount / TABLE_PAGE_LIMIT);

    return (
      <>
        <InterlayTable {...getTableProps()}>
          <InterlayThead>
            {/* TODO: should type properly */}
            {headerGroups.map((headerGroup: any) => (
              // eslint-disable-next-line react/jsx-key
              <InterlayTr {...headerGroup.getHeaderGroupProps()}>
                {/* TODO: should type properly */}
                {headerGroup.headers.map((column: any) => (
                  // eslint-disable-next-line react/jsx-key
                  <InterlayTh
                    {...column.getHeaderProps([
                      {
                        className: clsx(column.classNames),
                        style: column.style
                      }
                    ])}
                  >
                    {column.render('Header')}
                  </InterlayTh>
                ))}
              </InterlayTr>
            ))}
          </InterlayThead>
          <InterlayTbody {...getTableBodyProps()}>
            {/* TODO: should type properly */}
            {rows.map((row: any) => {
              prepareRow(row);

              return (
                // eslint-disable-next-line react/jsx-key
                <InterlayTr {...row.getRowProps()}>
                  {/* TODO: should type properly */}
                  {row.cells.map((cell: any) => {
                    return (
                      // eslint-disable-next-line react/jsx-key
                      <InterlayTd
                        {...cell.getCellProps([
                          {
                            className: clsx(cell.column.classNames),
                            style: cell.column.style
                          }
                        ])}
                      >
                        {cell.render('Cell')}
                      </InterlayTd>
                    );
                  })}
                </InterlayTr>
              );
            })}
          </InterlayTbody>
        </InterlayTable>
        {pageCount > 0 && (
          <div className={clsx('flex', 'justify-end')}>
            <InterlayPagination
              pageCount={pageCount}
              marginPagesDisplayed={2}
              pageRangeDisplayed={5}
              onPageChange={handlePageChange}
              forcePage={selectedPageIndex}
            />
          </div>
        )}
      </>
    );
  };

  return (
    <InterlayTableContainer className='space-y-6'>
      <SectionTitle>{t('issue_page.recent_requests')}</SectionTitle>
      {renderContent()}
    </InterlayTableContainer>
  );
}
Example #26
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
OracleTable = (): JSX.Element => {
  const { t } = useTranslation();
  const { bridgeLoaded } = useSelector((state: StoreType) => state.general);

  const { isIdle: namesMapIdle, isLoading: namesMapLoading, data: namesMap, error: namesMapError } = useQuery<
    Map<string, string>,
    Error
  >([GENERIC_FETCHER, 'oracle', 'getSourcesById'], genericFetcher<Map<string, string>>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(namesMapError);

  const {
    isIdle: oracleTimeoutIdle,
    isLoading: oracleTimeoutLoading,
    data: oracleTimeout,
    error: oracleTimeoutError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'oracle', 'getOnlineTimeout'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(oracleTimeoutError);

  const {
    isIdle: oracleSubmissionsIdle,
    isLoading: oracleSubmissionsLoading,
    data: oracleSubmissions,
    error: oracleSubmissionsError
  } = useQuery<BtcToCurrencyOracleStatus<CollateralUnit>[], Error>(
    [ORACLE_ALL_LATEST_UPDATES_FETCHER, COLLATERAL_TOKEN, oracleTimeout, namesMap],
    allLatestSubmissionsFetcher,
    {
      enabled: !!oracleTimeout && !!namesMap
    }
  );
  useErrorHandler(oracleSubmissionsError);

  const columns = React.useMemo(
    () => [
      {
        Header: t('source'),
        accessor: 'source',
        classNames: ['text-center']
      },
      {
        Header: t('feed'),
        accessor: 'feed',
        classNames: ['text-center']
      },
      {
        Header: t('last_update'),
        accessor: 'lastUpdate',
        classNames: ['text-center'],
        Cell: function FormattedCell({ value }: { value: Date }) {
          return <>{formatDateTime(value)}</>;
        }
      },
      {
        Header: t('exchange_rate'),
        accessor: 'exchangeRate',
        classNames: ['text-center'],
        Cell: function FormattedCell({ value }: { value: BTCToCollateralTokenRate }) {
          return (
            <>
              1 BTC = {value.toHuman(5)} {COLLATERAL_TOKEN_SYMBOL}
            </>
          );
        }
      },
      {
        Header: t('status'),
        accessor: 'online',
        classNames: ['text-center'],
        Cell: function FormattedCell({ value }: { value: boolean }) {
          return (
            <div className={clsx('inline-flex', 'items-center', 'space-x-1')}>
              {value ? (
                <>
                  <CheckCircleIcon className='text-interlayConifer' />
                  <span className='text-interlayConifer'>{t('online')}</span>
                </>
              ) : (
                <>
                  <CancelIcon className='text-interlayCinnabar' />
                  <span className='text-interlayCinnabar'>{t('offline')}</span>
                </>
              )}
            </div>
          );
        }
      }
    ],
    [t]
  );

  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
    columns,
    data: oracleSubmissions ?? []
  });

  if (
    namesMapIdle ||
    namesMapLoading ||
    oracleTimeoutIdle ||
    oracleTimeoutLoading ||
    oracleSubmissionsIdle ||
    oracleSubmissionsLoading
  ) {
    return <PrimaryColorEllipsisLoader />;
  }
  if (!oracleSubmissions) {
    throw new Error('Something went wrong!');
  }

  return (
    <InterlayTableContainer className={clsx('space-y-6', 'container', 'mx-auto')}>
      <SectionTitle>{t('dashboard.oracles.oracles')}</SectionTitle>
      <InterlayTable {...getTableProps()}>
        <InterlayThead>
          {headerGroups.map((headerGroup: any) => (
            // eslint-disable-next-line react/jsx-key
            <InterlayTr {...headerGroup.getHeaderGroupProps()}>
              {headerGroup.headers.map((column: any) => (
                // eslint-disable-next-line react/jsx-key
                <InterlayTh
                  {...column.getHeaderProps([
                    {
                      className: clsx(column.classNames),
                      style: column.style
                    }
                  ])}
                >
                  {column.render('Header')}
                </InterlayTh>
              ))}
            </InterlayTr>
          ))}
        </InterlayThead>
        <InterlayTbody {...getTableBodyProps()}>
          {rows.map((row: any) => {
            prepareRow(row);

            return (
              // eslint-disable-next-line react/jsx-key
              <InterlayTr {...row.getRowProps()}>
                {row.cells.map((cell: any) => {
                  return (
                    // eslint-disable-next-line react/jsx-key
                    <InterlayTd
                      {...cell.getCellProps([
                        {
                          className: clsx(cell.column.classNames),
                          style: cell.column.style
                        }
                      ])}
                    >
                      {cell.render('Cell')}
                    </InterlayTd>
                  );
                })}
              </InterlayTr>
            );
          })}
        </InterlayTbody>
      </InterlayTable>
    </InterlayTableContainer>
  );
}
Example #27
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
IssueRequestsTable = (): JSX.Element => {
  const queryParams = useQueryParams();
  const { bridgeLoaded } = useSelector((state: StoreType) => state.general);
  const selectedPage = Number(queryParams.get(QUERY_PARAMETERS.PAGE)) || 1;
  const updateQueryParameters = useUpdateQueryParameters();
  const { t } = useTranslation();

  const columns = React.useMemo(
    () => [
      {
        Header: t('date_created'),
        classNames: ['text-left'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          return <>{formatDateTimePrecise(new Date(issue.request.timestamp))}</>;
        }
      },
      {
        Header: t('last_update'),
        classNames: ['text-left'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          let date;
          if (issue.execution) {
            date = issue.execution.timestamp;
          } else if (issue.cancellation) {
            date = issue.cancellation.timestamp;
          } else {
            date = issue.request.timestamp;
          }

          return <>{formatDateTimePrecise(new Date(date))}</>;
        }
      },
      {
        Header: t('issue_page.parachain_block'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          let height;
          if (issue.execution) {
            height = issue.execution.height.active;
          } else if (issue.cancellation) {
            height = issue.cancellation.height.active;
          } else {
            height = issue.request.height.active;
          }

          return <>{height}</>;
        }
      },
      {
        Header: t('issue_page.amount'),
        classNames: ['text-right'],
        // TODO: should type properly (`Relay`)
        Cell: function FormattedCell({ row: { original: issue } }: any) {
          let wrappedTokenAmount;
          if (issue.execution) {
            wrappedTokenAmount = issue.execution.amountWrapped;
          } else {
            wrappedTokenAmount = issue.request.amountWrapped;
          }

          return <>{displayMonetaryAmount(wrappedTokenAmount)}</>;
        }
      },
      {
        Header: t('issue_page.vault_dot_address'),
        accessor: 'vault',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: any }) {
          return <>{shortAddress(value.accountId)}</>;
        }
      },
      {
        Header: t('issue_page.vault_btc_address'),
        accessor: 'vaultBackingAddress',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: string }) {
          return <ExternalLink href={`${BTC_EXPLORER_ADDRESS_API}${value}`}>{shortAddress(value)}</ExternalLink>;
        }
      },
      {
        Header: t('status'),
        accessor: 'status',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: IssueStatus }) {
          return (
            <StatusCell
              status={{
                completed: value === IssueStatus.Completed,
                cancelled: value === IssueStatus.Cancelled,
                isExpired: value === IssueStatus.Expired,
                reimbursed: false
              }}
            />
          );
        }
      }
    ],
    [t]
  );

  const {
    isIdle: stableBtcConfirmationsIdle,
    isLoading: stableBtcConfirmationsLoading,
    data: stableBtcConfirmations,
    error: stableBtcConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableBitcoinConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(stableBtcConfirmationsError);

  const {
    isIdle: latestActiveBlockIdle,
    isLoading: latestActiveBlockLoading,
    data: latestParachainActiveBlock,
    error: latestActiveBlockError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'system', 'getCurrentActiveBlockNumber'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(latestActiveBlockError);

  const {
    isIdle: stableParachainConfirmationsIdle,
    isLoading: stableParachainConfirmationsLoading,
    data: stableParachainConfirmations,
    error: stableParachainConfirmationsError
  } = useQuery<number, Error>(
    [GENERIC_FETCHER, 'btcRelay', 'getStableParachainConfirmations'],
    genericFetcher<number>(),
    {
      enabled: !!bridgeLoaded
    }
  );
  useErrorHandler(stableParachainConfirmationsError);

  const selectedPageIndex = selectedPage - 1;

  const {
    isIdle: issuesIdle,
    isLoading: issuesLoading,
    data: issues,
    error: issuesError
    // TODO: should type properly (`Relay`)
  } = useQuery<any, Error>(
    [
      ISSUE_FETCHER,
      selectedPageIndex * TABLE_PAGE_LIMIT, // offset
      TABLE_PAGE_LIMIT // limit
    ],
    issueFetcher
  );
  useErrorHandler(issuesError);

  const {
    isIdle: issuesCountIdle,
    isLoading: issuesCountLoading,
    data: issuesCount,
    error: issuesCountError
    // TODO: should type properly (`Relay`)
  } = useQuery<GraphqlReturn<any>, Error>([GRAPHQL_FETCHER, issueCountQuery()], graphqlFetcher<GraphqlReturn<any>>());
  useErrorHandler(issuesCountError);

  const data =
    issues === undefined ||
    stableBtcConfirmations === undefined ||
    stableParachainConfirmations === undefined ||
    latestParachainActiveBlock === undefined
      ? []
      : issues.map(
          // TODO: should type properly (`Relay`)
          (issue: any) =>
            getIssueWithStatus(issue, stableBtcConfirmations, stableParachainConfirmations, latestParachainActiveBlock)
        );

  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
    columns,
    data
  });

  const renderContent = () => {
    if (
      stableBtcConfirmationsIdle ||
      stableBtcConfirmationsLoading ||
      stableParachainConfirmationsIdle ||
      stableParachainConfirmationsLoading ||
      latestActiveBlockIdle ||
      latestActiveBlockLoading ||
      issuesIdle ||
      issuesLoading ||
      issuesCountIdle ||
      issuesCountLoading
    ) {
      return <PrimaryColorEllipsisLoader />;
    }
    if (issuesCount === undefined) {
      throw new Error('Something went wrong!');
    }

    const handlePageChange = ({ selected: newSelectedPageIndex }: { selected: number }) => {
      updateQueryParameters({
        [QUERY_PARAMETERS.PAGE]: (newSelectedPageIndex + 1).toString()
      });
    };

    const totalIssueCount = issuesCount.data.issuesConnection.totalCount || 0;
    const pageCount = Math.ceil(totalIssueCount / TABLE_PAGE_LIMIT);

    return (
      <>
        <InterlayTable {...getTableProps()}>
          <InterlayThead>
            {/* TODO: should type properly */}
            {headerGroups.map((headerGroup: any) => (
              // eslint-disable-next-line react/jsx-key
              <InterlayTr {...headerGroup.getHeaderGroupProps()}>
                {/* TODO: should type properly */}
                {headerGroup.headers.map((column: any) => (
                  // eslint-disable-next-line react/jsx-key
                  <InterlayTh
                    {...column.getHeaderProps([
                      {
                        className: clsx(column.classNames),
                        style: column.style
                      }
                    ])}
                  >
                    {column.render('Header')}
                  </InterlayTh>
                ))}
              </InterlayTr>
            ))}
          </InterlayThead>
          <InterlayTbody {...getTableBodyProps()}>
            {/* TODO: should type properly */}
            {rows.map((row: any) => {
              prepareRow(row);

              return (
                // eslint-disable-next-line react/jsx-key
                <InterlayTr {...row.getRowProps()}>
                  {/* TODO: should type properly */}
                  {row.cells.map((cell: any) => {
                    return (
                      // eslint-disable-next-line react/jsx-key
                      <InterlayTd
                        {...cell.getCellProps([
                          {
                            className: clsx(cell.column.classNames),
                            style: cell.column.style
                          }
                        ])}
                      >
                        {cell.render('Cell')}
                      </InterlayTd>
                    );
                  })}
                </InterlayTr>
              );
            })}
          </InterlayTbody>
        </InterlayTable>
        {pageCount > 0 && (
          <div className={clsx('flex', 'justify-end')}>
            <InterlayPagination
              pageCount={pageCount}
              marginPagesDisplayed={2}
              pageRangeDisplayed={5}
              onPageChange={handlePageChange}
              forcePage={selectedPageIndex}
            />
          </div>
        )}
      </>
    );
  };

  return (
    <InterlayTableContainer className='space-y-6'>
      <SectionTitle>{t('issue_page.recent_requests')}</SectionTitle>
      {renderContent()}
    </InterlayTableContainer>
  );
}
Example #28
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
BlocksTable = (): JSX.Element => {
  const { t } = useTranslation();

  const queryParams = useQueryParams();
  const selectedPage = Number(queryParams.get(QUERY_PARAMETERS.PAGE)) || 1;
  const selectedPageIndex = selectedPage - 1;
  const updateQueryParameters = useUpdateQueryParameters();

  const {
    isIdle: btcBlocksIdle,
    isLoading: btcBlocksLoading,
    data: btcBlocks,
    error: btcBlocksError
    // TODO: should type properly (`Relay`)
  } = useQuery<GraphqlReturn<any>, Error>(
    [
      GRAPHQL_FETCHER,
      btcBlocksQuery(),
      {
        limit: TABLE_PAGE_LIMIT,
        offset: selectedPageIndex * TABLE_PAGE_LIMIT
      }
    ],
    graphqlFetcher<GraphqlReturn<any>>()
  );
  useErrorHandler(btcBlocksError);

  const {
    isIdle: btcBlocksCountIdle,
    isLoading: btcBlocksCountLoading,
    data: btcBlocksCount,
    error: btcBlocksCountError
    // TODO: should type properly (`Relay`)
  } = useQuery<GraphqlReturn<any>, Error>(
    [GRAPHQL_FETCHER, btcBlocksCountQuery()],
    graphqlFetcher<GraphqlReturn<any>>()
  );
  useErrorHandler(btcBlocksCountError);

  const columns = React.useMemo(
    () => [
      {
        Header: t('dashboard.relay.block_height'),
        accessor: 'backingHeight',
        classNames: ['text-right']
      },
      {
        Header: t('dashboard.relay.block_hash'),
        accessor: 'blockHash',
        classNames: ['text-right'],
        Cell: function FormattedCell({ value }: { value: string }) {
          const hash = stripHexPrefix(value);
          return <ExternalLink href={`${BTC_EXPLORER_BLOCK_API}${hash}`}>{hash}</ExternalLink>;
        }
      },
      {
        Header: t('dashboard.relay.inclusion_timestamp'),
        accessor: 'timestamp',
        classNames: ['text-left'],
        Cell: function FormattedCell({ value }: { value: string }) {
          return <>{formatDateTimePrecise(new Date(value))}</>;
        }
      },
      {
        Header: t('dashboard.relay.inclusion_block'),
        accessor: 'relayedAtHeight',
        classNames: ['text-right'],
        Cell: function FormattedCell({ value }: { value: any }) {
          return <>{value.absolute}</>;
        }
      },
      {
        Header: t('dashboard.relay.relayed_by'),
        accessor: 'relayer',
        classNames: ['text-right']
      }
    ],
    [t]
  );

  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({
    columns,
    data: btcBlocks?.data?.relayedBlocks ?? []
  });

  if (btcBlocksIdle || btcBlocksLoading || btcBlocksCountIdle || btcBlocksCountLoading) {
    return <PrimaryColorEllipsisLoader />;
  }
  if (!btcBlocks) {
    throw new Error('Something went wrong!');
  }
  if (!btcBlocksCount) {
    throw new Error('Something went wrong!');
  }

  const handlePageChange = ({ selected: newSelectedPageIndex }: { selected: number }) => {
    updateQueryParameters({
      [QUERY_PARAMETERS.PAGE]: (newSelectedPageIndex + 1).toString()
    });
  };

  const pageCount = Math.ceil((btcBlocksCount.data.relayedBlocksConnection.totalCount || 0) / TABLE_PAGE_LIMIT);

  return (
    <InterlayTableContainer className={clsx('space-y-6', 'container', 'mx-auto')}>
      <SectionTitle>{t('dashboard.relay.blocks')}</SectionTitle>
      <InterlayTable {...getTableProps()}>
        <InterlayThead>
          {headerGroups.map((headerGroup: any) => (
            // eslint-disable-next-line react/jsx-key
            <InterlayTr {...headerGroup.getHeaderGroupProps()}>
              {headerGroup.headers.map((column: any) => (
                // eslint-disable-next-line react/jsx-key
                <InterlayTh
                  {...column.getHeaderProps([
                    {
                      className: clsx(column.classNames),
                      style: column.style
                    }
                  ])}
                >
                  {column.render('Header')}
                </InterlayTh>
              ))}
            </InterlayTr>
          ))}
        </InterlayThead>
        <InterlayTbody {...getTableBodyProps()}>
          {rows.map((row: any) => {
            prepareRow(row);

            return (
              // eslint-disable-next-line react/jsx-key
              <InterlayTr {...row.getRowProps()}>
                {row.cells.map((cell: any) => {
                  return (
                    // eslint-disable-next-line react/jsx-key
                    <InterlayTd
                      {...cell.getCellProps([
                        {
                          className: clsx(cell.column.classNames),
                          style: cell.column.style
                        }
                      ])}
                    >
                      {cell.render('Cell')}
                    </InterlayTd>
                  );
                })}
              </InterlayTr>
            );
          })}
        </InterlayTbody>
      </InterlayTable>
      {pageCount > 0 && (
        <div className={clsx('flex', 'justify-end')}>
          <InterlayPagination
            pageCount={pageCount}
            marginPagesDisplayed={2}
            pageRangeDisplayed={5}
            onPageChange={handlePageChange}
            forcePage={selectedPageIndex}
          />
        </div>
      )}
    </InterlayTableContainer>
  );
}
Example #29
Source File: index.tsx    From interbtc-ui with Apache License 2.0 4 votes vote down vote up
OracleStatusCard = ({ hasLinks }: Props): JSX.Element => {
  const { t } = useTranslation();
  const { bridgeLoaded } = useSelector((state: StoreType) => state.general);

  const {
    isIdle: oracleTimeoutIdle,
    isLoading: oracleTimeoutLoading,
    data: oracleTimeout,
    error: oracleTimeoutError
  } = useQuery<number, Error>([GENERIC_FETCHER, 'oracle', 'getOnlineTimeout'], genericFetcher<number>(), {
    enabled: !!bridgeLoaded
  });
  useErrorHandler(oracleTimeoutError);

  const {
    isIdle: oracleStatusIdle,
    isLoading: oracleStatusLoading,
    data: oracleStatus,
    error: oracleStatusError
  } = useQuery<BtcToCurrencyOracleStatus<CollateralUnit> | undefined, Error>(
    [ORACLE_LATEST_EXCHANGE_RATE_FETCHER, COLLATERAL_TOKEN, oracleTimeout],
    latestExchangeRateFetcher,
    {
      enabled: !!oracleTimeout
    }
  );
  useErrorHandler(oracleStatusError);

  const renderContent = () => {
    // TODO: should use skeleton loaders
    if (oracleStatusIdle || oracleStatusLoading || oracleTimeoutIdle || oracleTimeoutLoading) {
      return <>Loading...</>;
    }

    if (oracleTimeout === undefined) {
      throw new Error('Something went wrong!');
    }

    const exchangeRate = oracleStatus?.exchangeRate;
    const oracleOnline = oracleStatus && oracleStatus.online;

    let statusText;
    let statusCircleText;
    if (exchangeRate === undefined) {
      statusText = t('dashboard.oracles.not_available');
      statusCircleText = t('unavailable');
    } else if (oracleOnline === true) {
      statusText = t('dashboard.oracles.online');
      statusCircleText = t('online');
    } else if (oracleOnline === false) {
      statusText = t('dashboard.oracles.offline');
      statusCircleText = t('offline');
    } else {
      throw new Error('Something went wrong!');
    }

    return (
      <>
        <Stats
          leftPart={
            <>
              <StatsDt>{t('dashboard.oracles.oracles_are')}</StatsDt>
              <StatsDd
                className={clsx(
                  { 'text-interlayConifer': oracleOnline === true },
                  { 'text-interlayCinnabar': oracleOnline === false }
                )}
              >
                {statusText}
              </StatsDd>
            </>
          }
          rightPart={<>{hasLinks && <StatsRouterLink to={PAGES.DASHBOARD_ORACLES}>View oracles</StatsRouterLink>}</>}
        />
        <Ring64
          className={clsx(
            'mx-auto',
            { 'ring-interlayConifer': oracleOnline === true },
            { 'ring-interlayCinnabar': oracleOnline === false }
          )}
        >
          <Ring64Title
            className={clsx(
              { 'text-interlayConifer': oracleOnline === true },
              { 'text-interlayCinnabar': oracleOnline === false }
            )}
          >
            {statusCircleText}
          </Ring64Title>
          {exchangeRate && (
            <Ring64Value>
              {exchangeRate.toHuman(5)} BTC/{COLLATERAL_TOKEN_SYMBOL}
            </Ring64Value>
          )}
        </Ring64>
      </>
    );
  };

  return <DashboardCard>{renderContent()}</DashboardCard>;
}