Skip to content

Commit

Permalink
problem: may create tx with gas + value more than available
Browse files Browse the repository at this point in the history
  • Loading branch information
BOOMER74 authored Sep 14, 2023
1 parent f2d74b4 commit 1021c80
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 81 deletions.
52 changes: 35 additions & 17 deletions packages/core/src/workflow/create-tx/CreateErc20WrappedTx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BigAmount } from '@emeraldpay/bigamount';
import { BlockchainCode, Token, TokenAmount, TokenData, amountFactory, wrapTokenAbi } from '../../blockchains';
import { Contract } from '../../Contract';
import { DEFAULT_GAS_LIMIT_ERC20, EthereumTransaction, EthereumTransactionType } from '../../transaction/ethereum';
import { TxTarget } from './types';
import { TxTarget, ValidationResult } from './types';

export interface Erc20WrappedTxDetails {
address?: string;
Expand All @@ -20,17 +20,17 @@ export interface Erc20WrappedTxDetails {
}

export class CreateErc20WrappedTx {
public address?: string;
public amount: BigAmount;
public blockchain: BlockchainCode;
public gas: number;
public gasPrice?: BigAmount;
public maxGasPrice?: BigAmount;
public priorityGasPrice?: BigAmount;
public target: TxTarget;
public totalBalance: BigAmount;
public totalTokenBalance: TokenAmount;
public type: EthereumTransactionType;
address?: string;
amount: BigAmount;
blockchain: BlockchainCode;
gas: number;
gasPrice?: BigAmount;
maxGasPrice?: BigAmount;
priorityGasPrice?: BigAmount;
target: TxTarget;
totalBalance: BigAmount;
totalTokenBalance: TokenAmount;
type: EthereumTransactionType;

private readonly token: Token;
private readonly zeroAmount: BigAmount;
Expand Down Expand Up @@ -60,11 +60,11 @@ export class CreateErc20WrappedTx {
this.zeroAmount = zeroAmount;
}

public static fromPlain(details: Erc20WrappedTxDetails): CreateErc20WrappedTx {
static fromPlain(details: Erc20WrappedTxDetails): CreateErc20WrappedTx {
return new CreateErc20WrappedTx(details);
}

public build(): EthereumTransaction {
build(): EthereumTransaction {
const { amount, blockchain, gas, gasPrice, maxGasPrice, priorityGasPrice, totalBalance, type, address = '' } = this;

const isDeposit = amount.units.equals(totalBalance.units);
Expand All @@ -87,7 +87,7 @@ export class CreateErc20WrappedTx {
};
}

public dump(): Erc20WrappedTxDetails {
dump(): Erc20WrappedTxDetails {
return {
address: this.address,
amount: this.amount,
Expand All @@ -104,13 +104,13 @@ export class CreateErc20WrappedTx {
};
}

public getFees(): BigAmount {
getFees(): BigAmount {
const gasPrice = this.maxGasPrice ?? this.gasPrice ?? this.zeroAmount;

return gasPrice.multiply(this.gas);
}

public rebalance(): void {
rebalance(): void {
if (this.target === TxTarget.SEND_ALL) {
if (this.amount.units.equals(this.totalBalance.units)) {
const amount = this.totalBalance.minus(this.getFees());
Expand All @@ -123,4 +123,22 @@ export class CreateErc20WrappedTx {
}
}
}

validate(): ValidationResult {
if (this.amount.isZero()) {
return ValidationResult.NO_AMOUNT;
}

if (this.amount.units.equals(this.totalBalance.units)) {
const total = this.amount.plus(this.getFees());

if (total.isGreaterThan(this.totalBalance)) {
return ValidationResult.INSUFFICIENT_FUNDS;
}
} else if (this.amount.isGreaterThan(this.totalTokenBalance)) {
return ValidationResult.INSUFFICIENT_TOKEN_FUNDS;
}

return ValidationResult.OK;
}
}
12 changes: 7 additions & 5 deletions packages/react-app/src/common/EthTxSettings/EthTxSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Unit } from '@emeraldpay/bigamount';
import { Wei, WeiAny } from '@emeraldpay/bigamount-crypto';
import { CreateAmount, Unit } from '@emeraldpay/bigamount';
import { WeiAny } from '@emeraldpay/bigamount-crypto';
import { FormAccordion, FormLabel, FormRow } from '@emeraldwallet/ui';
import { Box, FormControlLabel, FormHelperText, Slider, Switch, createStyles, makeStyles } from '@material-ui/core';
import * as React from 'react';
Expand Down Expand Up @@ -42,6 +42,7 @@ const useStyles = makeStyles(
);

interface OwnProps {
factory: CreateAmount<WeiAny>;
initializing: boolean;
supportEip1559: boolean;
useEip1559: boolean;
Expand All @@ -59,6 +60,7 @@ interface OwnProps {
}

const EthTxSettings: React.FC<OwnProps> = ({
factory,
initializing,
supportEip1559,
useEip1559,
Expand Down Expand Up @@ -92,7 +94,7 @@ const EthTxSettings: React.FC<OwnProps> = ({
const [currentUseStdMaxGasPrice, setCurrentUseStdMaxGasPrice] = React.useState(true);
const [currentUseStdPriorityGasPrice, setCurrentUseStdPriorityGasPrice] = React.useState(true);

const toWeiInCurrentUnits = (decimal: number): Wei => new Wei(decimal, gasPriceUnit);
const toWei = (decimal: number): WeiAny => WeiAny.createFor(decimal, stdMaxGasPrice.units, factory, gasPriceUnit);

const handleUse1559Change = (event: React.ChangeEvent<HTMLInputElement>, checked: boolean): void => {
onUse1559Change(checked);
Expand All @@ -109,7 +111,7 @@ const EthTxSettings: React.FC<OwnProps> = ({
const handleMaxGasPriceChange = (event: React.ChangeEvent<unknown>, value: number | number[]): void => {
const [gasPriceDecimal] = Array.isArray(value) ? value : [value];

onMaxGasPriceChange(toWeiInCurrentUnits(gasPriceDecimal));
onMaxGasPriceChange(toWei(gasPriceDecimal));
};

const handleUseStdPriorityGasPriceChange = (event: React.ChangeEvent<HTMLInputElement>, checked: boolean): void => {
Expand All @@ -123,7 +125,7 @@ const EthTxSettings: React.FC<OwnProps> = ({
const handlePriorityGasPriceChange = (event: React.ChangeEvent<unknown>, value: number | number[]): void => {
const [gasPriceDecimal] = Array.isArray(value) ? value : [value];

onPriorityGasPriceChange(toWeiInCurrentUnits(gasPriceDecimal));
onPriorityGasPriceChange(toWei(gasPriceDecimal));
};

const maxGasPriceByUnit = maxGasPrice.getNumberByUnit(gasPriceUnit).toFixed(2);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigAmount } from '@emeraldpay/bigamount';
import { BigAmount, CreateAmount } from '@emeraldpay/bigamount';
import { WeiAny } from '@emeraldpay/bigamount-crypto';
import { EntryId, EthereumEntry, WalletEntry, isEthereumEntry } from '@emeraldpay/emerald-vault-core';
import {
Expand Down Expand Up @@ -144,7 +144,8 @@ const SetupApproveTransaction: React.FC<OwnProps & StateProps & DispatchProps> =

const [useEip1559, setUseEip1559] = React.useState(supportEip1559);

const zeroAmount = amountFactory(currentBlockchain)(0) as WeiAny;
const factory = amountFactory(currentBlockchain) as CreateAmount<WeiAny>;
const zeroAmount = factory(0);

const [maxGasPrice, setMaxGasPrice] = React.useState(zeroAmount);
const [priorityGasPrice, setPriorityGasPrice] = React.useState(zeroAmount);
Expand Down Expand Up @@ -195,8 +196,6 @@ const SetupApproveTransaction: React.FC<OwnProps & StateProps & DispatchProps> =
const fetchFees = (): Promise<void> =>
getFees(currentBlockchain).then(({ avgLast, avgMiddle, avgTail5 }) => {
if (mounted.current) {
const factory = amountFactory(currentBlockchain);

const newStdMaxGasPrice = factory(avgTail5.max) as WeiAny;
const newStdPriorityGasPrice = factory(avgTail5.priority) as WeiAny;

Expand Down Expand Up @@ -462,6 +461,7 @@ const SetupApproveTransaction: React.FC<OwnProps & StateProps & DispatchProps> =
</FormRow>
)}
<EthTxSettings
factory={factory}
initializing={initializing}
supportEip1559={supportEip1559}
useEip1559={useEip1559}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigAmount } from '@emeraldpay/bigamount';
import { BigAmount, CreateAmount } from '@emeraldpay/bigamount';
import { WeiAny } from '@emeraldpay/bigamount-crypto';
import { WalletEntry, isEthereumEntry } from '@emeraldpay/emerald-vault-core';
import {
Expand All @@ -15,6 +15,7 @@ import {
formatAmount,
workflow,
} from '@emeraldwallet/core';
import { ValidationResult } from '@emeraldwallet/core/lib/workflow';
import { FEE_KEYS, GasPrices, IState, SignData, accounts, screen, tokens, transaction } from '@emeraldwallet/store';
import { AccountSelect, Back, Button, ButtonGroup, FormLabel, FormRow, Page, PasswordInput } from '@emeraldwallet/ui';
import { CircularProgress, Typography, createStyles, makeStyles } from '@material-ui/core';
Expand Down Expand Up @@ -70,7 +71,6 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>
addresses,
blockchain,
coinTicker,
contractAddress,
entry: { address },
isHardware,
supportEip1559,
Expand All @@ -90,6 +90,7 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>
const mounted = React.useRef(true);

const [initializing, setInitializing] = React.useState(true);
const [preparing, setPreparing] = React.useState(false);
const [verifying, setVerifying] = React.useState(false);

const [convertable, setConvertable] = React.useState<string>(coinTicker);
Expand All @@ -109,7 +110,8 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>

const [useEip1559, setUseEip1559] = React.useState(supportEip1559);

const zeroAmount = amountFactory(blockchain)(0) as WeiAny;
const factory = amountFactory(blockchain) as CreateAmount<WeiAny>;
const zeroAmount = factory(0);

const [maxGasPrice, setMaxGasPrice] = React.useState(zeroAmount);
const [priorityGasPrice, setPriorityGasPrice] = React.useState(zeroAmount);
Expand All @@ -127,16 +129,14 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>
const [password, setPassword] = React.useState<string>();
const [passwordError, setPasswordError] = React.useState<string>();

const oldAmount = React.useRef(convertTx.amount);

const onChangeConvertable = (event: React.MouseEvent<HTMLElement>, value: string): void => {
const converting = value ?? convertable;

const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx);

const { number: amount } = tx.amount;

tx.amount = converting === coinTicker ? amountFactory(blockchain)(amount) : token.getAmount(amount);
tx.amount = converting === coinTicker ? factory(amount) : token.getAmount(amount);
tx.target = workflow.TxTarget.MANUAL;
tx.rebalance();

Expand Down Expand Up @@ -164,12 +164,34 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>
setConvertTx(tx.dump());
};

const onClickMaxAmount = (): void => {
const onClickMaxAmount = (callback: (value: BigAmount) => void): void => {
const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx);

tx.target = workflow.TxTarget.SEND_ALL;
tx.rebalance();

callback(tx.amount);

setConvertTx(tx.dump());
};

const onUseEip1559Change = (checked: boolean): void => {
setUseEip1559(checked);

const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx);

if (checked) {
tx.gasPrice = undefined;
tx.maxGasPrice = maxGasPrice;
tx.priorityGasPrice = maxGasPrice;
} else {
tx.gasPrice = maxGasPrice;
tx.maxGasPrice = undefined;
tx.priorityGasPrice = undefined;
}

tx.type = checked ? EthereumTransactionType.EIP1559 : EthereumTransactionType.LEGACY;

setConvertTx(tx.dump());
};

Expand Down Expand Up @@ -202,6 +224,20 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>
setConvertTx(tx.dump());
};

const onCreateTransaction = async (): Promise<void> => {
setPreparing(true);

const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx);

tx.gas = await estimateGas(tx.build());
tx.rebalance();

setConvertTx(tx.dump());
setStage(Stages.SIGN);

setPreparing(false);
};

const onSignTransaction = async (): Promise<void> => {
setPasswordError(undefined);

Expand Down Expand Up @@ -246,32 +282,10 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>
}
};

const onUseEip1559Change = (checked: boolean): void => {
setUseEip1559(checked);

const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx);

if (checked) {
tx.gasPrice = undefined;
tx.maxGasPrice = maxGasPrice;
tx.priorityGasPrice = maxGasPrice;
} else {
tx.gasPrice = maxGasPrice;
tx.maxGasPrice = undefined;
tx.priorityGasPrice = undefined;
}

tx.type = checked ? EthereumTransactionType.EIP1559 : EthereumTransactionType.LEGACY;

setConvertTx(tx.dump());
};

React.useEffect(
() => {
getFees(blockchain).then(({ avgLast, avgMiddle, avgTail5 }) => {
if (mounted.current) {
const factory = amountFactory(blockchain);

const newStdMaxGasPrice = factory(avgTail5.max) as WeiAny;
const newStdPriorityGasPrice = factory(avgTail5.priority) as WeiAny;

Expand Down Expand Up @@ -309,28 +323,6 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>
[],
);

React.useEffect(() => {
if (oldAmount.current == null || convertTx.amount?.equals(oldAmount.current) === false) {
oldAmount.current = convertTx.amount;

const total =
convertTx.totalBalance != null && convertTx.amount?.units.equals(convertTx.totalBalance.units) === true
? convertTx.totalBalance
: convertTx.totalTokenBalance;

if (total?.number != null && convertTx.amount?.number.isLessThanOrEqualTo(total.number) === true) {
(async (): Promise<void> => {
const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx);

tx.gas = await estimateGas(tx.build());
tx.rebalance();

setConvertTx(tx.dump());
})();
}
}
}, [blockchain, contractAddress, convertable, convertTx, estimateGas]);

React.useEffect(() => {
return () => {
mounted.current = false;
Expand Down Expand Up @@ -375,6 +367,7 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>
/>
</FormRow>
<EthTxSettings
factory={factory}
initializing={initializing}
supportEip1559={supportEip1559}
useEip1559={useEip1559}
Expand All @@ -399,9 +392,9 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>
<Button label="Cancel" onClick={goBack} />
<Button
primary
disabled={initializing || currentTx.amount.isZero()}
disabled={initializing || preparing || currentTx.validate() !== ValidationResult.OK}
label="Create Transaction"
onClick={(): void => setStage(Stages.SIGN)}
onClick={onCreateTransaction}
/>
</ButtonGroup>
</FormRow>
Expand Down
Loading

0 comments on commit 1021c80

Please sign in to comment.