Skip to content

Commit

Permalink
fix(bridge-ui-v2): processing fee and amount input validation (#14220)
Browse files Browse the repository at this point in the history
  • Loading branch information
jscriptcoder authored Jul 20, 2023
1 parent 4b639d7 commit 61138a8
Show file tree
Hide file tree
Showing 41 changed files with 435 additions and 178 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<script lang="ts">
import type { FetchBalanceResult } from '@wagmi/core';
import { t } from 'svelte-i18n';
import { formatEther, parseUnits } from 'viem';
import { formatUnits, parseUnits } from 'viem';
import Icon from '$components/Icon/Icon.svelte';
import { InputBox } from '$components/InputBox';
import { warningToast } from '$components/NotificationToast';
import { getMaxToBridge } from '$libs/bridge/getMaxToBridge';
import { checkBalanceToBridge, getMaxAmountToBridge } from '$libs/bridge';
import { InsufficientAllowanceError, InsufficientBalanceError } from '$libs/error';
import { debounce } from '$libs/util/debounce';
import { uid } from '$libs/util/uid';
import { account } from '$stores/account';
Expand All @@ -20,44 +21,49 @@
let inputBox: InputBox;
let computingMaxAmount = false;
let errorAmount = false;
// Let's get the max amount to bridge and see if it's less
// than what the user has entered. For ETH, will actually get an error
// when trying to get that max amount, if the user has entered too much ETH
// There are two possible errors that can happen when the user
// enters an amount:
// 1. Insufficient balance
// 2. Insufficient allowance
// The first one is an error and the user cannot proceed. The second one
// is a warning but the user must approve allowance before bridging.
let insufficientBalance = false;
let insufficientAllowance = false;
async function checkEnteredAmount() {
insufficientBalance = false;
insufficientAllowance = false;
if (
!$selectedToken ||
!$network ||
!$destNetwork ||
!$account?.address ||
$enteredAmount === BigInt(0) // why to even bother, right?
) {
errorAmount = false;
$enteredAmount === BigInt(0) // no need to check if the amount is 0
)
return;
}
try {
const maxAmount = await getMaxToBridge({
await checkBalanceToBridge({
to: $account.address,
token: $selectedToken,
amount: $enteredAmount,
balance: tokenBalance.value,
processingFee: $processingFee,
srcChainId: $network.id,
destChainId: $destNetwork?.id,
userAddress: $account.address,
amount: $enteredAmount,
destChainId: $destNetwork.id,
processingFee: $processingFee,
});
if ($enteredAmount > maxAmount) {
errorAmount = true;
}
} catch (err) {
console.error(err);
// Viem will throw an error that contains the following message, indicating
// that the user won't have enough to pay the transaction
// TODO: better way to handle this. Error codes?
if (`${err}`.toLocaleLowerCase().match('transaction exceeds the balance')) {
errorAmount = true;
switch (true) {
case err instanceof InsufficientBalanceError:
insufficientBalance = true;
break;
case err instanceof InsufficientAllowanceError:
insufficientAllowance = true;
break;
}
}
}
Expand All @@ -68,45 +74,51 @@
// Will trigger on input events. We update the entered amount
// and check it's validity
function updateAmount(event: Event) {
errorAmount = false;
insufficientBalance = false;
insufficientAllowance = false;
if (!$selectedToken) return;
const target = event.target as HTMLInputElement;
try {
$enteredAmount = parseUnits(target.value, $selectedToken?.decimals);
$enteredAmount = parseUnits(target.value, $selectedToken.decimals);
debouncedCheckEnteredAmount();
} catch (err) {
$enteredAmount = BigInt(0);
}
}
function setETHAmount(amount: bigint) {
inputBox.setValue(formatEther(amount));
$enteredAmount = amount;
}
// Will trigger when the user clicks on the "Max" button
// "MAX" button handler
async function useMaxAmount() {
errorAmount = false;
insufficientBalance = false;
insufficientAllowance = false;
if (!$selectedToken || !$network || !$account?.address) return;
// We cannot calculate the max amount without these guys
if (!$selectedToken || !$network || !$destNetwork || !$account?.address) return;
computingMaxAmount = true;
try {
const maxAmount = await getMaxToBridge({
const maxAmount = await getMaxAmountToBridge({
to: $account.address,
token: $selectedToken,
balance: tokenBalance.value,
processingFee: $processingFee,
srcChainId: $network.id,
destChainId: $destNetwork?.id,
userAddress: $account.address,
destChainId: $destNetwork.id,
amount: BigInt(1), // whatever amount to estimate the cost
});
setETHAmount(maxAmount);
// Update UI
inputBox.setValue(formatUnits(maxAmount, $selectedToken.decimals));
// Update state
$enteredAmount = maxAmount;
// Check validity
checkEnteredAmount();
} catch (err) {
console.error(err);
warningToast($t('amount_input.button.failed_max'));
Expand Down Expand Up @@ -134,19 +146,20 @@
placeholder="0.01"
min="0"
loading={computingMaxAmount}
error={errorAmount}
error={insufficientBalance}
on:input={updateAmount}
bind:this={inputBox}
class="w-full input-box outline-none py-6 pr-16 px-[26px] title-subsection-bold placeholder:text-tertiary-content" />
<!-- TODO: talk to Jane about the MAX button and its styling -->
<button
class="absolute right-6 uppercase"
class="absolute right-6 uppercase hover:font-bold"
disabled={!$selectedToken || !$network || computingMaxAmount}
on:click={useMaxAmount}>
{$t('amount_input.button.max')}
</button>
</div>

{#if errorAmount}
{#if insufficientBalance}
<!-- TODO: should we make another component for flat error messages? -->
<div class="f-items-center space-x-1 mt-3">
<Icon type="exclamation-circle" fillClass="fill-negative-sentiment" />
Expand All @@ -155,4 +168,13 @@
</div>
</div>
{/if}

{#if insufficientAllowance}
<div class="f-items-center space-x-1 mt-3">
<Icon type="exclamation-circle" fillClass="fill-warning-sentiment" />
<div class="body-small-regular text-warning-sentiment">
{$t('amount_input.error.insufficient_allowance')}
</div>
</div>
{/if}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@
let errorComputingTokenBalance = false;
async function updateTokenBalance(token: Maybe<Token>, account?: Account, srcChainId?: number, destChainId?: number) {
if (!token || !account || !account.address) return;
if (!token || !srcChainId || !account?.address) return;
computingTokenBalance = true;
errorComputingTokenBalance = false;
try {
value = await getTokenBalance({
token,
srcChainId,
destChainId,
userAddress: account.address,
chainId: srcChainId,
});
} catch (err) {
console.error(err);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Amount } from './Amount.svelte';

This file was deleted.

10 changes: 5 additions & 5 deletions packages/bridge-ui-v2/src/components/Bridge/Bridge.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
import type { Account } from '$stores/account';
import { type Network, network } from '$stores/network';
import { AmountInput } from './AmountInput';
import { Amount } from './Amount';
import { ProcessingFee } from './ProcessingFee';
import { RecipientInput } from './RecipientInput';
import { Recipient } from './Recipient';
import { destNetwork, selectedToken } from './state';
import SwitchChainsButton from './SwitchChainsButton.svelte';
Expand Down Expand Up @@ -45,23 +45,23 @@
<TokenDropdown {tokens} bind:value={$selectedToken} />
</div>

<AmountInput />
<Amount />

<div class="f-justify-center">
<SwitchChainsButton />
</div>

<div class="space-y-2">
<ChainSelector label={$t('chain.to')} value={$destNetwork} readOnly />
<RecipientInput />
<!-- <RecipientInput /> -->
</div>
</div>

<ProcessingFee />

<div class="h-sep" />

<Button type="primary" class="px-[28px] py-[14px]">
<Button type="primary" class="px-[28px] py-[14px] rounded-full w-full">
<span class="body-bold">{$t('bridge.button.bridge')}</span>
</Button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@
try {
// Get the balance of the user on the destination chain
const destBalance = await getBalance({
token,
userAddress,
chainId: destChainId,
srcChainId: destChainId,
});
// Calculate the recommended amount of ETH needed for processMessage call
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { formatEther } from 'viem';
import { Alert } from '$components/Alert';
import { Button } from '$components/Button';
import { Icon } from '$components/Icon';
import { InputBox } from '$components/InputBox';
import { LoadingText } from '$components/LoadingText';
Expand All @@ -19,6 +20,7 @@
let dialogId = `dialog-${uid()}`;
let selectedFeeMethod = ProcessingFeeMethod.RECOMMENDED;
let prevOptionSelected = ProcessingFeeMethod.RECOMMENDED;
let recommendedAmount = BigInt(0);
let calculatingRecommendedAmount = false;
Expand All @@ -42,10 +44,20 @@
}
function openModal() {
// Keep track of the selected method before opening the modal
// so if we cancel we can go back to the previous method
prevOptionSelected = selectedFeeMethod;
modalOpen = true;
}
function closeOnOptionClick() {
function cancelModal() {
inputBox.clear();
selectedFeeMethod = prevOptionSelected;
closeModal();
}
function closeModalWithDelay() {
// By adding delay there is enough time to see the selected option
// before closing the modal. Better experience for the user.
setTimeout(closeModal, processingFeeComponent.closingDelayOptionClick);
Expand Down Expand Up @@ -122,15 +134,15 @@
{/if}
</span>

<dialog id={dialogId} class="modal modal-bottom md:absolute md:px-4 md:pb-4" class:modal-open={modalOpen}>
<dialog id={dialogId} class="modal" class:modal-open={modalOpen}>
<div class="modal-box relative px-6 py-[35px] md:rounded-[20px] bg-neutral-background">
<button class="absolute right-6 top-[35px]" on:click={closeModal}>
<Icon type="x-close" fillClass="fill-primary-icon" size={24} />
</button>

<h3 class="title-body-bold mb-7">{$t('processing_fee.title')}</h3>

<ul class="space-y-7 mb-[20px]">
<ul class="space-y-7">
<!-- RECOMMENDED -->
<li class="f-between-center">
<div class="f-col">
Expand All @@ -155,7 +167,7 @@
value={ProcessingFeeMethod.RECOMMENDED}
name="processingFeeMethod"
bind:group={selectedFeeMethod}
on:click={closeOnOptionClick} />
on:click={closeModalWithDelay} />
</li>

<!-- NONE -->
Expand All @@ -177,7 +189,7 @@
value={ProcessingFeeMethod.NONE}
name="processingFeeMethod"
bind:group={selectedFeeMethod}
on:click={closeOnOptionClick} />
on:click={closeModalWithDelay} />
</div>

{#if !hasEnoughEth}
Expand Down Expand Up @@ -207,7 +219,7 @@
</li>
</ul>

<div class="relative f-items-center">
<div class="relative f-items-center my-[20px]">
<InputBox
type="number"
min="0"
Expand All @@ -218,6 +230,18 @@
bind:this={inputBox} />
<span class="absolute right-6 uppercase body-bold text-secondary-content">ETH</span>
</div>

<div class="grid grid-cols-2 gap-[20px]">
<Button
on:click={cancelModal}
type="neutral"
class="px-[28px] py-[10px] rounded-full w-auto bg-transparent !border border-primary-brand hover:border-primary-interactive-hover">
<span class="body-bold">{$t('processing_fee.button.cancel')}</span>
</Button>
<Button type="primary" class="px-[28px] py-[10px] rounded-full w-auto" on:click={closeModal}>
<span class="body-bold">{$t('processing_fee.button.confirm')}</span>
</Button>
</div>
</div>
</dialog>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
let interval: ReturnType<typeof setInterval>;
async function compute(token: Maybe<Token>, srcChainId?: number, destChainId?: number) {
if (!token) return;
// Without token nor destination chain we cannot compute this fee
if (!token || !destChainId) return;
calculating = true;
error = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Recipient } from './Recipient.svelte';

This file was deleted.

Loading

0 comments on commit 61138a8

Please sign in to comment.