react-icons/fa#FaExclamationCircle TypeScript Examples
The following examples show how to use
react-icons/fa#FaExclamationCircle.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: Toast.tsx From convoychat with GNU General Public License v3.0 | 6 votes |
toast = {
error: (message: string) => {
notify.addNotification({
type: "warning",
content: (
<Toast type="warning" icon={FaExclamationCircle}>
{message}
</Toast>
),
...(defaultConfig as any),
});
},
success: (message: string) => {
notify.addNotification({
type: "success",
content: (
<Toast type="success" icon={FaCheckCircle}>
{message}
</Toast>
),
...(defaultConfig as any),
});
},
info: (message: string) => {
notify.addNotification({
type: "info",
content: (
<Toast type="info" icon={FaInfoCircle}>
{message}
</Toast>
),
...(defaultConfig as any),
});
},
}
Example #2
Source File: index.tsx From interbtc-ui with Apache License 2.0 | 6 votes |
CancelledIssueRequest = (): JSX.Element => {
const { t } = useTranslation();
return (
<RequestWrapper className='px-12'>
<h2 className={clsx('text-3xl', 'font-medium', 'text-interlayCinnabar')}>{t('cancelled')}</h2>
<FaTimesCircle className={clsx('w-40', 'h-40', 'text-interlayCinnabar')} />
<p
className={clsx(
{ 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
{ 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA },
'text-justify'
)}
>
{t('issue_page.you_did_not_send', {
wrappedTokenSymbol: WRAPPED_TOKEN_SYMBOL
})}
</p>
{/* TODO: could componentize */}
<div>
<h6 className={clsx('flex', 'items-center', 'justify-center', 'space-x-0.5', 'text-interlayCinnabar')}>
<span>{t('note')}</span>
<FaExclamationCircle />
</h6>
<p
className={clsx(
'text-justify',
{ 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
{ 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
)}
>
{t('issue_page.contact_team')}
</p>
</div>
</RequestWrapper>
);
}
Example #3
Source File: index.tsx From interbtc-ui with Apache License 2.0 | 4 votes |
SubmittedRedeemRequestModal = ({
open,
onClose,
request
}: CustomProps & Omit<ModalProps, 'children'>): JSX.Element => {
const { t } = useTranslation();
const { prices } = useSelector((state: StoreType) => state.general);
const focusRef = React.useRef(null);
return (
<InterlayModal initialFocus={focusRef} open={open} onClose={onClose}>
<InterlayModalInnerWrapper className={clsx('p-8', 'max-w-lg')}>
<CloseIconButton ref={focusRef} onClick={onClose} />
<div className={clsx('flex', 'flex-col', 'space-y-8')}>
<h4 className={clsx('text-2xl', 'text-interlayCalifornia', 'font-medium', 'text-center')}>
{t('redeem_page.withdraw')}
</h4>
<div className='space-y-6'>
<div className='space-y-1'>
<h5
className={clsx(
'font-medium',
'text-interlayCalifornia',
'flex',
'items-center',
'justify-center',
'space-x-1'
)}
>
<FaExclamationCircle className='inline' />
<span>{t('redeem_page.redeem_processed')}</span>
</h5>
<h1 className={clsx('text-3xl', 'font-medium', 'space-x-1', 'text-center')}>
<span>{t('redeem_page.will_receive_BTC')}</span>
<span className='text-interlayCalifornia'>{displayMonetaryAmount(request.amountBTC)} BTC</span>
</h1>
<span
className={clsx(
'block',
{ 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
{ 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA },
'text-2xl',
'text-center'
)}
>
{`≈ $${getUsdAmount(request.amountBTC, prices.bitcoin?.usd)}`}
</span>
</div>
<div>
<label
htmlFor={USER_BTC_ADDRESS}
className={clsx(
{ 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
{ 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
)}
>
{t('redeem_page.btc_destination_address')}
</label>
<span
id={USER_BTC_ADDRESS}
// TODO: could componentize
className={clsx('block', 'p-2.5', 'border-2', 'font-medium', 'rounded-lg', 'text-center')}
>
{request.userBTCAddress}
</span>
</div>
<div>
<p>{t('redeem_page.we_will_inform_you_btc')}</p>
<p
className={clsx(
{ 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
{ 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
)}
>
{t('redeem_page.typically_takes')}
</p>
</div>
</div>
<InterlayRouterLink
to={{
pathname: PAGES.TRANSACTIONS,
search: queryString.stringify({
[QUERY_PARAMETERS.REDEEM_REQUEST_ID]: request.id
})
}}
>
<InterlayDefaultContainedButton onClick={onClose} className='w-full'>
{t('redeem_page.view_progress')}
</InterlayDefaultContainedButton>
</InterlayRouterLink>
</div>
</InterlayModalInnerWrapper>
</InterlayModal>
);
}
Example #4
Source File: index.tsx From interbtc-ui with Apache License 2.0 | 4 votes |
BTCPaymentPendingStatusUI = ({ request }: Props): JSX.Element => {
const { t } = useTranslation();
const { prices } = useSelector((state: StoreType) => state.general);
const { issuePeriod } = useSelector((state: StoreType) => state.issue);
const amountBTCToSend = (request.wrappedAmount || request.request.amountWrapped).add(
request.bridgeFee || request.request.bridgeFeeWrapped
);
const [initialLeftSeconds, setInitialLeftSeconds] = React.useState<number>();
React.useEffect(() => {
// TODO: double-check `request.request?.timestamp`
// Date.now() is an approximation, used with the parachain response until we can get the block timestamp later
const requestCreationTimestamp = request.request?.timestamp ?? Date.now();
const requestTimestamp = Math.floor(new Date(requestCreationTimestamp).getTime() / 1000);
const theInitialLeftSeconds = requestTimestamp + issuePeriod - Math.floor(Date.now() / 1000);
setInitialLeftSeconds(theInitialLeftSeconds);
}, [request.request, issuePeriod]);
return (
<div className='space-y-8'>
<div className={clsx('flex', 'flex-col', 'justify-center', 'items-center')}>
<div className='text-xl'>
{t('send')}
<span className='text-interlayCalifornia'> {displayMonetaryAmount(amountBTCToSend)} </span>
BTC
</div>
<span
className={clsx(
{ 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
{ 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA },
'block'
)}
>
{`≈ $ ${getUsdAmount(amountBTCToSend, prices.bitcoin?.usd)}`}
</span>
</div>
<div>
<p
className={clsx(
'text-center',
{ 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
{ 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
)}
>
{t('issue_page.single_transaction')}
</p>
{/* TODO: should improve UX */}
<InterlayTooltip label={t('click_to_copy')}>
<span
className={clsx('block', 'p-2.5', 'border-2', 'font-medium', 'rounded-lg', 'cursor-pointer', 'text-center')}
onClick={() => copyToClipboard(request.vaultWrappedAddress || request.vaultBackingAddress)}
>
{request.vaultWrappedAddress || request.vaultBackingAddress}
</span>
</InterlayTooltip>
{initialLeftSeconds && (
<p className={clsx('flex', 'justify-center', 'items-center', 'space-x-1')}>
<span
className={clsx(
{ 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
{ 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA },
'capitalize'
)}
>
{t('issue_page.within')}
</span>
<Timer initialLeftSeconds={initialLeftSeconds} />
</p>
)}
</div>
<p className='space-x-1'>
<span
className={clsx(
{ 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
{ 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA },
'break-all'
)}
>
{t('issue_page.warning_mbtc_wallets')}
</span>
<span className='text-interlayCalifornia'>{displayMonetaryAmount(amountBTCToSend.mul(1000))} mBTC</span>
</p>
<QRCode
includeMargin
className='mx-auto'
// eslint-disable-next-line max-len
value={`bitcoin:${request.vaultWrappedAddress || request.vaultBackingAddress}?amount=${displayMonetaryAmount(
amountBTCToSend
)}`}
/>
<div
className={clsx(
{ 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
{ 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
)}
>
<div className={clsx('inline-flex', 'items-center', 'space-x-0.5', 'mr-1')}>
<span>{t('note')}</span>
<FaExclamationCircle />
<span>:</span>
</div>
<span>{t('issue_page.waiting_deposit')}</span>
</div>
</div>
);
}
Example #5
Source File: index.tsx From interbtc-ui with Apache License 2.0 | 4 votes |
RetriedRedeemRequest = ({ request }: Props): JSX.Element => {
const { t } = useTranslation();
const { bridgeLoaded, prices } = useSelector((state: StoreType) => state.general);
const [punishmentCollateralTokenAmount, setPunishmentCollateralTokenAmount] = React.useState(
newMonetaryAmount(0, COLLATERAL_TOKEN)
);
React.useEffect(() => {
if (!bridgeLoaded) return;
if (!request) return;
// TODO: should add loading UX
(async () => {
try {
const [punishmentFee, btcDotRate] = await Promise.all([
window.bridge.vaults.getPunishmentFee(),
window.bridge.oracle.getExchangeRate(COLLATERAL_TOKEN)
]);
const btcAmount = request.request.requestedAmountBacking;
const theBurnDOTAmount = btcDotRate.toCounter(btcAmount);
const thePunishmentDOTAmount = theBurnDOTAmount.mul(new Big(punishmentFee));
setPunishmentCollateralTokenAmount(thePunishmentDOTAmount);
} catch (error) {
// TODO: should add error handling UX
console.log('[RetriedRedeemRequest useEffect] error.message => ', error.message);
}
})();
}, [request, bridgeLoaded]);
return (
<RequestWrapper>
<h2 className={clsx('text-3xl', 'font-medium', 'text-interlayConifer')}>
{t('redeem_page.compensation_success')}
</h2>
<p className='w-full'>{t('redeem_page.compensation_notice')}</p>
<p className='font-medium'>
<PrimaryColorSpan>{t('redeem_page.recover_receive_dot')}</PrimaryColorSpan>
<PrimaryColorSpan>
{`${displayMonetaryAmount(punishmentCollateralTokenAmount)} ${COLLATERAL_TOKEN_SYMBOL}`}
</PrimaryColorSpan>
<span> ({`≈ $${getUsdAmount(punishmentCollateralTokenAmount, prices.collateralToken?.usd)}`})</span>
<PrimaryColorSpan> {t('redeem_page.recover_receive_total')}.</PrimaryColorSpan>
</p>
<div className='w-full'>
<PriceInfo
title={
<h5
className={clsx(
{ 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
{ 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
)}
>
{t('redeem_page.compensation_payment')}
</h5>
}
unitIcon={<CollateralTokenLogoIcon width={20} />}
value={displayMonetaryAmount(punishmentCollateralTokenAmount)}
unitName={COLLATERAL_TOKEN_SYMBOL}
approxUSD={getUsdAmount(punishmentCollateralTokenAmount, prices.collateralToken?.usd)}
/>
<Hr2 className={clsx('border-t-2', 'my-2.5')} />
<PriceInfo
className='w-full'
title={
<h5
className={clsx(
{ 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
{ 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
)}
>
{t('you_received')}
</h5>
}
unitIcon={<CollateralTokenLogoIcon width={20} />}
value={displayMonetaryAmount(punishmentCollateralTokenAmount)}
unitName={COLLATERAL_TOKEN_SYMBOL}
approxUSD={getUsdAmount(punishmentCollateralTokenAmount, prices.collateralToken?.usd)}
/>
</div>
<ExternalLink className='text-sm' href={getPolkadotLink(request.request.height.absolute)}>
{t('issue_page.view_parachain_block')}
</ExternalLink>
<div className='w-full'>
<h6 className={clsx('flex', 'items-center', 'justify-center', 'space-x-0.5', 'text-interlayCinnabar')}>
<span>{t('note')}</span>
<FaExclamationCircle />
</h6>
<p
className={clsx(
'text-justify',
{ 'text-interlayTextSecondaryInLightMode': process.env.REACT_APP_RELAY_CHAIN_NAME === POLKADOT },
{ 'dark:text-kintsugiTextSecondaryInDarkMode': process.env.REACT_APP_RELAY_CHAIN_NAME === KUSAMA }
)}
>
{t('redeem_page.retry_new_redeem')}
</p>
</div>
</RequestWrapper>
);
}
Example #6
Source File: index.tsx From interbtc-ui with Apache License 2.0 | 4 votes |
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 #7
Source File: PaymentView.tsx From polkabtc-ui with Apache License 2.0 | 4 votes |
PaymentView = ({
request
}: Props): JSX.Element => {
const { t } = useTranslation();
const { prices } = useSelector((state: StoreType) => state.general);
const { issuePeriod } = useSelector((state: StoreType) => state.issue);
const amount = new Big(request.requestedAmountPolkaBTC).add(new Big(request.fee)).toString();
const [initialLeftSeconds, setInitialLeftSeconds] = React.useState<number>();
React.useEffect(() => {
if (!request.timestamp) return;
const requestTimestamp = Math.floor(new Date(Number(request.timestamp)).getTime() / 1000);
const theInitialLeftSeconds = requestTimestamp + issuePeriod - Math.floor(Date.now() / 1000);
setInitialLeftSeconds(theInitialLeftSeconds);
}, [
request.timestamp,
issuePeriod
]);
return (
<div className='space-y-8'>
<div
className={clsx(
'flex',
'flex-col',
'justify-center',
'items-center'
)}>
<div
className='text-xl'>
{t('send')}
<span className='text-interlayTreePoppy'> {request.amountBTC} </span>
BTC
</div>
<span
className={clsx(
'text-textSecondary',
'block'
)}>
{`≈ $ ${getUsdAmount(request.amountBTC, prices.bitcoin.usd)}`}
</span>
</div>
<div>
<p
className={clsx(
'text-center',
'text-textSecondary'
)}>
{t('issue_page.single_transaction')}
</p>
{/* TODO: should improve the UX */}
<Tooltip overlay={t('click_to_copy')}>
<span
className={clsx(
'block',
'p-2.5',
'border-2',
'font-medium',
'rounded-lg',
'cursor-pointer',
'text-center'
)}
onClick={() => copyToClipboard(request.vaultBTCAddress)}>
{request.vaultBTCAddress}
</span>
</Tooltip>
<p
className={clsx(
'flex',
'justify-center',
'items-center',
'space-x-1'
)}>
<span className='text-textSecondary'>{t('issue_page.within')}</span>
{initialLeftSeconds && <Timer initialLeftSeconds={initialLeftSeconds} />}
</p>
</div>
<p className='space-x-1'>
<span
className={clsx(
'text-textSecondary',
'break-all'
)}>
{t('issue_page.warning_mbtc_wallets')}
</span>
<span className='text-interlayTreePoppy'>
{displayBtcAmount(new Big(request.amountBTC).mul(1000).toString())} mBTC
</span>
</p>
<QRCode
className='mx-auto'
value={`bitcoin:${request.vaultBTCAddress}?amount=${amount}`} />
<div
className={clsx(
'text-textSecondary'
)}>
<div
className={clsx(
'inline-flex',
'items-center',
'space-x-0.5',
'mr-1'
)}>
<span>{t('note')}</span>
<FaExclamationCircle />
<span>:</span>
</div>
<span>{t('issue_page.waiting_deposit')}</span>
</div>
</div>
);
}
Example #8
Source File: EnterAmountAndAddress.tsx From polkabtc-ui with Apache License 2.0 | 4 votes |
EnterAmountAndAddress = (): JSX.Element | null => {
const dispatch = useDispatch();
const { t } = useTranslation();
const [status, setStatus] = React.useState(STATUSES.IDLE);
const [error, setError] = React.useState<Error | null>(null);
const usdPrice = useSelector((state: StoreType) => state.general.prices.bitcoin.usd);
const {
balancePolkaBTC,
polkaBtcLoaded,
address,
bitcoinHeight,
btcRelayHeight,
prices,
parachainStatus
} = useSelector((state: StoreType) => state.general);
const premiumRedeemSelected = useSelector((state: StoreType) => state.redeem.premiumRedeem);
const {
register,
handleSubmit,
formState: { errors },
watch,
setError: setFormError
} = useForm<RedeemForm>({
mode: 'onChange'
});
const polkaBTCAmount = watch(POLKA_BTC_AMOUNT);
const [dustValue, setDustValue] = React.useState('0');
const [redeemFee, setRedeemFee] = React.useState('0');
const [redeemFeeRate, setRedeemFeeRate] = React.useState(new Big(0.005));
const [btcToDotRate, setBtcToDotRate] = React.useState(new Big(0));
const [premiumRedeemVaults, setPremiumRedeemVaults] = React.useState<Map<AccountId, Big>>(new Map());
const [premiumRedeemFee, setPremiumRedeemFee] = React.useState(new Big(0));
const [currentInclusionFee, setCurrentInclusionFee] = React.useState(new Big(0));
const [submitStatus, setSubmitStatus] = React.useState(STATUSES.IDLE);
const [submitError, setSubmitError] = React.useState<Error | null>(null);
React.useEffect(() => {
if (!polkaBtcLoaded) return;
if (!polkaBTCAmount) return;
if (!redeemFeeRate) return;
const bigPolkaBTCAmount = new Big(polkaBTCAmount);
const theRedeemFee = bigPolkaBTCAmount.mul(redeemFeeRate);
setRedeemFee(theRedeemFee.toString());
}, [
polkaBtcLoaded,
polkaBTCAmount,
redeemFeeRate
]);
React.useEffect(() => {
if (!polkaBtcLoaded) return;
(async () => {
try {
setStatus(STATUSES.PENDING);
const [
dustValueResult,
premiumRedeemVaultsResult,
premiumRedeemFeeResult,
btcToDotRateResult,
redeemFeeRateResult,
currentInclusionFeeResult
] = await Promise.allSettled([
window.polkaBTC.redeem.getDustValue(),
window.polkaBTC.vaults.getPremiumRedeemVaults(),
window.polkaBTC.redeem.getPremiumRedeemFee(),
window.polkaBTC.oracle.getExchangeRate(),
window.polkaBTC.redeem.getFeeRate(),
window.polkaBTC.redeem.getCurrentInclusionFee()
]);
if (dustValueResult.status === 'rejected') {
throw new Error(dustValueResult.reason);
}
if (premiumRedeemFeeResult.status === 'rejected') {
throw new Error(premiumRedeemFeeResult.reason);
}
if (btcToDotRateResult.status === 'rejected') {
throw new Error(btcToDotRateResult.reason);
}
if (redeemFeeRateResult.status === 'rejected') {
throw new Error(redeemFeeRateResult.reason);
}
if (currentInclusionFeeResult.status === 'rejected') {
throw new Error(currentInclusionFeeResult.reason);
}
if (premiumRedeemVaultsResult.status === 'fulfilled') {
setPremiumRedeemVaults(premiumRedeemVaultsResult.value);
}
setDustValue(dustValueResult.value.toString());
setPremiumRedeemFee(new Big(premiumRedeemFeeResult.value));
setBtcToDotRate(btcToDotRateResult.value);
setRedeemFeeRate(redeemFeeRateResult.value);
setCurrentInclusionFee(currentInclusionFeeResult.value);
setStatus(STATUSES.RESOLVED);
} catch (error) {
setStatus(STATUSES.REJECTED);
setError(error);
}
})();
}, [polkaBtcLoaded]);
if (status === STATUSES.REJECTED && error) {
return (
<ErrorHandler error={error} />
);
}
if (status === STATUSES.IDLE || status === STATUSES.PENDING) {
return (
<div
className={clsx(
'flex',
'justify-center'
)}>
<EllipsisLoader dotClassName='bg-interlayTreePoppy-400' />
</div>
);
}
const onSubmit = async (data: RedeemForm) => {
try {
setSubmitStatus(STATUSES.PENDING);
const polkaBTCAmount = new Big(data[POLKA_BTC_AMOUNT]);
// Differentiate between premium and regular redeem
let vaultId;
if (premiumRedeemSelected) {
// Select a vault from the premium redeem vault list
for (const [id, redeemableTokens] of premiumRedeemVaults) {
if (redeemableTokens >= polkaBTCAmount) {
vaultId = id;
break;
}
}
if (vaultId === undefined) {
let maxAmount = new Big(0);
for (const redeemableTokens of premiumRedeemVaults.values()) {
if (maxAmount < redeemableTokens) {
maxAmount = redeemableTokens;
}
}
setFormError(POLKA_BTC_AMOUNT, {
type: 'manual',
message: t('redeem_page.error_max_premium_redeem', { maxPremiumRedeem: maxAmount.toString() })
});
return;
}
} else {
const vaults = await window.polkaBTC.vaults.getVaultsWithRedeemableTokens();
vaultId = getRandomVaultIdWithCapacity(Array.from(vaults || new Map()), polkaBTCAmount);
}
// FIXME: workaround to make premium redeem still possible
const relevantVaults = new Map<AccountId, Big>();
const id = window.polkaBTC.api.createType(ACCOUNT_ID_TYPE_NAME, vaultId);
// FIXME: a bit of a dirty workaround with the capacity
relevantVaults.set(id, polkaBTCAmount.mul(2));
const result = await window.polkaBTC.redeem.request(polkaBTCAmount, data[BTC_ADDRESS], true, 0, relevantVaults);
// TODO: handle redeem aggregator
const redeemRequest = await parachainToUIRedeemRequest(result[0].id, result[0].redeemRequest);
setSubmitStatus(STATUSES.RESOLVED);
// Get the redeem id from the request redeem event
const redeemId = stripHexPrefix(result[0].id.toString());
dispatch(changeRedeemIdAction(redeemId));
// Update the redeem status
dispatch(updateBalancePolkaBTCAction(new Big(balancePolkaBTC).sub(new Big(data[POLKA_BTC_AMOUNT])).toString()));
dispatch(addRedeemRequestAction(redeemRequest));
dispatch(changeRedeemStepAction('REDEEM_INFO'));
} catch (error) {
setSubmitStatus(STATUSES.REJECTED);
setSubmitError(error);
}
};
const validatePolkaBTCAmount = (value: number): string | undefined => {
const bigValue = new Big(value);
const minValue = new Big(dustValue).add(currentInclusionFee).add(new Big(redeemFee));
if (bigValue.gt(new Big(balancePolkaBTC))) {
return `${t('redeem_page.current_balance')}${balancePolkaBTC}`;
} else if (bigValue.lte(minValue)) {
return `${t('redeem_page.amount_greater_dust_inclusion')}${minValue} BTC).`;
}
if (!address) {
return t('redeem_page.must_select_account_warning');
}
if (!polkaBtcLoaded) {
return 'PolkaBTC must be loaded!';
}
if (bitcoinHeight - btcRelayHeight > BLOCKS_BEHIND_LIMIT) {
return t('issue_page.error_more_than_6_blocks_behind');
}
if (btcToSat(value.toString()) === undefined) {
return 'Invalid PolkaBTC amount input!';
}
const polkaBTCAmountInteger = value.toString().split('.')[0];
if (polkaBTCAmountInteger.length > BALANCE_MAX_INTEGER_LENGTH) {
return 'Input value is too high!';
}
return undefined;
};
const handlePremiumRedeemToggle = () => {
// TODO: should not use redux
dispatch(togglePremiumRedeemAction(!premiumRedeemSelected));
};
const redeemFeeInBTC = displayBtcAmount(redeemFee);
const redeemFeeInUSD = getUsdAmount(redeemFee, prices.bitcoin.usd);
const totalBTC =
polkaBTCAmount ?
displayBtcAmount(new Big(polkaBTCAmount).sub(new Big(redeemFee)).sub(currentInclusionFee)) :
'0';
const totalBTCInUSD = getUsdAmount(totalBTC, prices.bitcoin.usd);
const totalDOT =
polkaBTCAmount ?
new Big(polkaBTCAmount).mul(btcToDotRate).mul(premiumRedeemFee).toString() :
'0';
const totalDOTInUSD = getUsdAmount(totalDOT, prices.polkadot.usd);
const bitcoinNetworkFeeInBTC = displayBtcAmount(currentInclusionFee);
const bitcoinNetworkFeeInUSD = getUsdAmount(currentInclusionFee, prices.bitcoin.usd);
if (status === STATUSES.RESOLVED) {
return (
<>
<form
className='space-y-8'
onSubmit={handleSubmit(onSubmit)}>
<h4
className={clsx(
'font-medium',
'text-center',
'text-interlayTreePoppy'
)}>
{t('redeem_page.you_will_receive')}
</h4>
<PolkaBTCField
id='polka-btc-amount'
name={POLKA_BTC_AMOUNT}
type='number'
label='PolkaBTC'
step='any'
placeholder='0.00'
min={0}
ref={register({
required: {
value: true,
message: t('redeem_page.please_enter_amount')
},
validate: value => validatePolkaBTCAmount(value)
})}
approxUSD={`≈ $ ${getUsdAmount(polkaBTCAmount || '0', usdPrice)}`}
error={!!errors[POLKA_BTC_AMOUNT]}
helperText={errors[POLKA_BTC_AMOUNT]?.message} />
<ParachainStatusInfo status={parachainStatus} />
<TextField
id='btc-address'
name={BTC_ADDRESS}
type='text'
label='BTC Address'
placeholder={t('enter_btc_address')}
ref={register({
required: {
value: true,
message: t('redeem_page.enter_btc')
},
pattern: {
value: BTC_ADDRESS_REGEX, // TODO: regex need to depend on global mainnet | testnet parameter
message: t('redeem_page.valid_btc_address')
}
})}
error={!!errors[BTC_ADDRESS]}
helperText={errors[BTC_ADDRESS]?.message} />
{premiumRedeemVaults.size > 0 && (
<div
className={clsx(
'flex',
'justify-center',
'items-center',
'space-x-4'
)}>
<div
className={clsx(
'flex',
'items-center',
'space-x-1'
)}>
<span>{t('redeem_page.premium_redeem')}</span>
<Tooltip overlay={t('redeem_page.premium_redeem_info')}>
<FaExclamationCircle />
</Tooltip>
</div>
<Toggle
checked={premiumRedeemSelected}
onChange={handlePremiumRedeemToggle} />
</div>
)}
<PriceInfo
title={
<h5 className='text-textSecondary'>
{t('bridge_fee')}
</h5>
}
unitIcon={
<BitcoinLogoIcon
width={23}
height={23} />
}
value={redeemFeeInBTC}
unitName='BTC'
approxUSD={redeemFeeInUSD} />
<PriceInfo
title={
<h5 className='text-textSecondary'>
{t('bitcoin_network_fee')}
</h5>
}
unitIcon={
<BitcoinLogoIcon
width={23}
height={23} />
}
value={bitcoinNetworkFeeInBTC}
unitName='BTC'
approxUSD={bitcoinNetworkFeeInUSD} />
<hr
className={clsx(
'border-t-2',
'my-2.5',
'border-textSecondary'
)} />
<PriceInfo
title={
<h5 className='text-textPrimary'>
{t('you_will_receive')}
</h5>
}
unitIcon={
<BitcoinLogoIcon
width={23}
height={23} />
}
value={totalBTC}
unitName='BTC'
approxUSD={totalBTCInUSD} />
{premiumRedeemSelected && (
<PriceInfo
title={
<h5 className='text-interlayMalachite'>
{t('redeem_page.earned_premium')}
</h5>
}
unitIcon={
<PolkadotLogoIcon
width={23}
height={23} />
}
value={totalDOT}
unitName='DOT'
approxUSD={totalDOTInUSD} />
)}
<InterlayButton
type='submit'
style={{ display: 'flex' }}
className='mx-auto'
variant='contained'
color='primary'
disabled={parachainStatus !== ParachainStatus.Running}
pending={submitStatus === STATUSES.PENDING}>
{t('confirm')}
</InterlayButton>
</form>
{(submitStatus === STATUSES.REJECTED && submitError) && (
<ErrorModal
open={!!submitError}
onClose={() => {
setSubmitStatus(STATUSES.IDLE);
setSubmitError(null);
}}
title='Error'
description={
typeof submitError === 'string' ?
submitError :
submitError.message
} />
)}
</>
);
}
return null;
}