Skip to content
This repository was archived by the owner on Sep 25, 2025. It is now read-only.

Commit 1021c80

Browse files
authored
problem: may create tx with gas + value more than available
1 parent f2d74b4 commit 1021c80

5 files changed

Lines changed: 95 additions & 81 deletions

File tree

packages/core/src/workflow/create-tx/CreateErc20WrappedTx.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { BigAmount } from '@emeraldpay/bigamount';
22
import { BlockchainCode, Token, TokenAmount, TokenData, amountFactory, wrapTokenAbi } from '../../blockchains';
33
import { Contract } from '../../Contract';
44
import { DEFAULT_GAS_LIMIT_ERC20, EthereumTransaction, EthereumTransactionType } from '../../transaction/ethereum';
5-
import { TxTarget } from './types';
5+
import { TxTarget, ValidationResult } from './types';
66

77
export interface Erc20WrappedTxDetails {
88
address?: string;
@@ -20,17 +20,17 @@ export interface Erc20WrappedTxDetails {
2020
}
2121

2222
export class CreateErc20WrappedTx {
23-
public address?: string;
24-
public amount: BigAmount;
25-
public blockchain: BlockchainCode;
26-
public gas: number;
27-
public gasPrice?: BigAmount;
28-
public maxGasPrice?: BigAmount;
29-
public priorityGasPrice?: BigAmount;
30-
public target: TxTarget;
31-
public totalBalance: BigAmount;
32-
public totalTokenBalance: TokenAmount;
33-
public type: EthereumTransactionType;
23+
address?: string;
24+
amount: BigAmount;
25+
blockchain: BlockchainCode;
26+
gas: number;
27+
gasPrice?: BigAmount;
28+
maxGasPrice?: BigAmount;
29+
priorityGasPrice?: BigAmount;
30+
target: TxTarget;
31+
totalBalance: BigAmount;
32+
totalTokenBalance: TokenAmount;
33+
type: EthereumTransactionType;
3434

3535
private readonly token: Token;
3636
private readonly zeroAmount: BigAmount;
@@ -60,11 +60,11 @@ export class CreateErc20WrappedTx {
6060
this.zeroAmount = zeroAmount;
6161
}
6262

63-
public static fromPlain(details: Erc20WrappedTxDetails): CreateErc20WrappedTx {
63+
static fromPlain(details: Erc20WrappedTxDetails): CreateErc20WrappedTx {
6464
return new CreateErc20WrappedTx(details);
6565
}
6666

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

7070
const isDeposit = amount.units.equals(totalBalance.units);
@@ -87,7 +87,7 @@ export class CreateErc20WrappedTx {
8787
};
8888
}
8989

90-
public dump(): Erc20WrappedTxDetails {
90+
dump(): Erc20WrappedTxDetails {
9191
return {
9292
address: this.address,
9393
amount: this.amount,
@@ -104,13 +104,13 @@ export class CreateErc20WrappedTx {
104104
};
105105
}
106106

107-
public getFees(): BigAmount {
107+
getFees(): BigAmount {
108108
const gasPrice = this.maxGasPrice ?? this.gasPrice ?? this.zeroAmount;
109109

110110
return gasPrice.multiply(this.gas);
111111
}
112112

113-
public rebalance(): void {
113+
rebalance(): void {
114114
if (this.target === TxTarget.SEND_ALL) {
115115
if (this.amount.units.equals(this.totalBalance.units)) {
116116
const amount = this.totalBalance.minus(this.getFees());
@@ -123,4 +123,22 @@ export class CreateErc20WrappedTx {
123123
}
124124
}
125125
}
126+
127+
validate(): ValidationResult {
128+
if (this.amount.isZero()) {
129+
return ValidationResult.NO_AMOUNT;
130+
}
131+
132+
if (this.amount.units.equals(this.totalBalance.units)) {
133+
const total = this.amount.plus(this.getFees());
134+
135+
if (total.isGreaterThan(this.totalBalance)) {
136+
return ValidationResult.INSUFFICIENT_FUNDS;
137+
}
138+
} else if (this.amount.isGreaterThan(this.totalTokenBalance)) {
139+
return ValidationResult.INSUFFICIENT_TOKEN_FUNDS;
140+
}
141+
142+
return ValidationResult.OK;
143+
}
126144
}

packages/react-app/src/common/EthTxSettings/EthTxSettings.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Unit } from '@emeraldpay/bigamount';
2-
import { Wei, WeiAny } from '@emeraldpay/bigamount-crypto';
1+
import { CreateAmount, Unit } from '@emeraldpay/bigamount';
2+
import { WeiAny } from '@emeraldpay/bigamount-crypto';
33
import { FormAccordion, FormLabel, FormRow } from '@emeraldwallet/ui';
44
import { Box, FormControlLabel, FormHelperText, Slider, Switch, createStyles, makeStyles } from '@material-ui/core';
55
import * as React from 'react';
@@ -42,6 +42,7 @@ const useStyles = makeStyles(
4242
);
4343

4444
interface OwnProps {
45+
factory: CreateAmount<WeiAny>;
4546
initializing: boolean;
4647
supportEip1559: boolean;
4748
useEip1559: boolean;
@@ -59,6 +60,7 @@ interface OwnProps {
5960
}
6061

6162
const EthTxSettings: React.FC<OwnProps> = ({
63+
factory,
6264
initializing,
6365
supportEip1559,
6466
useEip1559,
@@ -92,7 +94,7 @@ const EthTxSettings: React.FC<OwnProps> = ({
9294
const [currentUseStdMaxGasPrice, setCurrentUseStdMaxGasPrice] = React.useState(true);
9395
const [currentUseStdPriorityGasPrice, setCurrentUseStdPriorityGasPrice] = React.useState(true);
9496

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

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

112-
onMaxGasPriceChange(toWeiInCurrentUnits(gasPriceDecimal));
114+
onMaxGasPriceChange(toWei(gasPriceDecimal));
113115
};
114116

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

126-
onPriorityGasPriceChange(toWeiInCurrentUnits(gasPriceDecimal));
128+
onPriorityGasPriceChange(toWei(gasPriceDecimal));
127129
};
128130

129131
const maxGasPriceByUnit = maxGasPrice.getNumberByUnit(gasPriceUnit).toFixed(2);

packages/react-app/src/transaction/CreateApproveTransaction/SetupApproveTransaction.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BigAmount } from '@emeraldpay/bigamount';
1+
import { BigAmount, CreateAmount } from '@emeraldpay/bigamount';
22
import { WeiAny } from '@emeraldpay/bigamount-crypto';
33
import { EntryId, EthereumEntry, WalletEntry, isEthereumEntry } from '@emeraldpay/emerald-vault-core';
44
import {
@@ -144,7 +144,8 @@ const SetupApproveTransaction: React.FC<OwnProps & StateProps & DispatchProps> =
144144

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

147-
const zeroAmount = amountFactory(currentBlockchain)(0) as WeiAny;
147+
const factory = amountFactory(currentBlockchain) as CreateAmount<WeiAny>;
148+
const zeroAmount = factory(0);
148149

149150
const [maxGasPrice, setMaxGasPrice] = React.useState(zeroAmount);
150151
const [priorityGasPrice, setPriorityGasPrice] = React.useState(zeroAmount);
@@ -195,8 +196,6 @@ const SetupApproveTransaction: React.FC<OwnProps & StateProps & DispatchProps> =
195196
const fetchFees = (): Promise<void> =>
196197
getFees(currentBlockchain).then(({ avgLast, avgMiddle, avgTail5 }) => {
197198
if (mounted.current) {
198-
const factory = amountFactory(currentBlockchain);
199-
200199
const newStdMaxGasPrice = factory(avgTail5.max) as WeiAny;
201200
const newStdPriorityGasPrice = factory(avgTail5.priority) as WeiAny;
202201

@@ -462,6 +461,7 @@ const SetupApproveTransaction: React.FC<OwnProps & StateProps & DispatchProps> =
462461
</FormRow>
463462
)}
464463
<EthTxSettings
464+
factory={factory}
465465
initializing={initializing}
466466
supportEip1559={supportEip1559}
467467
useEip1559={useEip1559}

packages/react-app/src/transaction/CreateConvertTransaction/CreateConvertTransaction.tsx

Lines changed: 46 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BigAmount } from '@emeraldpay/bigamount';
1+
import { BigAmount, CreateAmount } from '@emeraldpay/bigamount';
22
import { WeiAny } from '@emeraldpay/bigamount-crypto';
33
import { WalletEntry, isEthereumEntry } from '@emeraldpay/emerald-vault-core';
44
import {
@@ -15,6 +15,7 @@ import {
1515
formatAmount,
1616
workflow,
1717
} from '@emeraldwallet/core';
18+
import { ValidationResult } from '@emeraldwallet/core/lib/workflow';
1819
import { FEE_KEYS, GasPrices, IState, SignData, accounts, screen, tokens, transaction } from '@emeraldwallet/store';
1920
import { AccountSelect, Back, Button, ButtonGroup, FormLabel, FormRow, Page, PasswordInput } from '@emeraldwallet/ui';
2021
import { CircularProgress, Typography, createStyles, makeStyles } from '@material-ui/core';
@@ -70,7 +71,6 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>
7071
addresses,
7172
blockchain,
7273
coinTicker,
73-
contractAddress,
7474
entry: { address },
7575
isHardware,
7676
supportEip1559,
@@ -90,6 +90,7 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>
9090
const mounted = React.useRef(true);
9191

9292
const [initializing, setInitializing] = React.useState(true);
93+
const [preparing, setPreparing] = React.useState(false);
9394
const [verifying, setVerifying] = React.useState(false);
9495

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

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

112-
const zeroAmount = amountFactory(blockchain)(0) as WeiAny;
113+
const factory = amountFactory(blockchain) as CreateAmount<WeiAny>;
114+
const zeroAmount = factory(0);
113115

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

130-
const oldAmount = React.useRef(convertTx.amount);
131-
132132
const onChangeConvertable = (event: React.MouseEvent<HTMLElement>, value: string): void => {
133133
const converting = value ?? convertable;
134134

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

137137
const { number: amount } = tx.amount;
138138

139-
tx.amount = converting === coinTicker ? amountFactory(blockchain)(amount) : token.getAmount(amount);
139+
tx.amount = converting === coinTicker ? factory(amount) : token.getAmount(amount);
140140
tx.target = workflow.TxTarget.MANUAL;
141141
tx.rebalance();
142142

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

167-
const onClickMaxAmount = (): void => {
167+
const onClickMaxAmount = (callback: (value: BigAmount) => void): void => {
168168
const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx);
169169

170170
tx.target = workflow.TxTarget.SEND_ALL;
171171
tx.rebalance();
172172

173+
callback(tx.amount);
174+
175+
setConvertTx(tx.dump());
176+
};
177+
178+
const onUseEip1559Change = (checked: boolean): void => {
179+
setUseEip1559(checked);
180+
181+
const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx);
182+
183+
if (checked) {
184+
tx.gasPrice = undefined;
185+
tx.maxGasPrice = maxGasPrice;
186+
tx.priorityGasPrice = maxGasPrice;
187+
} else {
188+
tx.gasPrice = maxGasPrice;
189+
tx.maxGasPrice = undefined;
190+
tx.priorityGasPrice = undefined;
191+
}
192+
193+
tx.type = checked ? EthereumTransactionType.EIP1559 : EthereumTransactionType.LEGACY;
194+
173195
setConvertTx(tx.dump());
174196
};
175197

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

227+
const onCreateTransaction = async (): Promise<void> => {
228+
setPreparing(true);
229+
230+
const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx);
231+
232+
tx.gas = await estimateGas(tx.build());
233+
tx.rebalance();
234+
235+
setConvertTx(tx.dump());
236+
setStage(Stages.SIGN);
237+
238+
setPreparing(false);
239+
};
240+
205241
const onSignTransaction = async (): Promise<void> => {
206242
setPasswordError(undefined);
207243

@@ -246,32 +282,10 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>
246282
}
247283
};
248284

249-
const onUseEip1559Change = (checked: boolean): void => {
250-
setUseEip1559(checked);
251-
252-
const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx);
253-
254-
if (checked) {
255-
tx.gasPrice = undefined;
256-
tx.maxGasPrice = maxGasPrice;
257-
tx.priorityGasPrice = maxGasPrice;
258-
} else {
259-
tx.gasPrice = maxGasPrice;
260-
tx.maxGasPrice = undefined;
261-
tx.priorityGasPrice = undefined;
262-
}
263-
264-
tx.type = checked ? EthereumTransactionType.EIP1559 : EthereumTransactionType.LEGACY;
265-
266-
setConvertTx(tx.dump());
267-
};
268-
269285
React.useEffect(
270286
() => {
271287
getFees(blockchain).then(({ avgLast, avgMiddle, avgTail5 }) => {
272288
if (mounted.current) {
273-
const factory = amountFactory(blockchain);
274-
275289
const newStdMaxGasPrice = factory(avgTail5.max) as WeiAny;
276290
const newStdPriorityGasPrice = factory(avgTail5.priority) as WeiAny;
277291

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

312-
React.useEffect(() => {
313-
if (oldAmount.current == null || convertTx.amount?.equals(oldAmount.current) === false) {
314-
oldAmount.current = convertTx.amount;
315-
316-
const total =
317-
convertTx.totalBalance != null && convertTx.amount?.units.equals(convertTx.totalBalance.units) === true
318-
? convertTx.totalBalance
319-
: convertTx.totalTokenBalance;
320-
321-
if (total?.number != null && convertTx.amount?.number.isLessThanOrEqualTo(total.number) === true) {
322-
(async (): Promise<void> => {
323-
const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx);
324-
325-
tx.gas = await estimateGas(tx.build());
326-
tx.rebalance();
327-
328-
setConvertTx(tx.dump());
329-
})();
330-
}
331-
}
332-
}, [blockchain, contractAddress, convertable, convertTx, estimateGas]);
333-
334326
React.useEffect(() => {
335327
return () => {
336328
mounted.current = false;
@@ -375,6 +367,7 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>
375367
/>
376368
</FormRow>
377369
<EthTxSettings
370+
factory={factory}
378371
initializing={initializing}
379372
supportEip1559={supportEip1559}
380373
useEip1559={useEip1559}
@@ -399,9 +392,9 @@ const CreateConvertTransaction: React.FC<OwnProps & StateProps & DispatchProps>
399392
<Button label="Cancel" onClick={goBack} />
400393
<Button
401394
primary
402-
disabled={initializing || currentTx.amount.isZero()}
395+
disabled={initializing || preparing || currentTx.validate() !== ValidationResult.OK}
403396
label="Create Transaction"
404-
onClick={(): void => setStage(Stages.SIGN)}
397+
onClick={onCreateTransaction}
405398
/>
406399
</ButtonGroup>
407400
</FormRow>

0 commit comments

Comments
 (0)