Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement send max to send flow #12754

14 changes: 14 additions & 0 deletions app/actions/transaction/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,17 @@ export function setProposedNonce(proposedNonce) {
proposedNonce,
};
}

export function setMaxValueMode(maxValueMode) {
return {
type: 'SET_MAX_VALUE_MODE',
maxValueMode,
};
}

export function setTransactionValue(value) {
return {
type: 'SET_TRANSACTION_VALUE',
value,
};
}
18 changes: 16 additions & 2 deletions app/components/Views/confirmations/SendFlow/Amount/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
prepareTransaction,
setTransactionObject,
resetTransaction,
setMaxValueMode,
} from '../../../../../actions/transaction';
import { getSendFlowTitle } from '../../../../UI/Navbar';
import StyledButton from '../../../../UI/StyledButton';
Expand Down Expand Up @@ -485,6 +486,10 @@
* Type of gas fee estimate provided by the gas fee controller.
*/
gasEstimateType: PropTypes.string,
/**
* Function that sets the max value mode
*/
setMaxValueMode: PropTypes.func,
};
OGPoyraz marked this conversation as resolved.
Show resolved Hide resolved

state = {
Expand Down Expand Up @@ -926,9 +931,17 @@
};

onInputChange = (inputValue, selectedAsset, useMax) => {
const { contractExchangeRates, conversionRate, currentCurrency, ticker } =
this.props;
const {
contractExchangeRates,
conversionRate,
currentCurrency,
ticker,
setMaxValueMode,
} = this.props;
const { internalPrimaryCurrencyIsCrypto } = this.state;

setMaxValueMode(useMax ?? false)

Check warning on line 943 in app/components/Views/confirmations/SendFlow/Amount/index.js

View workflow job for this annotation

GitHub Actions / scripts (lint)

Missing semicolon

let inputValueConversion,
renderableInputValueConversion,
hasExchangeRate,
Expand Down Expand Up @@ -1567,6 +1580,7 @@
setSelectedAsset: (selectedAsset) =>
dispatch(setSelectedAsset(selectedAsset)),
resetTransaction: () => dispatch(resetTransaction()),
setMaxValueMode: (maxValueMode) => dispatch(setMaxValueMode(maxValueMode)),
});

export default connect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import TransactionTypes from '../../../../../core/TransactionTypes';
import { AmountViewSelectorsIDs } from '../../../../../../e2e/selectors/SendFlow/AmountView.selectors';

import { backgroundState } from '../../../../../util/test/initial-root-state';
import { setMaxValueMode } from '../../../../../actions/transaction';

const mockTransactionTypes = TransactionTypes;

Expand Down Expand Up @@ -67,6 +68,13 @@ jest.mock('../../../../../util/transaction-controller', () => ({
),
}));

jest.mock('../../../../../actions/transaction', () => ({
...jest.requireActual('../../../../../actions/transaction'),
setMaxValueMode: jest.fn().mockReturnValue({
type: 'SET_MAX_VALUE_MODE',
}),
}));

const mockNavigate = jest.fn();

const CURRENT_ACCOUNT = '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3';
Expand Down Expand Up @@ -236,6 +244,15 @@ describe('Amount', () => {
expect(toJSON()).toMatchSnapshot();
});

it('should set max value mode when toggled on', () => {
const { getByText } = renderComponent(initialState);

const useMaxButton = getByText(/Use max/);
fireEvent.press(useMaxButton);

expect(setMaxValueMode).toHaveBeenCalled();
});

it('should proceed if balance is sufficient while on Native primary currency', async () => {
const { getByText, getByTestId, toJSON } = renderComponent({
engine: {
Expand Down
103 changes: 66 additions & 37 deletions app/components/Views/confirmations/SendFlow/Confirm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
setNonce,
setProposedNonce,
setTransactionId,
setTransactionValue,
} from '../../../../../actions/transaction';
import { getGasLimit } from '../../../../../util/custom-gas';
import Engine from '../../../../../core/Engine';
Expand Down Expand Up @@ -135,6 +136,7 @@ import {
validateSufficientBalance,
} from './validation';
import { buildTransactionParams } from '../../../../../util/confirmation/transactions';
import { updateTransactionToMaxValue } from './utils';

const EDIT = 'edit';
const EDIT_NONCE = 'edit_nonce';
Expand Down Expand Up @@ -284,6 +286,14 @@ class Confirm extends PureComponent {
* Object containing blockaid validation response for confirmation
*/
securityAlertResponse: PropTypes.object,
/**
* Boolean that indicates if the max value mode is enabled
*/
maxValueMode: PropTypes.bool,
/**
* Function that sets the transaction value
*/
setTransactionValue: PropTypes.func,
};

state = {
Expand Down Expand Up @@ -318,7 +328,8 @@ class Confirm extends PureComponent {
);

setNetworkNonce = async () => {
const { networkClientId, setNonce, setProposedNonce, transaction } = this.props;
const { networkClientId, setNonce, setProposedNonce, transaction } =
this.props;
const proposedNonce = await getNetworkNonce(transaction, networkClientId);
setNonce(proposedNonce);
setProposedNonce(proposedNonce);
Expand Down Expand Up @@ -349,8 +360,8 @@ class Confirm extends PureComponent {
request_source: this.originIsMMSDKRemoteConn
? AppConstants.REQUEST_SOURCES.SDK_REMOTE_CONN
: this.originIsWalletConnect
? AppConstants.REQUEST_SOURCES.WC
: AppConstants.REQUEST_SOURCES.IN_APP_BROWSER,
? AppConstants.REQUEST_SOURCES.WC
: AppConstants.REQUEST_SOURCES.IN_APP_BROWSER,
is_smart_transaction: shouldUseSmartTransaction || false,
};

Expand Down Expand Up @@ -544,13 +555,20 @@ class Confirm extends PureComponent {

componentDidUpdate = (prevProps, prevState) => {
const {
accounts,
transactionState: {
transactionTo,
transaction: { value, gas },
transaction: { value, gas, from },
},
contractBalances,
selectedAsset,
maxValueMode,
gasFeeEstimates,
} = this.props;

const { transactionMeta } = this.state;
const { id: transactionId } = transactionMeta;

this.updateNavBar();

const transaction = this.prepareTransactionToSend();
Expand All @@ -573,6 +591,13 @@ class Confirm extends PureComponent {
const haveEIP1559TotalMaxHexChanged =
EIP1559GasTransaction.totalMaxHex !==
prevState.EIP1559GasTransaction.totalMaxHex;
const isEIP1559Transaction =
this.props.gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET;
const haveGasFeeMaxNativeChanged = isEIP1559Transaction
? EIP1559GasTransaction.gasFeeMaxNative !==
prevState.EIP1559GasTransaction.gasFeeMaxNative
: legacyGasTransaction.gasFeeMaxNative !==
prevState.legacyGasTransaction.gasFeeMaxNative;

const haveGasPropertiesChanged =
(this.props.gasFeeEstimates &&
Expand Down Expand Up @@ -602,14 +627,33 @@ class Confirm extends PureComponent {
? AppConstants.GAS_OPTIONS.MEDIUM
: this.state.gasSelected;

if (
maxValueMode &&
selectedAsset.isETH &&
OGPoyraz marked this conversation as resolved.
Show resolved Hide resolved
!isEmpty(gasFeeEstimates) &&
haveGasFeeMaxNativeChanged
) {
updateTransactionToMaxValue({
transactionId,
isEIP1559Transaction,
EIP1559GasTransaction,
legacyGasTransaction,
accountBalance: accounts[from].balance,
setTransactionValue: this.props.setTransactionValue,
});

// In order to prevent race condition do not remove this early return.
// Another update will be triggered by `updateEditableParams` and validateAmount will be called next update.
return;
}

if (
(!this.state.stopUpdateGas && !this.state.advancedGasInserted) ||
gasEstimateTypeChanged
) {
if (this.props.gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
error = this.validateAmount({
transaction,
total: EIP1559GasTransaction.totalMaxHex,
});
this.setError(error);
// eslint-disable-next-line react/no-did-update-set-state
Expand Down Expand Up @@ -639,7 +683,6 @@ class Confirm extends PureComponent {
} else {
error = this.validateAmount({
transaction,
total: legacyGasTransaction.totalHex,
});
this.setError(error);
}
Expand Down Expand Up @@ -803,7 +846,7 @@ class Confirm extends PureComponent {
* Validates transaction balances
* @returns - Whether there is an error with the amount
*/
validateAmount = ({ transaction, total }) => {
validateAmount = ({ transaction }) => {
const {
accounts,
contractBalances,
Expand All @@ -819,7 +862,7 @@ class Confirm extends PureComponent {

const selectedAddress = transaction?.from;
const weiBalance = hexToBN(accounts[selectedAddress].balance);
const totalTransactionValue = hexToBN(total);
const totalTransactionValue = hexToBN(value);

if (!isDecimal(value)) {
return strings('transaction.invalid_amount');
Expand Down Expand Up @@ -911,20 +954,14 @@ class Confirm extends PureComponent {
transactionState: { assetType },
navigation,
resetTransaction,
gasEstimateType,
shouldUseSmartTransaction,
transactionMetadata,
} = this.props;

const transactionSimulationData = transactionMetadata?.simulationData;
const { isUpdatedAfterSecurityCheck } = transactionSimulationData ?? {};

const {
legacyGasTransaction,
transactionConfirmed,
EIP1559GasTransaction,
isChangeInSimulationModalShown,
} = this.state;
const { transactionConfirmed, isChangeInSimulationModalShown } = this.state;
if (transactionConfirmed) return;

if (isUpdatedAfterSecurityCheck && !isChangeInSimulationModalShown) {
Expand All @@ -948,18 +985,9 @@ class Confirm extends PureComponent {
try {
const transaction = this.prepareTransactionToSend();

let error;
if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
error = this.validateAmount({
transaction,
total: EIP1559GasTransaction.totalMaxHex,
});
} else {
error = this.validateAmount({
transaction,
total: legacyGasTransaction.totalHex,
});
}
const error = this.validateAmount({
transaction,
});
this.setError(error);
if (error) {
this.setState({ transactionConfirmed: false, stopUpdateGas: true });
Expand Down Expand Up @@ -1245,15 +1273,15 @@ class Confirm extends PureComponent {
closeModal: true,
...(txnType
? {
legacyGasTransaction: gasTxn,
legacyGasObject: gasObj,
advancedGasInserted: !gasSelect,
stopUpdateGas: false,
}
legacyGasTransaction: gasTxn,
legacyGasObject: gasObj,
advancedGasInserted: !gasSelect,
stopUpdateGas: false,
}
: {
EIP1559GasTransaction: gasTxn,
EIP1559GasObject: gasObj,
}),
EIP1559GasTransaction: gasTxn,
EIP1559GasObject: gasObj,
}),
});
};

Expand Down Expand Up @@ -1576,10 +1604,10 @@ const mapStateToProps = (state) => ({
),
shouldUseSmartTransaction: selectShouldUseSmartTransaction(state),
transactionMetricsById: selectTransactionMetrics(state),
transactionMetadata:
selectCurrentTransactionMetadata(state),
transactionMetadata: selectCurrentTransactionMetadata(state),
useTransactionSimulations: selectUseTransactionSimulations(state),
securityAlertResponse: selectCurrentTransactionSecurityAlertResponse(state),
maxValueMode: state.transaction.maxValueMode,
});

const mapDispatchToProps = (dispatch) => ({
Expand All @@ -1595,6 +1623,7 @@ const mapDispatchToProps = (dispatch) => ({
showAlert: (config) => dispatch(showAlert(config)),
updateTransactionMetrics: ({ transactionId, params }) =>
dispatch(updateTransactionMetrics({ transactionId, params })),
setTransactionValue: (value) => dispatch(setTransactionValue(value)),
});

export default connect(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { updateTransactionToMaxValue } from './utils';
import { BN } from 'ethereumjs-util';
import { toWei } from '../../../../../util/number';

// Mock the Engine and its context
jest.mock('../../../../../util/transaction-controller', () => ({
updateEditableParams: jest.fn().mockResolvedValue({
txParams: { value: '0x0' },
}),
}));

describe('updateTransactionToMaxValue', () => {
it('should update the transaction value correctly', async () => {
const transactionId = 'testTransactionId';
const isEIP1559Transaction = true;
const EIP1559GasTransaction = { gasFeeMaxNative: '0.01' };
const legacyGasTransaction = { gasFeeMaxNative: '0.02' };
const accountBalance = '0x2386f26fc10000'; // 0.1 ether in wei
const setTransactionValue = jest.fn();

await updateTransactionToMaxValue({
transactionId,
isEIP1559Transaction,
EIP1559GasTransaction,
legacyGasTransaction,
accountBalance,
setTransactionValue,
});

// Calculate expected max transaction value
const accountBalanceBN = new BN('2386f26fc10000', 16); // 0.1 ether in wei
const transactionFeeMax = new BN(toWei('0.01', 'ether'), 10);
const expectedMaxTransactionValueBN =
accountBalanceBN.sub(transactionFeeMax);
const expectedMaxTransactionValueHex =
'0x' + expectedMaxTransactionValueBN.toString(16);

// Check if setTransactionValue was called with the correct value
expect(setTransactionValue).toHaveBeenCalledWith(
expectedMaxTransactionValueHex,
);
});
});
Loading
Loading