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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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>
{displayMonetaryAmount(punishmentCollateralTokenAmount)} {COLLATERAL_TOKEN_SYMBOL}
</PrimaryColorSpan>
<span> {`(≈ $ ${getUsdAmount(punishmentCollateralTokenAmount, prices.collateralToken?.usd)})`}</span>
<span>
{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>
{displayMonetaryAmount(punishmentCollateralTokenAmount)} {COLLATERAL_TOKEN_SYMBOL}
</PrimaryColorSpan>
<span>
{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>
{displayMonetaryAmount(collateralTokenAmount)} {COLLATERAL_TOKEN_SYMBOL}
</PrimaryColorSpan>
<span>
{t('redeem_page.with_added', {
amountPrice: getUsdAmount(collateralTokenAmount, prices.collateralToken?.usd)
})}
</span>
<PrimaryColorSpan>
{displayMonetaryAmount(punishmentCollateralTokenAmount)} {COLLATERAL_TOKEN_SYMBOL}
</PrimaryColorSpan>
<span>
{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 |
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 |
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 |
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 |
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 |
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 |
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')}
{renderNewCollateralizationLabel()}
</p>
{renderSubmitButton()}
</form>
</InterlayModalInnerWrapper>
</InterlayModal>
);
}
Example #17
Source File: index.tsx From interbtc-ui with Apache License 2.0 | 4 votes |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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()}
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 |
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 |
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 |
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 |
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 |
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>;
}