Skip to content

Commit

Permalink
feat(bridge-ui-v2): amount input validation (#14213)
Browse files Browse the repository at this point in the history
  • Loading branch information
jscriptcoder authored Jul 20, 2023
1 parent 57ef8f2 commit 4b639d7
Show file tree
Hide file tree
Showing 31 changed files with 584 additions and 93 deletions.
5 changes: 5 additions & 0 deletions packages/bridge-ui-v2/src/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ export const processingFeeComponent = {
closingDelayOptionClick: 300,
intervalComputeRecommendedFee: 20000,
};

export const bridge = {
noOwnerGasLimit: BigInt(140000),
noTokenDeployedGasLimit: BigInt(3000000),
};
Original file line number Diff line number Diff line change
@@ -1,26 +1,158 @@
<script lang="ts">
import type { FetchBalanceResult } from '@wagmi/core';
import { t } from 'svelte-i18n';
import { formatEther, 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 { debounce } from '$libs/util/debounce';
import { uid } from '$libs/util/uid';
import { account } from '$stores/account';
import { network } from '$stores/network';
import { destNetwork, enteredAmount, processingFee, selectedToken } from '../state';
import Balance from './Balance.svelte';
let inputId = `input-${uid()}`;
let tokenBalance: FetchBalanceResult;
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
async function checkEnteredAmount() {
if (
!$selectedToken ||
!$network ||
!$account?.address ||
$enteredAmount === BigInt(0) // why to even bother, right?
) {
errorAmount = false;
return;
}
try {
const maxAmount = await getMaxToBridge({
token: $selectedToken,
balance: tokenBalance.value,
processingFee: $processingFee,
srcChainId: $network.id,
destChainId: $destNetwork?.id,
userAddress: $account.address,
amount: $enteredAmount,
});
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;
}
}
}
// We want to debounce this function for input events
const debouncedCheckEnteredAmount = debounce(checkEnteredAmount, 300);
// Will trigger on input events. We update the entered amount
// and check it's validity
function updateAmount(event: Event) {
errorAmount = false;
if (!$selectedToken) return;
const target = event.target as HTMLInputElement;
try {
$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
async function useMaxAmount() {
errorAmount = false;
if (!$selectedToken || !$network || !$account?.address) return;
computingMaxAmount = true;
try {
const maxAmount = await getMaxToBridge({
token: $selectedToken,
balance: tokenBalance.value,
processingFee: $processingFee,
srcChainId: $network.id,
destChainId: $destNetwork?.id,
userAddress: $account.address,
});
setETHAmount(maxAmount);
} catch (err) {
console.error(err);
warningToast($t('amount_input.button.failed_max'));
} finally {
computingMaxAmount = false;
}
}
// Let's also trigger the check when either the processingFee or
// the selectedToken change and debounce it, just in case
// TODO: better way? maybe store.subscribe(), or different component
$: $processingFee && $selectedToken && debouncedCheckEnteredAmount();
</script>

<div class="AmountInput f-col space-y-2">
<div class="f-between-center text-secondary-content">
<label class="body-regular" for={inputId}>{$t('amount_input.label')}</label>
<Balance />
<Balance bind:value={tokenBalance} />
</div>

<div class="relative f-items-center">
<InputBox
id={inputId}
type="number"
placeholder="0.01"
min="0"
loading={computingMaxAmount}
error={errorAmount}
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" />
<button class="absolute right-6 uppercase">{$t('amount_input.button.max')}</button>
<button
class="absolute right-6 uppercase"
disabled={!$selectedToken || !$network || computingMaxAmount}
on:click={useMaxAmount}>
{$t('amount_input.button.max')}
</button>
</div>

{#if errorAmount}
<!-- 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" />
<div class="body-small-regular text-negative-sentiment">
{$t('amount_input.error.insufficient_balance')}
</div>
</div>
{/if}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
import { destNetwork, selectedToken } from '../state';
let tokenBalance: Maybe<FetchBalanceResult>;
export let value: Maybe<FetchBalanceResult>;
let computingTokenBalance = false;
let errorComputingTokenBalance = false;

Check warning on line 16 in packages/bridge-ui-v2/src/components/Bridge/AmountInput/Balance.svelte

View workflow job for this annotation

GitHub Actions / build

'errorComputingTokenBalance' is assigned a value but never used
Expand All @@ -21,14 +22,14 @@
errorComputingTokenBalance = false;
try {
tokenBalance = await getTokenBalance({
value = await getTokenBalance({
token,
destChainId,
userAddress: account.address,
chainId: srcChainId,
});
} catch (error) {
console.error(error);
} catch (err) {
console.error(err);
errorComputingTokenBalance = true;
} finally {
computingTokenBalance = false;
Expand All @@ -50,7 +51,7 @@
<LoadingText mask="0.0000" />
<LoadingText mask="XXX" />
{:else}
{renderTokenBalance(tokenBalance)}
{renderTokenBalance(value)}
{/if}
</span>
</div>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import type { Address } from 'abitype';
import type { Address } from 'viem';
import { recommendProcessingFee } from '$libs/fee';
import { getBalance, type Token } from '$libs/token';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
let errorCalculatingEnoughEth = false;
let modalOpen = false;
let customInput: InputBox;
let inputBox: InputBox;
function closeModal() {
// Let's check if we are closing with CUSTOM method selected and zero amount entered
Expand All @@ -51,11 +51,11 @@
setTimeout(closeModal, processingFeeComponent.closingDelayOptionClick);
}
function focusCustomInput() {
customInput?.focus();
function focusInputBox() {
inputBox.focus();
}
function onCustomInputChange(event: Event) {
function onInputBoxChange(event: Event) {
if (selectedFeeMethod !== ProcessingFeeMethod.CUSTOM) return;
const input = event.target as HTMLInputElement;
Expand All @@ -66,20 +66,20 @@
switch (method) {
case ProcessingFeeMethod.RECOMMENDED:
$processingFee = recommendedAmount;
customInput?.clear();
inputBox?.clear();
break;
case ProcessingFeeMethod.CUSTOM:
// Get a previous value entered if exists, otherwise default to 0
$processingFee = parseToWei(customInput?.value());
$processingFee = parseToWei(inputBox?.getValue());
// We need to wait for Svelte to set the attribute `disabled` on the input
// to false to be able to focus it
tick().then(focusCustomInput);
tick().then(focusInputBox);
break;
case ProcessingFeeMethod.NONE:
$processingFee = BigInt(0);
customInput?.clear();
inputBox?.clear();
break;
}
Expand Down Expand Up @@ -214,8 +214,8 @@
placeholder="0.01"
disabled={selectedFeeMethod !== ProcessingFeeMethod.CUSTOM}
class="w-full input-box outline-none p-6 pr-16 title-subsection-bold placeholder:text-tertiary-content"
on:input={onCustomInputChange}
bind:this={customInput} />
on:input={onInputBoxChange}
bind:this={inputBox} />
<span class="absolute right-6 uppercase body-bold text-secondary-content">ETH</span>
</div>
</div>
Expand Down
7 changes: 4 additions & 3 deletions packages/bridge-ui-v2/src/components/Bridge/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { Token } from '$libs/token';
// but once again, we don't need such level of security that we have to
// prevent other components outside the Bridge from accessing this store.

export const selectedToken = writable<Maybe<Token>>();
export const destNetwork = writable<Maybe<Chain>>();
export const processingFee = writable<bigint>();
export const selectedToken = writable<Maybe<Token>>(null);
export const enteredAmount = writable<bigint>(BigInt(0));
export const destNetwork = writable<Maybe<Chain>>(null);
export const processingFee = writable<bigint>(BigInt(0));
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@
try {
await switchNetwork({ chainId: chain.id });
closeModal();
} catch (error) {
console.error(error);
} catch (err) {
console.error(err);
if (error instanceof UserRejectedRequestError) {
if (err instanceof UserRejectedRequestError) {
warningToast($t('messages.network.rejected'));
}
} finally {
Expand Down
33 changes: 13 additions & 20 deletions packages/bridge-ui-v2/src/components/Faucet/Faucet.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { type Chain, getWalletClient, switchNetwork } from '@wagmi/core';
import { type Chain, switchNetwork } from '@wagmi/core';
import { t } from 'svelte-i18n';
import { UserRejectedRequestError } from 'viem';
Expand Down Expand Up @@ -30,11 +30,11 @@
switchingNetwork = true;
try {
await switchNetwork({ chainId: +PUBLIC_L1_CHAIN_ID });
} catch (error) {
console.error(error);
await switchNetwork({ chainId: Number(PUBLIC_L1_CHAIN_ID) });
} catch (err) {
console.error(err);
if (error instanceof UserRejectedRequestError) {
if (err instanceof UserRejectedRequestError) {
warningToast($t('messages.network.rejected'));
}
} finally {
Expand All @@ -49,15 +49,11 @@
// A token and a source chain must be selected in order to be able to mint
if (!selectedToken || !$network) return;
// ... and of course, our wallet must be connected
const walletClient = await getWalletClient({ chainId: $network.id });
if (!walletClient) return;
// Let's begin the minting process
minting = true;
try {
const txHash = await mint(selectedToken, walletClient);
const txHash = await mint(selectedToken);
successToast(
$t('faucet.minting_tx', {
Expand All @@ -70,8 +66,8 @@
);
// TODO: pending transaction logic
} catch (error) {
console.error(error);
} catch (err) {
console.error(err);
// const { cause } = error as Error;
} finally {
Expand All @@ -98,17 +94,14 @@
reasonNotMintable = '';
try {
await checkMintable(token, network);
await checkMintable(token, network.id);
mintButtonEnabled = true;
} catch (error) {
console.error(error);
} catch (err) {
console.error(err);
const { cause } = error as Error;
const { cause } = err as Error;
switch (cause) {
case MintableError.NOT_CONNECTED:
reasonNotMintable = $t('faucet.warning.no_connected');
break;
case MintableError.INSUFFICIENT_BALANCE:
reasonNotMintable = $t('faucet.warning.insufficient_balance');
break;
Expand Down Expand Up @@ -140,7 +133,7 @@
<Card class="md:w-[524px]" title={$t('faucet.title')} text={$t('faucet.subtitle')}>
<div class="space-y-[35px]">
<div class="space-y-2">
<ChainSelector label={$t('chain_selector.currently_on')} value={$network} />
<ChainSelector label={$t('chain_selector.currently_on')} value={$network} switchWallet />
<TokenDropdown tokens={testERC20Tokens} bind:value={selectedToken} />
</div>

Expand Down
Loading

0 comments on commit 4b639d7

Please sign in to comment.