Skip to content

Commit

Permalink
wallet-ext: improve validation for token transfer and staking (Mysten…
Browse files Browse the repository at this point in the history
…Labs#6706)

* take into account that only one single coin is used for gas
  • Loading branch information
pchrysochoidis authored Dec 15, 2022
1 parent 6caf520 commit 3271796
Show file tree
Hide file tree
Showing 11 changed files with 153 additions and 132 deletions.
15 changes: 13 additions & 2 deletions apps/wallet/src/shared/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export function createTokenValidation(
decimals: number,
// TODO: We can move this to a constant when MIST is fully rolled out.
gasDecimals: number,
gasBudget: number
gasBudget: number,
maxSuiSingleCoinBalance: bigint
) {
return Yup.mixed()
.transform((_, original) => {
Expand Down Expand Up @@ -52,11 +53,21 @@ export function createTokenValidation(
)
.test(
'max-decimals',
`The value exeeds the maximum decimals (${decimals}).`,
`The value exceeds the maximum decimals (${decimals}).`,
(amount?: BigNumber) => {
return amount ? amount.shiftedBy(decimals).isInteger() : false;
}
)
.test(
'gas-balance-check-enough-single-coin',
`Insufficient ${GAS_SYMBOL}, there is no individual coin with enough balance to cover for the gas fee (${formatBalance(
gasBudget,
gasDecimals
)} ${GAS_SYMBOL})`,
() => {
return maxSuiSingleCoinBalance >= gasBudget;
}
)
.test(
'gas-balance-check',
`Insufficient ${GAS_SYMBOL} balance to cover gas fee (${formatBalance(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import BigNumber from 'bignumber.js';
import cl from 'classnames';
import { ErrorMessage, Field, Form, useFormikContext } from 'formik';
import { Field, Form, useFormikContext } from 'formik';
import { useEffect, useRef, memo } from 'react';

import { parseAmount } from './utils';
import { Content, Menu } from '_app/shared/bottom-menu-layout';
import Button from '_app/shared/button';
import ActiveCoinsCard from '_components/active-coins-card';
import Alert from '_components/alert';
import Icon, { SuiIcons } from '_components/icon';
import NumberInput from '_components/number-input';
import { useCoinDecimals } from '_hooks';
Expand All @@ -17,19 +18,6 @@ import type { FormValues } from '../';

import st from './TransferCoinForm.module.scss';

function parseAmount(amount: string, coinDecimals: number) {
try {
return BigInt(
new BigNumber(amount)
.shiftedBy(coinDecimals)
.integerValue()
.toString()
);
} catch (e) {
return BigInt(0);
}
}

export type TransferCoinFormProps = {
coinSymbol: string;
coinType: string;
Expand All @@ -47,6 +35,8 @@ function StepOne({
isValid,
validateForm,
values: { amount },
errors,
touched,
} = useFormikContext<FormValues>();
const onClearRef = useRef(onClearSubmitError);
onClearRef.current = onClearSubmitError;
Expand Down Expand Up @@ -88,12 +78,11 @@ function StepOne({
className={st.input}
decimals
/>

<ErrorMessage
className={st.error}
name="amount"
component="div"
/>
{errors.amount && touched.amount ? (
<div className="mt-3">
<Alert>{errors.amount}</Alert>
</div>
) : null}
</div>
<div className={st.activeCoinCard}>
<ActiveCoinsCard activeCoinType={coinType} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import cl from 'classnames';
import { Field, Form, useFormikContext } from 'formik';
import { useEffect, useRef, memo, useMemo } from 'react';

import { parseAmount } from './utils';
import { Content, Menu } from '_app/shared/bottom-menu-layout';
import Button from '_app/shared/button';
import AddressInput from '_components/address-input';
import Alert from '_components/alert';
import Icon, { SuiIcons } from '_components/icon';
import LoadingIndicator from '_components/loading/LoadingIndicator';
import { useCoinDecimals, useFormatCoin } from '_hooks';
Expand All @@ -22,46 +24,42 @@ export type TransferCoinFormProps = {
submitError: string | null;
coinSymbol: string;
coinType: string;
gasBudget: number;
gasBudgetEstimation: number | null;
gasCostEstimation: number | null;
gasEstimationLoading?: boolean;
onClearSubmitError: () => void;
};

function StepTwo({
submitError,
coinSymbol,
coinType,
gasBudget,
gasBudgetEstimation,
gasCostEstimation,
gasEstimationLoading,
onClearSubmitError,
}: TransferCoinFormProps) {
const {
isSubmitting,
isValid,
values: { amount, to },
} = useFormikContext<FormValues>();

const onClearRef = useRef(onClearSubmitError);
onClearRef.current = onClearSubmitError;

useEffect(() => {
onClearRef.current();
}, [amount, to]);

const [decimals] = useCoinDecimals(coinType);
const amountWithoutDecimals = useMemo(
() =>
new BigNumber(amount).shiftedBy(decimals).integerValue().toString(),
() => parseAmount(amount, decimals),
[amount, decimals]
);

const totalAmount = new BigNumber(gasBudget)
.plus(GAS_SYMBOL === coinSymbol ? amountWithoutDecimals : 0)
const totalSuiAmount = new BigNumber(gasCostEstimation || 0)
.plus(GAS_SYMBOL === coinSymbol ? amountWithoutDecimals.toString() : 0)
.toString();

const validAddressBtn = !isValid || to === '' || isSubmitting;

const [formattedBalance] = useFormatCoin(amountWithoutDecimals, coinType);
const [formattedTotal] = useFormatCoin(totalAmount, GAS_TYPE_ARG);
const [formattedGas] = useFormatCoin(gasBudget, GAS_TYPE_ARG);
const [formattedTotalSui] = useFormatCoin(totalSuiAmount, GAS_TYPE_ARG);
const [formattedGas] = useFormatCoin(gasCostEstimation, GAS_TYPE_ARG);

return (
<Form className={st.container} autoComplete="off" noValidate={true}>
Expand All @@ -79,7 +77,9 @@ function StepTwo({
</div>

{submitError ? (
<div className={st.error}>{submitError}</div>
<div className="mt-3">
<Alert>{submitError}</Alert>
</div>
) : null}

<div className={st.responseCard}>
Expand All @@ -88,31 +88,48 @@ function StepTwo({
</div>

<div className={st.details}>
<div className={st.txFees}>
<div className={st.txInfoLabel}>Gas Fee</div>
<div className={st.walletInfoValue}>
{formattedGas} {GAS_SYMBOL}
</div>
</div>

<div className={st.txFees}>
<div className={st.txInfoLabel}>Total Amount</div>
<div className={st.walletInfoValue}>
{formattedTotal} {GAS_SYMBOL}
{[
['Estimated Gas Fee', formattedGas, GAS_SYMBOL],
['Total Amount', formattedTotalSui, GAS_SYMBOL],
].map(([label, frmt, symbol]) => (
<div className={st.txFees} key={label}>
<div className={st.txInfoLabel}>{label}</div>
<div className={st.walletInfoValue}>
{gasEstimationLoading &&
!(
gasBudgetEstimation || gasCostEstimation
) ? (
<LoadingIndicator />
) : frmt ? (
`${frmt} ${symbol}`
) : (
'-'
)}
</div>
</div>
</div>
))}
</div>
</div>
</Content>
<Menu stuckClass={st.shadow}>
<div className={cl(st.group, st.cta)}>
<Button
type="submit"
disabled={validAddressBtn}
disabled={
!isValid ||
to === '' ||
isSubmitting ||
!gasBudgetEstimation
}
mode="primary"
className={st.btn}
>
{isSubmitting ? <LoadingIndicator /> : 'Send Coins Now'}
{isSubmitting ||
(gasEstimationLoading && !gasBudgetEstimation) ? (
<LoadingIndicator />
) : (
'Send Coins Now'
)}
<Icon
icon={SuiIcons.ArrowLeft}
className={cl(st.arrowLeft)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,6 @@
color: colors.$gray-80;
}

.error {
@include utils.error-message-box;
}

.muted {
font-size: 10px;
font-weight: 400;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import BigNumber from 'bignumber.js';

export function parseAmount(amount: string, coinDecimals: number) {
try {
return BigInt(
new BigNumber(amount)
.shiftedBy(coinDecimals)
.integerValue()
.toString()
);
} catch (e) {
return BigInt(0);
}
}
Loading

0 comments on commit 3271796

Please sign in to comment.